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