hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

date.ha (15346B)


      1 // License: MPL-2.0
      2 // (c) 2021-2022 Byron Torres <b@torresjrjr.com>
      3 // (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net>
      4 use errors;
      5 use time::chrono;
      6 
      7 // Hare internally uses the Unix epoch (1970-01-01) for calendrical logic. Here
      8 // we provide useful constant for working with the astronomically numbered
      9 // proleptic Gregorian calendar, as offsets from the Hare epoch.
     10 
     11 // The Hare epoch of the Julian Day Number.
     12 export def EPOCHAL_JULIAN: i64 = -2440588;
     13 
     14 // The Hare epoch of the Gregorian Common Era.
     15 export def EPOCHAL_GREGORIAN: i64 = -719164;
     16 
     17 // Calculates whether a year is a leap year.
     18 export fn is_leap_year(y: int) bool = {
     19 	return if (y % 4 != 0) false
     20 	else if (y % 100 != 0) true
     21 	else if (y % 400 != 0) false
     22 	else true;
     23 };
     24 
     25 // Calculates whether a given year, month, and day-of-month, is a valid date.
     26 fn is_valid_ymd(y: int, m: int, d: int) bool = {
     27 	return m >= 1 && m <= 12 && d >= 1 &&
     28 		d <= calc_n_days_in_month(y, m);
     29 };
     30 
     31 // Calculates whether a given year, and day-of-year, is a valid date.
     32 fn is_valid_yd(y: int, yd: int) bool = {
     33 	return yd >= 1 && yd <= calc_n_days_in_year(y);
     34 };
     35 
     36 // Calculates the number of days in the given month of the given year.
     37 fn calc_n_days_in_month(y: int, m: int) int = {
     38 	const days_per_month: [_]int = [
     39 		31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
     40 	];
     41 	if (m == 2) {
     42 		if (is_leap_year(y)) {
     43 			return 29;
     44 		} else {
     45 			return 28;
     46 		};
     47 	} else {
     48 		return days_per_month[m - 1];
     49 	};
     50 };
     51 
     52 // Calculates the number of days in a given year.
     53 fn calc_n_days_in_year(y: int) int = {
     54 	if (is_leap_year(y)) {
     55 		return 366;
     56 	} else {
     57 		return 365;
     58 	};
     59 };
     60 
     61 // Calculates the day-of-week of January 1st, given a year.
     62 fn calc_janfirstweekday(y: int) int = {
     63 	const y = (y % 400) + 400; // keep year > 0 (using Gegorian cycle)
     64 	// Gauss' algorithm
     65 	const wd = (
     66 		+ 5 * ((y - 1) % 4)
     67 		+ 4 * ((y - 1) % 100)
     68 		+ 6 * ((y - 1) % 400)
     69 	) % 7;
     70 	return wd + 1;
     71 };
     72 
     73 // Calculates the era, given a year.
     74 fn calc_era(y: int) int = {
     75 	return if (y >= 0) {
     76 		yield 1; // CE "Common Era"
     77 	} else {
     78 		yield 0; // BCE "Before Common Era"
     79 	};
     80 };
     81 
     82 // Calculates the year, month, and day-of-month, given an epochal day.
     83 fn calc_ymd(e: chrono::date) (int, int, int) = {
     84 	// Algorithm adapted from:
     85 	// https://en.wikipedia.org/wiki/Julian_day#Julian_or_Gregorian_calendar_from_Julian_day_number
     86 	//
     87 	// Alternate methods of date calculation should be explored.
     88 	const J = e - EPOCHAL_JULIAN;
     89 
     90 	// TODO: substitute numbers where possible
     91 	const b = 274277;
     92 	const c = -38;
     93 	const j = 1401;
     94 	const m = 2;
     95 	const n = 12;
     96 	const p = 1461;
     97 	const r = 4;
     98 	const s = 153;
     99 	const u = 5;
    100 	const v = 3;
    101 	const w = 2;
    102 	const y = 4716;
    103 
    104 	const f = J + j + (((4 * J + b) / 146097) * 3) / 4 + c;
    105 	const a = r * f + v;
    106 	const g = (a % p) / r;
    107 	const h = u * g + w;
    108 
    109 	const D = (h % s) / u + 1;
    110 	const M = ((h / s + m) % n) + 1;
    111 	const Y = (a / p) - y + (n + m - M) / n;
    112 
    113 	return (Y: int, M: int, D: int);
    114 };
    115 
    116 // Calculates the day-of-year, given a year, month, and day-of-month.
    117 fn calc_yearday(y: int, m: int, d: int) int = {
    118 	const months_firsts: [_]int = [
    119 		0, 31, 59,
    120 		90, 120, 151,
    121 		181, 212, 243,
    122 		273, 304, 334,
    123 	];
    124 
    125 	if (m >= 3 && is_leap_year(y)) {
    126 		return months_firsts[m - 1] + d + 1;
    127 	} else {
    128 		return months_firsts[m - 1] + d;
    129 	};
    130 };
    131 
    132 // Calculates the ISO week-numbering year,
    133 // given a year, month, day-of-month, and day-of-week.
    134 fn calc_isoweekyear(y: int, m: int, d: int, wd: int) int = {
    135 	if (
    136 		// if the date is within a week whose Thurday
    137 		// belongs to the previous gregorian year
    138 		m == 1 && (
    139 			(d == 1 && (wd == 5 || wd == 6 || wd == 7))
    140 			|| (d == 2 && (wd == 6 || wd == 7))
    141 			|| (d == 3 && wd == 7)
    142 		)
    143 	) {
    144 		return y - 1;
    145 	} else if (
    146 		// if the date is within a week whose Thurday
    147 		// belongs to the next gregorian year
    148 		m == 12 && (
    149 			(d == 29 && wd == 1)
    150 			|| (d == 30 && (wd == 1 || wd == 2))
    151 			|| (d == 31 && (wd == 1 || wd == 2 || wd == 3))
    152 		)
    153 	) {
    154 		return y + 1;
    155 	} else {
    156 		return y;
    157 	};
    158 };
    159 
    160 // Calculates the ISO week,
    161 // given a year, week, day-of-week, and day-of-year.
    162 fn calc_isoweek(y: int, w: int) int = {
    163 	const jan1wd = calc_janfirstweekday(y);
    164 	const iw = if (jan1wd == 1) {
    165 		yield w;
    166 	} else if (jan1wd == 2 || jan1wd == 3 || jan1wd == 4) {
    167 		yield w + 1;
    168 	} else {
    169 		yield if (w == 0) {
    170 			yield if (jan1wd == 5) {
    171 				yield 53;
    172 			} else if (jan1wd == 6) {
    173 				yield if (is_leap_year(y - 1)) {
    174 					yield 53;
    175 				} else {
    176 					yield 52;
    177 				};
    178 			} else if (jan1wd == 7) {
    179 				yield 52;
    180 			} else {
    181 				// all jan1wd values exhausted
    182 				abort("Unreachable");
    183 			};
    184 		} else {
    185 			yield w;
    186 		};
    187 	};
    188 	return iw;
    189 };
    190 
    191 // Calculates the week within a Gregorian year [0..53],
    192 // given a day-of-year and day-of-week.
    193 // All days in a year before the year's first Monday belong to week 0.
    194 fn calc_week(yd: int, wd: int) int = {
    195 	return (yd + 7 - wd) / 7;
    196 };
    197 
    198 // Calculates the week within a Gregorian year [0..53],
    199 // given a day-of-year and day-of-week.
    200 // All days in a year before the year's first Sunday belong to week 0.
    201 fn calc_sundayweek(yd: int, wd: int) int = {
    202 	return (yd + 6 - (wd % 7)) / 7;
    203 };
    204 
    205 // Calculates the day-of-week, given a epochal day,
    206 // from Monday=1 to Sunday=7.
    207 fn calc_weekday(e: chrono::date) int = {
    208 	const wd = ((e + 3) % 7 + 1): int;
    209 	return if (wd > 0) wd else wd + 7;
    210 };
    211 
    212 // Calculates the zero-indexed day-of-week, given a day-of-week,
    213 // from Monday=0 to Sunday=6.
    214 fn calc_zeroweekday(wd: int) int = {
    215 	return wd - 1;
    216 };
    217 
    218 // Calculates the [[chrono::date]],
    219 // given a year, month, and day-of-month.
    220 fn calc_date_from_ymd(y: int, m: int, d: int) (chrono::date | invalid) = {
    221 	if (!is_valid_ymd(y, m, d)) {
    222 		return invalid;
    223 	};
    224 	// Algorithm adapted from:
    225 	// https://en.wikipedia.org/wiki/Julian_day
    226 	//
    227 	// TODO: Review, cite, verify, annotate.
    228 	const jdn = (
    229 		(1461 * (y + 4800 + (m - 14) / 12)) / 4
    230 		+ (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12
    231 		- (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4
    232 		+ d
    233 		- 32075
    234 	);
    235 	const e = jdn + EPOCHAL_JULIAN;
    236 	return e;
    237 };
    238 
    239 // Calculates the [[chrono::date]],
    240 // given a year, week, and day-of-week.
    241 fn calc_date_from_ywd(y: int, w: int, wd: int) (chrono::date | invalid) = {
    242 	const jan1wd = calc_janfirstweekday(y);
    243 	const yd = wd - jan1wd + 1 + 7 * w;
    244 	return calc_date_from_yd(y, yd)?;
    245 };
    246 
    247 // Calculates the [[chrono::date]],
    248 // given a year and day-of-year.
    249 fn calc_date_from_yd(y: int, yd: int) (chrono::date | invalid) = {
    250 	if (yd < 1 || yd > calc_n_days_in_year(y)) {
    251 		return invalid;
    252 	};
    253 	return calc_date_from_ymd(y, 1, 1)? + yd - 1;
    254 };
    255 
    256 @test fn calc_date_from_ymd() void = {
    257 	const cases = [
    258 		((-0768, 02, 05),  -999999, false),
    259 		((-0001, 12, 31),  -719529, false),
    260 		(( 0000, 01, 01),  -719528, false),
    261 		(( 0000, 01, 02),  -719527, false),
    262 		(( 0000, 12, 31),  -719163, false),
    263 		(( 0001, 01, 01),  -719162, false),
    264 		(( 0001, 01, 02),  -719161, false),
    265 		(( 1965, 03, 23),    -1745, false),
    266 		(( 1969, 12, 31),       -1, false),
    267 		(( 1970, 01, 01),        0, false),
    268 		(( 1970, 01, 02),        1, false),
    269 		(( 1999, 12, 31),    10956, false),
    270 		(( 2000, 01, 01),    10957, false),
    271 		(( 2000, 01, 02),    10958, false),
    272 		(( 2038, 01, 18),    24854, false),
    273 		(( 2038, 01, 19),    24855, false),
    274 		(( 2038, 01, 20),    24856, false),
    275 		(( 2243, 10, 17),   100000, false),
    276 		(( 4707, 11, 28),   999999, false),
    277 		(( 4707, 11, 29),  1000000, false),
    278 		((29349, 01, 25),  9999999, false),
    279 
    280 		(( 1970,-99,-99),  0, true),
    281 		(( 1970, -9, -9),  0, true),
    282 		(( 1970, -1, -1),  0, true),
    283 		(( 1970, 00, 00),  0, true),
    284 		(( 1970, 00, 01),  0, true),
    285 		(( 1970, 01, 99),  0, true),
    286 		(( 1970, 99, 99),  0, true),
    287 	];
    288 	for (let i = 0z; i < len(cases); i += 1) {
    289 		const params = cases[i].0;
    290 		const expect = cases[i].1;
    291 		const should_error = cases[i].2;
    292 		const actual = calc_date_from_ymd(
    293 			params.0, params.1, params.2,
    294 		);
    295 
    296 		if (should_error) {
    297 			assert(actual is invalid, "invalid date accepted");
    298 		} else {
    299 			assert(actual is chrono::date, "valid date not accepted");
    300 			assert(actual as chrono::date == expect, "date miscalculation");
    301 		};
    302 	};
    303 };
    304 
    305 @test fn calc_date_from_ywd() void = {
    306 	const cases = [
    307 		((-0768, 00, 4), -1000034),
    308 		((-0768, 05, 4), -999999),
    309 		((-0001, 52, 5), -719529),
    310 		(( 0000, 00, 6), -719528),
    311 		(( 0000, 00, 7), -719527),
    312 		(( 0000, 52, 7), -719163),
    313 		(( 0001, 00, 1), -719162),
    314 		(( 0001, 00, 2), -719161),
    315 		(( 1965, 12, 2), -1745),
    316 		(( 1969, 52, 3), -1),
    317 		(( 1970, 00, 4), 0),
    318 		(( 1970, 00, 5), 1),
    319 		(( 1999, 52, 5), 10956),
    320 		(( 2000, 00, 6), 10957),
    321 		(( 2000, 00, 7), 10958),
    322 		(( 2020, 00, 3), 18262),
    323 		(( 2022, 09, 1), 19051),
    324 		(( 2022, 09, 2), 19052),
    325 		(( 2023, 51, 7), 19715),
    326 		(( 2024, 08, 3), 19781),
    327 		(( 2024, 08, 4), 19782),
    328 		(( 2024, 08, 5), 19783),
    329 		(( 2024, 49, 4), 20069),
    330 		(( 2024, 52, 2), 20088),
    331 		(( 2038, 03, 1), 24854),
    332 		(( 2038, 03, 2), 24855),
    333 		(( 2038, 03, 3), 24856),
    334 		(( 2243, 41, 2), 99993),
    335 		(( 4707, 47, 4), 999999),
    336 		(( 4707, 47, 5), 1000000),
    337 		((29349, 03, 6), 9999999),
    338 	];
    339 
    340 	for (let i = 0z; i < len(cases); i += 1) {
    341 		const ywd = cases[i].0;
    342 		const expected = cases[i].1;
    343 		const actual = calc_date_from_ywd(ywd.0, ywd.1, ywd.2)!;
    344 		assert(actual == expected,
    345 			"incorrect calc_date_from_ywd() result");
    346 	};
    347 };
    348 
    349 @test fn calc_date_from_yd() void = {
    350 	const cases = [
    351 		(-0768, 36,  -999999),
    352 		(-0001, 365, -719529),
    353 		( 0000, 1,   -719528),
    354 		( 0000, 2,   -719527),
    355 		( 0000, 366, -719163),
    356 		( 0001, 1,   -719162),
    357 		( 0001, 2,   -719161),
    358 		( 1965, 82,  -1745  ),
    359 		( 1969, 365, -1     ),
    360 		( 1970, 1,   0      ),
    361 		( 1970, 2,   1      ),
    362 		( 1999, 365, 10956  ),
    363 		( 2000, 1,   10957  ),
    364 		( 2000, 2,   10958  ),
    365 		( 2038, 18,  24854  ),
    366 		( 2038, 19,  24855  ),
    367 		( 2038, 20,  24856  ),
    368 		( 2243, 290, 100000 ),
    369 		( 4707, 332, 999999 ),
    370 		( 4707, 333, 1000000),
    371 		(29349, 25,  9999999),
    372 	];
    373 
    374 	for (let i = 0z; i < len(cases); i += 1) {
    375 		const y = cases[i].0;
    376 		const yd = cases[i].1;
    377 		const expected = cases[i].2;
    378 		const actual = calc_date_from_yd(y, yd)!;
    379 		assert(expected == actual,
    380 			"error in date calculation from yd");
    381 	};
    382 	assert(calc_date_from_yd(2020, 0) is invalid,
    383 		"calc_date_from_yd() did not reject invalid yearday");
    384 	assert(calc_date_from_yd(2020, 400) is invalid,
    385 		"calc_date_from_yd() did not reject invalid yearday");
    386 };
    387 
    388 @test fn calc_ymd() void = {
    389 	const cases = [
    390 		(-999999,  (-0768, 02, 05)),
    391 		(-719529,  (-0001, 12, 31)),
    392 		(-719528,  ( 0000, 01, 01)),
    393 		(-719527,  ( 0000, 01, 02)),
    394 		(-719163,  ( 0000, 12, 31)),
    395 		(-719162,  ( 0001, 01, 01)),
    396 		(-719161,  ( 0001, 01, 02)),
    397 		(  -1745,  ( 1965, 03, 23)),
    398 		(     -1,  ( 1969, 12, 31)),
    399 		(      0,  ( 1970, 01, 01)),
    400 		(      1,  ( 1970, 01, 02)),
    401 		(  10956,  ( 1999, 12, 31)),
    402 		(  10957,  ( 2000, 01, 01)),
    403 		(  10958,  ( 2000, 01, 02)),
    404 		(  24854,  ( 2038, 01, 18)),
    405 		(  24855,  ( 2038, 01, 19)),
    406 		(  24856,  ( 2038, 01, 20)),
    407 		( 100000,  ( 2243, 10, 17)),
    408 		( 999999,  ( 4707, 11, 28)),
    409 		(1000000,  ( 4707, 11, 29)),
    410 		(9999999,  (29349, 01, 25)),
    411 	];
    412 	for (let i = 0z; i < len(cases); i += 1) {
    413 		const paramt = cases[i].0;
    414 		const expect = cases[i].1;
    415 		const actual = calc_ymd(paramt);
    416 		assert(expect.0 == actual.0, "year mismatch");
    417 		assert(expect.1 == actual.1, "month mismatch");
    418 		assert(expect.2 == actual.2, "day mismatch");
    419 	};
    420 };
    421 
    422 @test fn calc_yearday() void = {
    423 	const cases = [
    424 		((-0768, 02, 05),  036),
    425 		((-0001, 12, 31),  365),
    426 		(( 0000, 01, 01),  001),
    427 		(( 0000, 01, 02),  002),
    428 		(( 0000, 12, 31),  366),
    429 		(( 0001, 01, 01),  001),
    430 		(( 0001, 01, 02),  002),
    431 		(( 1965, 03, 23),  082),
    432 		(( 1969, 12, 31),  365),
    433 		(( 1970, 01, 01),  001),
    434 		(( 1970, 01, 02),  002),
    435 		(( 1999, 12, 31),  365),
    436 		(( 2000, 01, 01),  001),
    437 		(( 2000, 01, 02),  002),
    438 		(( 2020, 02, 12),  043),
    439 		(( 2038, 01, 18),  018),
    440 		(( 2038, 01, 19),  019),
    441 		(( 2038, 01, 20),  020),
    442 		(( 2243, 10, 17),  290),
    443 		(( 4707, 11, 28),  332),
    444 		(( 4707, 11, 29),  333),
    445 		((29349, 01, 25),  025),
    446 	];
    447 	for (let i = 0z; i < len(cases); i += 1) {
    448 		const params = cases[i].0;
    449 		const expect = cases[i].1;
    450 		const actual = calc_yearday(params.0, params.1, params.2);
    451 		assert(expect == actual, "yearday miscalculation");
    452 	};
    453 };
    454 
    455 @test fn calc_week() void = {
    456 	const cases = [
    457 		((1, 1), 1),
    458 		((1, 2), 0),
    459 		((1, 3), 0),
    460 		((1, 4), 0),
    461 		((1, 5), 0),
    462 		((1, 6), 0),
    463 		((1, 7), 0),
    464 		((21, 2), 3),
    465 		((61, 3), 9),
    466 		((193, 5), 27),
    467 		((229, 1), 33),
    468 		((286, 4), 41),
    469 		((341, 7), 48),
    470 		((365, 6), 52),
    471 		((366, 1), 53),
    472 	];
    473 
    474 	for (let i = 0z; i < len(cases); i += 1) {
    475 		const params = cases[i].0;
    476 		const expect = cases[i].1;
    477 		const actual = calc_week(params.0, params.1);
    478 		assert(expect == actual, "week miscalculation");
    479 	};
    480 };
    481 
    482 @test fn calc_sundayweek() void = {
    483 	const cases = [
    484 		((1, 1), 0),
    485 		((1, 2), 0),
    486 		((1, 3), 0),
    487 		((1, 4), 0),
    488 		((1, 5), 0),
    489 		((1, 6), 0),
    490 		((1, 7), 1),
    491 		((21, 2), 3),
    492 		((61, 3), 9),
    493 		((193, 5), 27),
    494 		((229, 1), 33),
    495 		((286, 4), 41),
    496 		((341, 7), 49),
    497 		((365, 6), 52),
    498 		((366, 1), 53),
    499 	];
    500 
    501 	for (let i = 0z; i < len(cases); i += 1) {
    502 		const params = cases[i].0;
    503 		const expect = cases[i].1;
    504 		const actual = calc_sundayweek(params.0, params.1);
    505 		assert(expect == actual, "week miscalculation");
    506 	};
    507 };
    508 
    509 @test fn calc_weekday() void = {
    510 	const cases = [
    511 		(-999999,  4), // -0768-02-05
    512 		(-719529,  5), // -0001-12-31
    513 		(-719528,  6), //  0000-01-01
    514 		(-719527,  7), //  0000-01-02
    515 		(-719163,  7), //  0000-12-31
    516 		(-719162,  1), //  0001-01-01
    517 		(-719161,  2), //  0001-01-02
    518 		(  -1745,  2), //  1965-03-23
    519 		(     -1,  3), //  1969-12-31
    520 		(      0,  4), //  1970-01-01
    521 		(      1,  5), //  1970-01-02
    522 		(  10956,  5), //  1999-12-31
    523 		(  10957,  6), //  2000-01-01
    524 		(  10958,  7), //  2000-01-02
    525 		(  24854,  1), //  2038-01-18
    526 		(  24855,  2), //  2038-01-19
    527 		(  24856,  3), //  2038-01-20
    528 		( 100000,  2), //  2243-10-17
    529 		( 999999,  4), //  4707-11-28
    530 		(1000000,  5), //  4707-11-29
    531 		(9999999,  6), // 29349-01-25
    532 	];
    533 	for (let i = 0z; i < len(cases); i += 1) {
    534 		const paramt = cases[i].0;
    535 		const expect = cases[i].1;
    536 		const actual = calc_weekday(paramt);
    537 		assert(expect == actual, "weekday miscalculation");
    538 	};
    539 };
    540 
    541 @test fn calc_janfirstweekday() void = {
    542 	const cases = [
    543 	//	 year   weekday
    544 		(1969,  3),
    545 		(1970,  4),
    546 		(1971,  5),
    547 		(1972,  6),
    548 		(1973,  1),
    549 		(1974,  2),
    550 		(1975,  3),
    551 		(1976,  4),
    552 		(1977,  6),
    553 		(1978,  7),
    554 		(1979,  1),
    555 		(1980,  2),
    556 		(1981,  4),
    557 		(1982,  5),
    558 		(1983,  6),
    559 		(1984,  7),
    560 		(1985,  2),
    561 		(1986,  3),
    562 		(1987,  4),
    563 		(1988,  5),
    564 		(1989,  7),
    565 		(1990,  1),
    566 		(1991,  2),
    567 		(1992,  3),
    568 		(1993,  5),
    569 		(1994,  6),
    570 		(1995,  7),
    571 		(1996,  1),
    572 		(1997,  3),
    573 		(1998,  4),
    574 		(1999,  5),
    575 		(2000,  6),
    576 		(2001,  1),
    577 		(2002,  2),
    578 		(2003,  3),
    579 		(2004,  4),
    580 		(2005,  6),
    581 		(2006,  7),
    582 		(2007,  1),
    583 		(2008,  2),
    584 		(2009,  4),
    585 		(2010,  5),
    586 		(2011,  6),
    587 		(2012,  7),
    588 		(2013,  2),
    589 		(2014,  3),
    590 		(2015,  4),
    591 		(2016,  5),
    592 		(2017,  7),
    593 		(2018,  1),
    594 		(2019,  2),
    595 		(2020,  3),
    596 		(2021,  5),
    597 		(2022,  6),
    598 		(2023,  7),
    599 		(2024,  1),
    600 		(2025,  3),
    601 		(2026,  4),
    602 		(2027,  5),
    603 		(2028,  6),
    604 		(2029,  1),
    605 		(2030,  2),
    606 		(2031,  3),
    607 		(2032,  4),
    608 		(2033,  6),
    609 		(2034,  7),
    610 		(2035,  1),
    611 		(2036,  2),
    612 		(2037,  4),
    613 		(2038,  5),
    614 		(2039,  6),
    615 	];
    616 	for (let i = 0z; i < len(cases); i += 1) {
    617 		const paramt = cases[i].0;
    618 		const expect = cases[i].1;
    619 		const actual = calc_janfirstweekday(paramt);
    620 		assert(expect == actual, "calc_janfirstweekday() miscalculation");
    621 	};
    622 };