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