virtual.ha (16375B)
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 switch (len(valid_dates)) { 276 case 0 => 277 if (0 != zf & zflag::GAP_END) { 278 return realize_gapbounds(v).1; 279 } else if (0 != zf & zflag::GAP_START) { 280 return realize_gapbounds(v).0; 281 } else { 282 return false: zfunresolved; 283 }; 284 case 1 => 285 return valid_dates[0]; 286 case => 287 if (0 != zf & zflag::LAP_LATE) { 288 return valid_dates[len(valid_dates) - 1]; 289 } else if (0 != zf & zflag::LAP_EARLY) { 290 return valid_dates[0]; 291 } else { 292 return true: zfunresolved; 293 }; 294 }; 295 }; 296 }; 297 298 fn realize_validzoff( 299 v: virtual, 300 locs: chrono::locality... 301 ) (date | insufficient | invalid) = { 302 let d = realize_datetimezoff(v, locs...)?; 303 304 // verify zone offset 305 if (chrono::ozone(&d).zoff != v.zoff as time::duration) { 306 return invalid; 307 }; 308 309 return d; 310 }; 311 312 fn realize_datetimezoff( 313 v: virtual, 314 locs: chrono::locality... 315 ) (date | insufficient | invalid) = { 316 let lacking = 0u8; 317 318 // determine .loc 319 if (v.vloc is chrono::locality) { 320 v.loc = v.vloc as chrono::locality; 321 } else if (v.locname is str) { 322 for (let loc .. locs) { 323 if (loc.name == v.locname as str) { 324 v.loc = loc; 325 break; 326 }; 327 }; 328 } else { 329 lacking |= insufficient::LOCALITY; 330 }; 331 332 // try using .vsec .vnsec 333 if (v.vsec is i64 && v.vnsec is i64) { 334 return from_instant( 335 v.loc, 336 time::instant{ 337 sec = v.vsec as i64, 338 nsec = v.vnsec as i64, 339 }, 340 ); 341 }; 342 343 // try using .daydate, .daytime, .zoff 344 345 // determine zone offset 346 if (v.zoff is i64) { 347 void; 348 } else { 349 lacking |= insufficient::ZOFF; 350 }; 351 352 // determine .daydate 353 if (v.daydate is i64) { 354 void; 355 } else :daydate { 356 const year = 357 if (v.year is int) { 358 yield v.year as int; 359 } else if (v.century is int && v.year100 is int) { 360 let cc = v.century as int; 361 let yy = v.year100 as int; 362 if (yy < 0 || yy > 99) { 363 return invalid; 364 }; 365 yield cc * 100 + yy; 366 } else { 367 lacking |= lack::DAYDATE; 368 yield :daydate; 369 }; 370 371 if ( 372 v.month is int && 373 v.day is int 374 ) { 375 v.daydate = calc_daydate__ymd( 376 year, 377 v.month as int, 378 v.day as int, 379 )?; 380 } else if ( 381 v.yearday is int 382 ) { 383 v.daydate = calc_daydate__yd( 384 year, 385 v.yearday as int, 386 )?; 387 } else if ( 388 v.week is int && 389 v.weekday is int 390 ) { 391 v.daydate = calc_daydate__ywd( 392 year, 393 v.week as int, 394 v.weekday as int, 395 )?; 396 } else if (false) { 397 // TODO: calendar.ha: calc_daydate__isoywd() 398 void; 399 } else { 400 // cannot deduce daydate 401 lacking |= insufficient::DAYDATE; 402 }; 403 }; 404 405 // determine .daytime 406 if (v.daytime is i64) { 407 void; 408 } else :daytime { 409 const hour = 410 if (v.hour is int) { 411 yield v.hour as int; 412 } else if (v.hour12 is int && v.ampm is bool) { 413 const hr = v.hour12 as int; 414 const pm = v.ampm as bool; 415 yield if (pm) hr * 2 else hr; 416 } else { 417 lacking |= insufficient::DAYTIME; 418 yield :daytime; 419 }; 420 421 if ( 422 v.minute is int && 423 v.second is int && 424 v.nanosecond is int 425 ) { 426 v.daytime = calc_daytime__hmsn( 427 hour, 428 v.minute as int, 429 v.second as int, 430 v.nanosecond as int, 431 )?; 432 } else { 433 lacking |= insufficient::DAYTIME; 434 }; 435 }; 436 437 if (lacking != 0u8) { 438 return lacking: insufficient; 439 }; 440 441 // determine .sec, .nsec 442 const d = from_moment(chrono::from_datetime( 443 v.loc, 444 v.zoff as time::duration, 445 v.daydate as i64, 446 v.daytime as i64, 447 )); 448 449 return d; 450 }; 451 452 fn realize_validzoffs( 453 v: virtual, 454 locs: chrono::locality... 455 ) ([]date | insufficient | invalid) = { 456 // check if only zoff is missing 457 v.zoff = 0o0; 458 match (realize_validzoff(v, locs...)) { 459 case (date | invalid) => 460 void; 461 case let ins: insufficient => 462 return ins; 463 }; 464 v.zoff = void; 465 466 let dates: []date = []; 467 468 // determine .loc 469 if (v.vloc is chrono::locality) { 470 v.loc = v.vloc as chrono::locality; 471 } else if (v.locname is str) { 472 for (let loc .. locs) { 473 if (loc.name == v.locname as str) { 474 v.loc = loc; 475 v.vloc = loc; 476 break; 477 }; 478 }; 479 } else { 480 return insufficient::LOCALITY; 481 }; 482 483 // try matching zone abbreviation 484 if (v.zabbr is str) { 485 for (let zone .. v.loc.zones) { 486 if (v.zabbr as str == zone.abbr) { 487 v.zoff = zone.zoff; 488 match (realize_validzoff(v, locs...)) { 489 case let d: date => 490 match (sort::search( 491 dates, size(date), &d, &cmpdates, 492 )) { 493 case size => 494 void; 495 case void => 496 append(dates, d); 497 sort::sort(dates, size(date), &cmpdates); 498 }; 499 case invalid => 500 continue; 501 case => 502 abort(); 503 }; 504 }; 505 }; 506 507 return invalid; 508 }; 509 510 // try zone offsets from locality 511 for (let zone .. v.loc.zones) { 512 v.zoff = zone.zoff; 513 match (realize_validzoff(v, locs...)) { 514 case let d: date => 515 match (sort::search(dates, size(date), &d, &cmpdates)) { 516 case size => 517 void; 518 case void => 519 append(dates, d); 520 sort::sort(dates, size(date), &cmpdates); 521 }; 522 case invalid => 523 continue; 524 case => 525 abort(); 526 }; 527 }; 528 529 return dates; 530 }; 531 532 fn cmpdates(a: const *opaque, b: const *opaque) int = { 533 let a = a: *date; 534 let b = b: *date; 535 return chrono::compare(a, b)!: int; 536 }; 537 538 fn realize_gapbounds(v: virtual) (date, date) = { 539 let loc = v.vloc as chrono::locality; 540 541 let zlo: time::duration = 48 * time::HOUR; 542 let zhi: time::duration = -48 * time::HOUR; 543 for (let zone .. loc.zones) { 544 if (zone.zoff > zhi) { 545 zhi = zone.zoff; 546 }; 547 if (zone.zoff < zlo) { 548 zlo = zone.zoff; 549 }; 550 }; 551 552 v.zoff = zhi; 553 let earliest = realize_datetimezoff(v)!; 554 let earliest = *(&earliest: *time::instant); 555 556 v.zoff = zlo; 557 let latest = realize_datetimezoff(v)!; 558 let latest = *(&latest: *time::instant); 559 560 let t = time::instant{ ... }; 561 for (let tr .. loc.transitions) { 562 let is_within_bounds = ( 563 time::compare(earliest, tr.when) < 0 564 && time::compare(latest, tr.when) > 0 565 ); 566 567 if (is_within_bounds) { 568 t = tr.when; 569 break; 570 }; 571 }; 572 573 let gapstart = from_instant(loc, time::add(t, -time::NANOSECOND)); 574 let gapend = from_instant(loc, t); 575 576 // TODO: check if original v falls within gapstart & gapend? 577 578 return (gapstart, gapend); 579 };