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