hare

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

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