hare

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

daydate.ha (14793B)


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