hare

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

date.ha (14721B)


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