# 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 };
```