hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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