virtual.ha (16488B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use sort; 5 use time; 6 use time::chrono; 7 8 // Flags for resolving an absent zone-offset. Handles timezone transitions. 9 // 10 // The [[realize]] function, as well as other date creation functions (like 11 // [[new]], [[truncate]], [[reckon]]...) accept zflags. If zflags are provided, 12 // these functions normally calculate an intermediate date with a best-guess 13 // numerical zone-offset. This intermediate date can be [[invalid]] if it falls 14 // within the observed overlap or gap of a timezone transition, where such dates 15 // are ambiguous or nonexistent. In this case, the provided zflags are 16 // consulted, and a final calculation takes place before the final resultant 17 // date (or [[zfunresolved]]) is returned. 18 // 19 // Timezone transitions create gaps and overlaps, the two causes of [[invalid]] 20 // intermediate dates. Passing one "GAP_" and one "LAP_" flag covers both cases. 21 // 22 // let zf = date::zflag::LAP_EARLY | date::zflag::GAP_END; 23 // date::new(loc, zf, fields...)!; // will never return [[zfunresolved]] 24 // 25 // Note that usage of "GAP_" flags will cause the resultant date to be different 26 // to what is originally specified if the intermediate date falls within a gap. 27 // Flags with greater value take precedent. 28 // 29 // The following figures exist to help understand the effect of these flags. 30 // 31 // Fig A 2000 October 29th 32 // -1 hour 33 // 34 // f=02:30+0200 35 // g=02:30+0100 36 // lp | lq 37 // +0200 | | | +0100 38 // Observed time: 00 01 02 | 03 04 05 39 // Amsterdam: |-----|-----|==*==|-----|-----| 40 // . . .\ :: |. . 41 // . . . \: :| . . 42 // . . . : : . . 43 // . . . :\ |: . . 44 // . . . : \| : . . 45 // UTC: |-----|-----|--*--|--*--|-----| 46 // Contiguous time: 22 23 00 | 01 | 02 03 47 // | | | 48 // a tx b 49 // 50 // Fig A -- A backjump timezone transition in the Europe/Amsterdam locality. 51 // The transition is marked by "tx". There is an overlap in the chronology, 52 // marked by "lp" and "lq". The specified local time 02:30 falls within the 53 // observed overlap, and so has two valid zone-offsets and can be observed 54 // twice, as dates "f" and "g". When localized to UTC, these two observations 55 // correspond to UTC dates "a" and "b" respectively. 56 // 57 // Fig B 2000 March 26th 58 // +1 hour 59 // 60 // f~02:30+!!!! 61 // gp | gq 62 // +0100 | | | +0200 63 // Observed time: 00 01 02 | 03 04 05 64 // Amsterdam: |-----|-----| * |-----|-----| 65 // . . | / . . 66 // . . | / . . 67 // . . | / . . 68 // . . | / . . 69 // . . |/ . . 70 // UTC: |-----|-----|-----|-----|-----| 71 // Contiguous time: 23 00 01 02 03 04 72 // | 73 // tx 74 // 75 // Fig B -- A forejump timezone transition in the Europe/Amsterdam locality. 76 // The transition is marked by "tx". There is a gap in the chronology, marked by 77 // "gp" and "gq". The specified local time 02:30 falls within the observed gap, 78 // and so cannot be observed and is [[invalid]]. 79 export type zflag = enum u8 { 80 // Assume a contiguous chronology with no observed gaps or overlaps. 81 // Upon encountering an observed gap or overlap, fail with [[invalid]]. 82 // In other words, accept one and only one zone-offset. 83 CONTIG = 0b00000000, 84 85 // Upon encountering an observed overlap, select the earliest possible 86 // date (Fig A "f") using the most positive (eastmost) zone-offset. 87 LAP_EARLY = 0b00000001, 88 // Upon encountering an observed overlap, select the latest possible 89 // date (Fig A "g") using the most negative (westmost) zone-offset. 90 LAP_LATE = 0b00000010, 91 92 // Upon encountering an observed gap, disregard the specified date and 93 // select the date at the start boundary of the observed gap (Fig B 94 // "gp"), corresponding to the contiguous time just before the 95 // transition (Fig B "tx"). 96 GAP_START = 0b00000100, 97 // Upon encountering an observed gap, disregard the specified date and 98 // select the date at the end boundary of the observed gap (Fig B "gq"), 99 // corresponding to the contiguous time at the transition (Fig B "tx"). 100 GAP_END = 0b00001000, 101 }; 102 103 // Failed to resolve an absent zone-offset. The provided [[zflag]]s failed to 104 // account for some timezone effect and could not produce a valid zone-offset. 105 // A false value signifies the occurence of a timezone transition gap. 106 // A true value signifies the occurence of a timezone transition overlap. 107 export type zfunresolved = !bool; 108 109 // A [[virtual]] date does not have enough information from which to create a 110 // valid [[date]]. 111 export type insufficient = !lack; // TODO: drop alias workaround 112 113 export type lack = enum u8 { 114 LOCALITY = 1 << 0, // could not deduce locality 115 DAYDATE = 1 << 1, // could not deduce daydate 116 DAYTIME = 1 << 2, // could not deduce time-of-day 117 ZOFF = 1 << 3, // could not deduce zone offset 118 }; 119 120 // A virtual date; a [[date]] wrapper interface, which represents a date of 121 // uncertain validity. Its fields need not be valid observed chronological 122 // values. It is meant as an intermediary container for date information to be 123 // resolved with the [[realize]] function. 124 // 125 // Unlike [[date]], a virtual date's fields are meant to be treated as public 126 // and mutable. The embedded [[time::instant]] and [[time::chrono::locality]] 127 // fields (.sec .nsec .loc) are considered meaningless. Behaviour with the 128 // observer functions is undefined. 129 // 130 // This can be used to safely construct a new [[date]] piece-by-piece. Start 131 // with [[newvirtual]], then collect enough date/time information incrementally 132 // by direct field assignments and/or with [[parse]]. Finish with [[realize]]. 133 // 134 // let v = date::newvirtual(); 135 // v.vloc = chrono::tz("Europe/Amsterdam")!; 136 // v.zoff = date::zflag::LAP_EARLY | date::zflag::GAP_END; 137 // date::parse(&v, "Date: %Y-%m-%d", "Date: 2000-01-02")!; 138 // v.hour = 15; 139 // v.minute = 4; 140 // v.second = 5; 141 // v.nanosecond = 600000000; 142 // let d = date::realize(v)!; 143 // 144 export type virtual = struct { 145 date, 146 // virtual's timescalar second 147 vsec: (void | i64), 148 // virtual's nanosecond of timescalar second 149 vnsec: (void | i64), 150 // virtual's locality 151 vloc: (void | chrono::locality), 152 // locality name 153 locname: (void | str), 154 // zone offset 155 zoff: (void | time::duration | zflag), 156 // zone abbreviation 157 zabbr: (void | str), 158 // all but the last two digits of the year 159 century: (void | int), 160 // the last two digits of the year 161 year100: (void | int), 162 // hour of 12 hour clock 163 hour12: (void | int), 164 // AM/PM (false/true) 165 ampm: (void | bool), 166 }; 167 168 // Creates a new [[virtual]] date. All its fields are voided or nulled. 169 export fn newvirtual() virtual = virtual { 170 sec = 0, 171 nsec = 0, 172 loc = chrono::UTC, 173 zone = null, 174 daydate = void, 175 daytime = void, 176 177 era = void, 178 year = void, 179 month = void, 180 day = void, 181 yearday = void, 182 isoweekyear = void, 183 isoweek = void, 184 week = void, 185 sundayweek = void, 186 weekday = void, 187 188 hour = void, 189 minute = void, 190 second = void, 191 nanosecond = void, 192 193 vsec = void, 194 vnsec = void, 195 vloc = void, 196 locname = void, 197 zoff = void, 198 zabbr = void, 199 century = void, 200 year100 = void, 201 hour12 = void, 202 ampm = void, 203 }; 204 205 // Realizes a valid [[date]] from a [[virtual]] date, or fails appropriately. 206 // 207 // The virtual date must hold enough valid date information to be able to 208 // calculate values for the resulting date. A valid combination of its fields 209 // must be "filled-in" (hold numerical, non-void values). For example: 210 // 211 // let v = date::newvirtual(); 212 // v.locname = "Europe/Amsterdam"; 213 // v.zoff = date::zflag::LAP_EARLY | date::zflag::GAP_END; 214 // date::parse(&v, // fills-in .year .month .day 215 // "Date: %Y-%m-%d", "Date: 2038-01-19")!; 216 // v.hour = 4; 217 // v.minute = 14; 218 // v.second = 7; 219 // v.nanosecond = 0; 220 // let d = date::realize(v, time::chrono::tz("Europe/Amsterdam")!)!; 221 // 222 // This function consults the fields of the given virtual date using a 223 // predictable procedure, attempting the simplest and most common field 224 // combinations first. Fields marked below with an asterisk (*), when empty, 225 // depend on other filled-in field-sets to calculate a new value for itself. The 226 // order in which these "dependency" field-sets are tried is described below. 227 // 228 // The resultant date depends on a locality value and instant value. 229 // 230 // The locality ([[time::chrono::locality]]) value depends on: 231 // 232 // - .vloc 233 // - .locname : This is compared to the .name field of each locality 234 // provided via the locs parameter, or "UTC" if none are provided. 235 // The first matching locality is used. 236 // 237 // The instant ([[time::instant]]) value depends on: 238 // 239 // - .vsec, .vnsec 240 // - .daydate*, .daytime*, .zoff 241 // 242 // An empty .daydate depends on: 243 // 244 // - .year*, .month, .day 245 // - .year*, .yearday 246 // - .year*, .week, .weekday 247 // - .isoweekyear, .isoweek, .weekday 248 // 249 // An empty .daytime depends on: 250 // 251 // - .hour*, .minute, .second, .nanosecond 252 // 253 // An empty .year depends on: 254 // 255 // - .century, .year100 256 // 257 // An empty .hour depends on: 258 // 259 // - .hour12, .ampm 260 // 261 // If not enough information was provided, [[insufficient]] is returned. 262 // If invalid information was provided, [[invalid]] is returned. 263 // Any [[zflag]]s assigned to the .zoff field affect the final result. 264 export fn realize( 265 v: virtual, 266 locs: chrono::locality... 267 ) (date | insufficient | invalid | zfunresolved) = { 268 match (v.zoff) { 269 case void => 270 return lack::ZOFF; 271 case time::duration => 272 return realize_validzoff(v, locs...); 273 case let zf: zflag => 274 let valid_dates = realize_validzoffs(v, locs...)?; 275 defer free(valid_dates); 276 switch (len(valid_dates)) { 277 case 0 => 278 if (0 != zf & zflag::GAP_END) { 279 return realize_gapbounds(v).1; 280 } else if (0 != zf & zflag::GAP_START) { 281 return realize_gapbounds(v).0; 282 } else { 283 return false: zfunresolved; 284 }; 285 case 1 => 286 return valid_dates[0]; 287 case => 288 if (0 != zf & zflag::LAP_LATE) { 289 return valid_dates[len(valid_dates) - 1]; 290 } else if (0 != zf & zflag::LAP_EARLY) { 291 return valid_dates[0]; 292 } else { 293 return true: zfunresolved; 294 }; 295 }; 296 }; 297 }; 298 299 fn realize_validzoff( 300 v: virtual, 301 locs: chrono::locality... 302 ) (date | insufficient | invalid) = { 303 let d = realize_datetimezoff(v, locs...)?; 304 305 // verify zone offset 306 if (chrono::ozone(&d).zoff != v.zoff as time::duration) { 307 return invalid; 308 }; 309 310 return d; 311 }; 312 313 fn realize_datetimezoff( 314 v: virtual, 315 locs: chrono::locality... 316 ) (date | insufficient | invalid) = { 317 let lacking = 0u8; 318 319 // determine .loc 320 if (v.vloc is chrono::locality) { 321 v.loc = v.vloc as chrono::locality; 322 } else if (v.locname is str) { 323 for (let loc .. locs) { 324 if (loc.name == v.locname as str) { 325 v.loc = loc; 326 break; 327 }; 328 }; 329 } else { 330 lacking |= insufficient::LOCALITY; 331 }; 332 333 // try using .vsec .vnsec 334 if (v.vsec is i64 && v.vnsec is i64) { 335 return from_instant( 336 v.loc, 337 time::instant{ 338 sec = v.vsec as i64, 339 nsec = v.vnsec as i64, 340 }, 341 ); 342 }; 343 344 // try using .daydate, .daytime, .zoff 345 346 // determine zone offset 347 if (v.zoff is i64) { 348 void; 349 } else { 350 lacking |= insufficient::ZOFF; 351 }; 352 353 // determine .daydate 354 if (v.daydate is i64) { 355 void; 356 } else :daydate { 357 const year = 358 if (v.year is int) { 359 yield v.year as int; 360 } else if (v.century is int && v.year100 is int) { 361 let cc = v.century as int; 362 let yy = v.year100 as int; 363 if (yy < 0 || yy > 99) { 364 return invalid; 365 }; 366 yield cc * 100 + yy; 367 }; 368 369 if ( 370 v.month is int && 371 v.day is int 372 ) { 373 v.daydate = calc_daydate__ymd( 374 year as int, 375 v.month as int, 376 v.day as int, 377 )?; 378 } else if ( 379 v.yearday is int 380 ) { 381 v.daydate = calc_daydate__yd( 382 year as int, 383 v.yearday as int, 384 )?; 385 } else if ( 386 v.week is int && 387 v.weekday is int 388 ) { 389 v.daydate = calc_daydate__ywd( 390 year as int, 391 v.week as int, 392 v.weekday as int, 393 )?; 394 } else if ( 395 v.isoweekyear is int && 396 v.isoweek is int && 397 v.weekday is int 398 ) { 399 v.daydate = calc_daydate__isoywd( 400 v.isoweekyear as int, 401 v.isoweek as int, 402 v.weekday as int, 403 )?; 404 } else { 405 // cannot deduce daydate 406 lacking |= insufficient::DAYDATE; 407 }; 408 }; 409 410 // determine .daytime 411 if (v.daytime is i64) { 412 void; 413 } else :daytime { 414 const hour = 415 if (v.hour is int) { 416 yield v.hour as int; 417 } else if (v.hour12 is int && v.ampm is bool) { 418 const hr = v.hour12 as int; 419 const pm = v.ampm as bool; 420 yield if (pm) hr * 2 else hr; 421 } else { 422 lacking |= insufficient::DAYTIME; 423 yield :daytime; 424 }; 425 426 if ( 427 v.minute is int && 428 v.second is int && 429 v.nanosecond is int 430 ) { 431 v.daytime = calc_daytime__hmsn( 432 hour, 433 v.minute as int, 434 v.second as int, 435 v.nanosecond as int, 436 )?; 437 } else { 438 lacking |= insufficient::DAYTIME; 439 }; 440 }; 441 442 if (lacking != 0u8) { 443 return lacking: insufficient; 444 }; 445 446 // determine .sec, .nsec 447 const d = from_moment(chrono::from_datetime( 448 v.loc, 449 v.zoff as time::duration, 450 v.daydate as i64, 451 v.daytime as i64, 452 )); 453 454 return d; 455 }; 456 457 fn realize_validzoffs( 458 v: virtual, 459 locs: chrono::locality... 460 ) ([]date | insufficient | invalid) = { 461 // check if only zoff is missing 462 v.zoff = 0o0; 463 match (realize_validzoff(v, locs...)) { 464 case (date | invalid) => 465 void; 466 case let ins: insufficient => 467 return ins; 468 }; 469 v.zoff = void; 470 471 let dates: []date = []; 472 473 // determine .loc 474 if (v.vloc is chrono::locality) { 475 v.loc = v.vloc as chrono::locality; 476 } else if (v.locname is str) { 477 for (let loc .. locs) { 478 if (loc.name == v.locname as str) { 479 v.loc = loc; 480 v.vloc = loc; 481 break; 482 }; 483 }; 484 } else { 485 return insufficient::LOCALITY; 486 }; 487 488 // try matching zone abbreviation 489 if (v.zabbr is str) { 490 for (let zone .. v.loc.zones) { 491 if (v.zabbr as str == zone.abbr) { 492 v.zoff = zone.zoff; 493 match (realize_validzoff(v, locs...)) { 494 case let d: date => 495 match (sort::search( 496 dates, size(date), &d, &cmpdates, 497 )) { 498 case size => 499 void; 500 case void => 501 append(dates, d)!; 502 sort::sort(dates, size(date), &cmpdates); 503 }; 504 case invalid => 505 continue; 506 case => 507 abort(); 508 }; 509 }; 510 }; 511 512 return invalid; 513 }; 514 515 // try zone offsets from locality 516 for (let zone .. v.loc.zones) { 517 v.zoff = zone.zoff; 518 match (realize_validzoff(v, locs...)) { 519 case let d: date => 520 match (sort::search(dates, size(date), &d, &cmpdates)) { 521 case size => 522 void; 523 case void => 524 append(dates, d)!; 525 sort::sort(dates, size(date), &cmpdates); 526 }; 527 case invalid => 528 continue; 529 case => 530 abort(); 531 }; 532 }; 533 534 return dates; 535 }; 536 537 fn cmpdates(a: const *opaque, b: const *opaque) int = { 538 let a = a: *date; 539 let b = b: *date; 540 return chrono::compare(a, b)!: int; 541 }; 542 543 fn realize_gapbounds(v: virtual) (date, date) = { 544 let loc = v.vloc as chrono::locality; 545 546 let zlo: time::duration = 48 * time::HOUR; 547 let zhi: time::duration = -48 * time::HOUR; 548 for (let zone .. loc.zones) { 549 if (zone.zoff > zhi) { 550 zhi = zone.zoff; 551 }; 552 if (zone.zoff < zlo) { 553 zlo = zone.zoff; 554 }; 555 }; 556 557 v.zoff = zhi; 558 let earliest = realize_datetimezoff(v)!; 559 let earliest = *(&earliest: *time::instant); 560 561 v.zoff = zlo; 562 let latest = realize_datetimezoff(v)!; 563 let latest = *(&latest: *time::instant); 564 565 let t = time::instant{ ... }; 566 for (let tr .. loc.transitions) { 567 let is_within_bounds = ( 568 time::compare(earliest, tr.when) < 0 569 && time::compare(latest, tr.when) > 0 570 ); 571 572 if (is_within_bounds) { 573 t = tr.when; 574 break; 575 }; 576 }; 577 578 let gapstart = from_instant(loc, time::add(t, -time::NANOSECOND)); 579 let gapend = from_instant(loc, t); 580 581 // TODO: check if original v falls within gapstart & gapend? 582 583 return (gapstart, gapend); 584 };