timescale.ha (18752B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use time; 5 6 // Represents a scale of time; a time standard. See [[convert]]. 7 export type timescale = struct { 8 name: str, 9 abbr: str, 10 convto: *tsconverter, 11 convfrom: *tsconverter, 12 }; 13 14 export type tsconverter = fn(ts: *timescale, i: time::instant) ([]time::instant | void); 15 16 // A discontinuity between two [[timescale]]s caused a one-to-one 17 // [[time::instant]] conversion to fail. 18 export type discontinuity = !void; 19 20 // The analytical result of a [[time::instant]] conversion between two 21 // [[timescale]]s at a point of [[discontinuity]]. 22 // 23 // An empty slice represents a nonexistent conversion result. 24 // A populated (>1) slice represents an ambiguous conversion result. 25 export type analytical = ![]time::instant; 26 27 // Converts a [[time::instant]] from one [[timescale]] to the next exhaustively. 28 // The final conversion result is returned. For each active pair of timescales, 29 // if neither implements conversion from the first to the second, a two-step 30 // intermediary TAI conversion will occur. If given zero or one timescales, the 31 // given instant is returned. 32 export fn convert(i: time::instant, tscs: *timescale...) (time::instant | analytical) = { 33 let ts: []time::instant = [i]; 34 let tmps: []time::instant = []; 35 36 for (let j = 1z; j < len(tscs); j += 1) { 37 let a = tscs[j - 1]; 38 let b = tscs[j]; 39 40 for (let k = 0z; k < len(ts); k += 1) { 41 const t = ts[k]; 42 43 // try .convto 44 match (a.convto(b, t)) { 45 case let convs: []time::instant => 46 append(tmps, convs...)!; 47 continue; 48 case void => void; 49 }; 50 51 // try .convfrom 52 match (b.convfrom(a, t)) { 53 case let convs: []time::instant => 54 append(tmps, convs...)!; 55 continue; 56 case void => void; 57 }; 58 59 // default to TAI intermediary 60 const convs = a.convto(&tai, t) as []time::instant; 61 62 for (let conv .. convs) { 63 append(tmps, ( 64 b.convfrom(&tai, conv) as []time::instant 65 )...)!; 66 }; 67 }; 68 69 // TODO: sort and deduplicate 'ts' here 70 ts = tmps; 71 tmps = []; 72 }; 73 74 return if (len(ts) == 1) ts[0] else ts; 75 }; 76 77 78 // International Atomic Time 79 // 80 // The realisation of proper time on Earth's geoid. 81 // Continuous (no leap seconds). 82 export const tai: timescale = timescale { 83 name = "International Atomic Time", 84 abbr = "TAI", 85 convto = &tai_conv, 86 convfrom = &tai_conv, 87 }; 88 89 fn tai_conv(ts: *timescale, i: time::instant) ([]time::instant | void) = { 90 if (ts == &tai) { 91 return alloc([i]...)!; 92 }; 93 }; 94 95 96 // Coordinated Universal Time 97 // 98 // Used as the basis of civil timekeeping. 99 // Based on TAI; time-dependent offset. 100 // Discontinuous (has leap seconds). 101 // 102 // During a program's initialization, this timescale initializes by loading its 103 // UTC/TAI leap second data from [[UTC_LEAPSECS_PATH]]; otherwise, fails 104 // silently. If failed, any attempt to consult UTC leapsec data (e.g. calling 105 // [[convert]] on UTC) causes an abort. This includes [[in]]. 106 export const utc: timescale = timescale { 107 name = "Coordinated Universal Time", 108 abbr = "UTC", 109 convto = &utc_convto, 110 convfrom = &utc_convfrom, 111 }; 112 113 fn utc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { 114 if (ts == &utc) { 115 return alloc([i]...)!; 116 } else if (ts == &tai) { 117 let ret: []time::instant = []; 118 if (!utc_isinitialized) { 119 match (init_utc_leapsecs()) { 120 case void => 121 utc_isinitialized = true; 122 case => 123 abort("Failed to initialize UTC timescale"); 124 }; 125 }; 126 127 const firstleap = utc_leapsecs[0]; // TODO: no leapsecs loaded 128 if (time::compare(i, time::from_unix(firstleap.0)) < 0) { 129 append(ret, time::instant { 130 sec = i.sec + firstleap.1, 131 nsec = i.nsec, 132 })!; 133 return ret; 134 }; 135 136 for (let idx = len(utc_leapsecs) - 1; idx >= 0 ; idx -= 1) { 137 const leap = utc_leapsecs[idx]; 138 const leapsecond = time::from_unix(leap.0); 139 const leapoffset = leap.1; 140 const diff = time::diff(leapsecond, i); 141 142 const prevleapoffset = 143 if (idx == 0) 0i64 else utc_leapsecs[idx - 1].1; 144 const offsetdiff = 145 (leapoffset - prevleapoffset) * time::SECOND; 146 147 // case of positive leap second (UTC repeats a second) 148 if (offsetdiff >= 0) { 149 if (diff >= 0) { 150 append(ret, time::instant { 151 sec = i.sec + leapoffset, 152 nsec = i.nsec, 153 })!; 154 return ret; 155 }; 156 157 if (diff >= -offsetdiff && diff < 0) { 158 append(ret, [ 159 time::instant { 160 sec = i.sec + prevleapoffset, 161 nsec = i.nsec, 162 }, 163 time::instant { 164 sec = i.sec + leapoffset, 165 nsec = i.nsec, 166 }, 167 ]...)!; 168 return ret; 169 }; 170 171 continue; 172 }; 173 174 // case of negative leap second (UTC skips a second) 175 if (offsetdiff < 0) { 176 if (diff >= 0) { 177 append(ret, time::instant { 178 sec = i.sec + leapoffset, 179 nsec = i.nsec, 180 })!; 181 return ret; 182 }; 183 184 if (diff >= offsetdiff && diff < 0) { 185 return ret; 186 }; 187 188 continue; 189 }; 190 }; 191 }; 192 }; 193 194 fn utc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { 195 if (ts == &utc) { 196 return alloc([i]...)!; 197 } else if (ts == &tai) { 198 let ret: []time::instant = []; 199 if (!utc_isinitialized) { 200 match (init_utc_leapsecs()) { 201 case void => 202 utc_isinitialized = true; 203 case => 204 abort("Failed to initialize UTC timescale"); 205 }; 206 }; 207 208 const firstleap = utc_leapsecs[0]; // TODO: no leapsecs loaded 209 if (time::compare(i, time::from_unix(firstleap.0 + firstleap.1)) < 0) { 210 append(ret, time::instant { 211 sec = i.sec - firstleap.1, 212 nsec = i.nsec, 213 })!; 214 return ret; 215 }; 216 217 for (let idx = len(utc_leapsecs) - 1; idx >= 0 ; idx -= 1) { 218 const leap = utc_leapsecs[idx]; 219 const leapsecond = time::from_unix(leap.0 + leap.1); 220 const leapoffset = leap.1; 221 const diff = time::diff(leapsecond, i); 222 223 const prevleapoffset = 224 if (idx == 0) 10i64 else utc_leapsecs[idx - 1].1; 225 const offsetdiff 226 = (leapoffset - prevleapoffset) * time::SECOND; 227 228 // case of positive leap second (UTC repeats a second) 229 if (offsetdiff >= 0) { 230 if (diff >= -offsetdiff) { 231 append(ret, time::instant { 232 sec = i.sec - leapoffset, 233 nsec = i.nsec, 234 })!; 235 return ret; 236 }; 237 238 continue; 239 }; 240 241 // case of negative leap second (UTC skips a second) 242 if (offsetdiff < 0) { 243 if (diff >= 0) { 244 append(ret, time::instant { 245 sec = i.sec - leapoffset, 246 nsec = i.nsec, 247 })!; 248 return ret; 249 }; 250 251 continue; 252 }; 253 }; 254 }; 255 }; 256 257 258 // Global Positioning System Time 259 // 260 // Used for GPS coordination. 261 // Based on TAI; constant -19 second offset. 262 // Continuous (no leap seconds). 263 export const gps: timescale = timescale { 264 name = "Global Positioning System Time", 265 abbr = "GPS", 266 convto = &gps_convto, 267 convfrom = &gps_convfrom, 268 }; 269 270 // The constant offset between GPS-Time (Global Positioning System Time) and TAI 271 // (International Atomic Time). Used by [[gps]]. 272 def GPS_OFFSET: time::duration = -19 * time::SECOND; 273 274 fn gps_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { 275 if (ts == &gps) { 276 return alloc([i]...)!; 277 } else if (ts == &tai) { 278 return alloc([time::add(i, -GPS_OFFSET)]...)!; 279 }; 280 }; 281 282 fn gps_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { 283 if (ts == &gps) { 284 return alloc([i]...)!; 285 } else if (ts == &tai) { 286 return alloc([time::add(i, GPS_OFFSET)]...)!; 287 }; 288 }; 289 290 291 // Terrestrial Time 292 // 293 // Used for astronomical timekeeping. 294 // Based on TAI; constant +32.184 offset. 295 // Continuous (no leap seconds). 296 export const tt: timescale = timescale { 297 name = "Terrestrial Time", 298 abbr = "TT", 299 convto = &tt_convto, 300 convfrom = &tt_convfrom, 301 }; 302 303 // The constant offset between TT (Terrestrial Time) and TAI (International 304 // Atomic Time). Used by [[tt]]. 305 def TT_OFFSET: time::duration = 32184 * time::MILLISECOND; // 32.184 seconds 306 307 fn tt_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { 308 if (ts == &tt) { 309 return alloc([i]...)!; 310 } else if (ts == &tai) { 311 return alloc([time::add(i, -TT_OFFSET)]...)!; 312 }; 313 }; 314 315 316 fn tt_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { 317 if (ts == &tt) { 318 return alloc([i]...)!; 319 } else if (ts == &tai) { 320 return alloc([time::add(i, TT_OFFSET)]...)!; 321 }; 322 }; 323 324 // Arthur David Olson had expressed support for Martian time in his timezone 325 // database project <https://data.iana.org/time-zones/theory.html>: 326 // 327 // > The tz database does not currently support Mars time, but it is documented 328 // > here in the hopes that support will be added eventually. 329 330 // Coordinated Mars Time 331 // 332 // Used for timekeeping on Mars. 333 // Based on TT; constant factor. 334 // Continuous (no leap seconds). 335 export const mtc: timescale = timescale { 336 name = "Coordinated Mars Time", 337 abbr = "MTC", 338 convto = &mtc_convto, 339 convfrom = &mtc_convfrom, 340 }; 341 342 // Factor f, where Martian-time * f == Earth-time. 343 def FACTOR_TERRESTRIAL_MARTIAN: f64 = 1.0274912517; 344 345 // [[time::duration]] in Earth-time between the Unix epoch of 1970 Jan 1st 346 // midnight, and the Earth-Mars convergence date of 2000 Jan 6th midnight. 347 def DELTA_UNIXEPOCH_JANSIX: time::duration = 10962 * 24 * time::HOUR; 348 349 // [[time::duration]] in Mars-time between the Mars Sol Date epoch corresponding 350 // to the Gregorian Earth date 1873 Dec 29th, and the Earth-Mars convergence 351 // date of 2000 Jan 6. 352 def DELTA_MARSEPOCH_JANSIX: time::duration = 44796 * 24 * time::HOUR; 353 354 // [[time::duration]] in Mars-time between the midnights of 2000 Jan 6th on 355 // Earth and Mars. Earth's midnight occurred first. 356 def DELTA_JANSIX_ADJUSTMENT: time::duration = 82944 * time::MILLISECOND; 357 358 fn mtc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = { 359 let ret: []time::instant = []; 360 if (ts == &mtc) { 361 return alloc([i]...)!; 362 } else if (ts == &tai) { 363 // Change epoch from that of the Mars Sol Date 364 // to the Earth-Mars convergence date 2000 Jan 6th. 365 let i = time::add(i, -DELTA_MARSEPOCH_JANSIX); 366 367 // Slightly adjust epoch for the actual Martian midnight. 368 // Earth's midnight occurred before Mars'. 369 i = time::add(i, DELTA_JANSIX_ADJUSTMENT); 370 371 // Scale from Mars-time to Earth-time. 372 i = time::mult(i, FACTOR_TERRESTRIAL_MARTIAN); 373 374 // Change epoch to the Unix epoch 1970 Jan 1st (Terrestrial Time). 375 i = time::add(i, DELTA_UNIXEPOCH_JANSIX); 376 377 // Get the TAI time. 378 // assertion since TT and TAI are continuous. 379 return alloc([(tt.convto(&tai, i) as []time::instant)[0]]...)!; 380 }; 381 }; 382 383 fn mtc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = { 384 if (ts == &mtc) { 385 return alloc([i]...)!; 386 } else if (ts == &tai) { 387 // Get the "Terrestrial Time". 388 // assertion since TT and TAI are continuous. 389 let i = (tt.convfrom(&tai, i) as []time::instant)[0]; 390 391 // Change epoch from the Unix epoch 1970 Jan 1st (Terrestrial Time) 392 // to the Earth-Mars convergence date 2000 Jan 6th midnight. 393 i = time::add(i, -DELTA_UNIXEPOCH_JANSIX); 394 395 // Scale from Earth-time to Mars-time. 396 i = time::mult(i, 1.0 / FACTOR_TERRESTRIAL_MARTIAN); 397 398 // Slightly adjust epoch for the actual Martian midnight. 399 // Earth's midnight occurred before Mars'. 400 i = time::add(i, -DELTA_JANSIX_ADJUSTMENT); 401 402 // Change epoch to that of the Mars Sol Date. 403 return alloc([time::add(i, DELTA_MARSEPOCH_JANSIX)]...)!; 404 }; 405 }; 406 407 408 @test fn utc_convto_tai() void = { 409 // TODO: skip test if no leapsec data available (!utc_isinitialized) 410 // TODO: test negative leapsecs somehow 411 let testcases: []( 412 (i64, i64), // give 413 (void | [0](i64, i64) | [1](i64, i64) | [2](i64, i64)) // expect 414 ) = [ 415 ((- 1000i64, 0i64), [(- 990i64, 0i64)]), 416 (( 0i64, 0i64), [( 10i64, 0i64)]), 417 (( 1000i64, 0i64), [( 1010i64, 0i64)]), 418 // 1970 Jan 01 419 (( 63071998i64, 0i64), [( 63072008i64, 0i64)]), 420 (( 63071998i64, 500000000i64), [( 63072008i64, 500000000i64)]), 421 (( 63071999i64, 0i64), [( 63072009i64, 0i64)]), 422 (( 63071999i64, 500000000i64), [( 63072009i64, 500000000i64)]), 423 (( 63072000i64, 0i64), [( 63072010i64, 0i64)]), 424 (( 63072000i64, 500000000i64), [( 63072010i64, 500000000i64)]), 425 (( 63072001i64, 0i64), [( 63072011i64, 0i64)]), 426 (( 63072001i64, 500000000i64), [( 63072011i64, 500000000i64)]), 427 (( 63072002i64, 0i64), [( 63072012i64, 0i64)]), 428 // 1981 Jul 01 429 (( 362793598i64, 0i64), [( 362793617i64, 0i64)]), 430 (( 362793598i64, 500000000i64), [( 362793617i64, 500000000i64)]), 431 (( 362793599i64, 0i64), [ 432 ( 362793618i64, 0i64), 433 ( 362793619i64, 0i64), 434 ]), 435 (( 362793599i64, 500000000i64), [ 436 ( 362793618i64, 500000000i64), 437 ( 362793619i64, 500000000i64), 438 ]), 439 (( 362793600i64, 0i64), [( 362793620i64, 0i64)]), 440 (( 362793600i64, 500000000i64), [( 362793620i64, 500000000i64)]), 441 (( 362793601i64, 0i64), [( 362793621i64, 0i64)]), 442 (( 362793601i64, 500000000i64), [( 362793621i64, 500000000i64)]), 443 (( 362793602i64, 0i64), [( 362793622i64, 0i64)]), 444 // 2017 Jan 01 445 (( 1483228798i64, 0i64), [( 1483228834i64, 0i64)]), 446 (( 1483228798i64, 500000000i64), [( 1483228834i64, 500000000i64)]), 447 (( 1483228799i64, 0i64), [ 448 ( 1483228835i64, 0i64), 449 ( 1483228836i64, 0i64), 450 ]), 451 (( 1483228799i64, 500000000i64), [ 452 ( 1483228835i64, 500000000i64), 453 ( 1483228836i64, 500000000i64), 454 ]), 455 (( 1483228800i64, 0i64), [( 1483228837i64, 0i64)]), 456 (( 1483228800i64, 500000000i64), [( 1483228837i64, 500000000i64)]), 457 (( 1483228801i64, 0i64), [( 1483228838i64, 0i64)]), 458 (( 1483228801i64, 500000000i64), [( 1483228838i64, 500000000i64)]), 459 (( 1483228802i64, 0i64), [( 1483228839i64, 0i64)]), 460 ]; 461 462 for (let testcase .. testcases) { 463 let params = testcase.0; 464 let param = time::instant{ sec = params.0, nsec = params.1 }; 465 let expect = testcase.1; 466 let actual = utc_convto(&tai, param); 467 468 match (expect) { 469 case void => 470 assert(actual is void); 471 472 case [0](i64, i64) => 473 assert(actual is []time::instant); 474 const actual = actual as []time::instant; 475 assert(len(actual) == 0); 476 477 case let insts: [1](i64, i64) => 478 assert(actual is []time::instant); 479 const actual = actual as []time::instant; 480 assert(len(actual) == 1); 481 assert(0 == time::compare( 482 actual[0], 483 time::instant{ 484 sec = insts[0].0, 485 nsec = insts[0].1, 486 }, 487 )); 488 489 case let insts: [2](i64, i64) => 490 assert(actual is []time::instant); 491 const actual = actual as []time::instant; 492 assert(len(actual) == 2); 493 assert(0 == time::compare( 494 actual[0], 495 time::instant{ 496 sec = insts[0].0, 497 nsec = insts[0].1, 498 }, 499 )); 500 assert(0 == time::compare( 501 actual[1], 502 time::instant{ 503 sec = insts[1].0, 504 nsec = insts[1].1, 505 }, 506 )); 507 }; 508 if (actual is []time::instant) { 509 free(actual as []time::instant); 510 }; 511 512 }; 513 }; 514 515 @test fn utc_convfrom_tai() void = { 516 // TODO: skip test if no leapsec data available (!utc_isinitialized) 517 // TODO: test negative leapsecs somehow 518 let testcases: []( 519 (i64, i64), // give 520 (void | [0](i64, i64) | [1](i64, i64) | [2](i64, i64)) // expect 521 ) = [ 522 ((- 990i64, 0i64), [(- 1000i64, 0i64)]), 523 (( 10i64, 0i64), [( 0i64, 0i64)]), 524 (( 1010i64, 0i64), [( 1000i64, 0i64)]), 525 // 1970 Jan 01 526 (( 63072008i64, 0i64), [( 63071998i64, 0i64)]), 527 (( 63072008i64, 500000000i64), [( 63071998i64, 500000000i64)]), 528 (( 63072009i64, 0i64), [( 63071999i64, 0i64)]), 529 (( 63072009i64, 500000000i64), [( 63071999i64, 500000000i64)]), 530 (( 63072010i64, 0i64), [( 63072000i64, 0i64)]), 531 (( 63072010i64, 500000000i64), [( 63072000i64, 500000000i64)]), 532 (( 63072011i64, 0i64), [( 63072001i64, 0i64)]), 533 (( 63072011i64, 500000000i64), [( 63072001i64, 500000000i64)]), 534 (( 63072012i64, 0i64), [( 63072002i64, 0i64)]), 535 // 1981 Jul 01 536 (( 362793617i64, 0i64), [( 362793598i64, 0i64)]), 537 (( 362793617i64, 500000000i64), [( 362793598i64, 500000000i64)]), 538 (( 362793618i64, 0i64), [( 362793599i64, 0i64)]), 539 (( 362793618i64, 500000000i64), [( 362793599i64, 500000000i64)]), 540 (( 362793619i64, 0i64), [( 362793599i64, 0i64)]), 541 (( 362793619i64, 500000000i64), [( 362793599i64, 500000000i64)]), 542 (( 362793620i64, 0i64), [( 362793600i64, 0i64)]), 543 (( 362793620i64, 500000000i64), [( 362793600i64, 500000000i64)]), 544 (( 362793621i64, 0i64), [( 362793601i64, 0i64)]), 545 (( 362793621i64, 500000000i64), [( 362793601i64, 500000000i64)]), 546 (( 362793622i64, 0i64), [( 362793602i64, 0i64)]), 547 // 2017 Jan 01 548 (( 1483228834i64, 0i64), [( 1483228798i64, 0i64)]), 549 (( 1483228834i64, 500000000i64), [( 1483228798i64, 500000000i64)]), 550 (( 1483228835i64, 0i64), [( 1483228799i64, 0i64)]), 551 (( 1483228835i64, 500000000i64), [( 1483228799i64, 500000000i64)]), 552 (( 1483228836i64, 0i64), [( 1483228799i64, 0i64)]), 553 (( 1483228836i64, 500000000i64), [( 1483228799i64, 500000000i64)]), 554 (( 1483228837i64, 0i64), [( 1483228800i64, 0i64)]), 555 (( 1483228837i64, 500000000i64), [( 1483228800i64, 500000000i64)]), 556 (( 1483228838i64, 0i64), [( 1483228801i64, 0i64)]), 557 (( 1483228838i64, 500000000i64), [( 1483228801i64, 500000000i64)]), 558 (( 1483228839i64, 0i64), [( 1483228802i64, 0i64)]), 559 ]; 560 561 for (let testcase .. testcases) { 562 let params = testcase.0; 563 let param = time::instant{ sec = params.0, nsec = params.1 }; 564 let expect = testcase.1; 565 let actual = utc_convfrom(&tai, param); 566 567 match (expect) { 568 case void => 569 assert(actual is void); 570 571 case [0](i64, i64) => 572 assert(actual is []time::instant); 573 const actual = actual as []time::instant; 574 assert(len(actual) == 0); 575 576 case let insts: [1](i64, i64) => 577 assert(actual is []time::instant); 578 const actual = actual as []time::instant; 579 assert(len(actual) == 1); 580 assert(0 == time::compare( 581 actual[0], 582 time::instant{ 583 sec = insts[0].0, 584 nsec = insts[0].1, 585 }, 586 )); 587 588 case let insts: [2](i64, i64) => 589 assert(actual is []time::instant); 590 const actual = actual as []time::instant; 591 assert(len(actual) == 2); 592 assert(0 == time::compare( 593 actual[0], 594 time::instant{ 595 sec = insts[0].0, 596 nsec = insts[0].1, 597 }, 598 )); 599 assert(0 == time::compare( 600 actual[1], 601 time::instant{ 602 sec = insts[1].0, 603 nsec = insts[1].1, 604 }, 605 )); 606 }; 607 if (actual is []time::instant) { 608 free(actual as []time::instant); 609 }; 610 }; 611 };