hare

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

timezone.ha (7536B)


      1 // SPDX-License-Identifier: MPL-2.0
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 use os;
      5 use sort;
      6 use strings;
      7 use time;
      8 
      9 // The locality of a [[moment]]. Contains information about how to calculate a
     10 // moment's observed chronological values.
     11 export type locality = *timezone;
     12 
     13 // A timezone; a political or otherwise theoretical region with a ruleset
     14 // regarding offsets for calculating localized date/time.
     15 export type timezone = struct {
     16 	// The textual identifier ("Europe/Amsterdam")
     17 	name: str,
     18 
     19 	// The base timescale (time::chrono::utc)
     20 	timescale: *timescale,
     21 
     22 	// The duration of a day in this timezone (24 * time::HOUR)
     23 	daylength: time::duration,
     24 
     25 	// The possible temporal zones a locality with this timezone can observe
     26 	// (CET, CEST, ...)
     27 	zones: []zone,
     28 
     29 	// The transitions between this timezone's zones
     30 	transitions: []transition,
     31 
     32 	// A timezone specifier in the POSIX "expanded" TZ format.
     33 	// See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
     34 	//
     35 	// Used for extending calculations beyond the last known transition.
     36 	posix_extend: str,
     37 };
     38 
     39 // A [[timezone]] state, with an offset for calculating localized date/time.
     40 export type zone = struct {
     41 	// The offset from the normal timezone (2 * time::HOUR)
     42 	zoff: time::duration,
     43 
     44 	// The full descriptive name ("Central European Summer Time")
     45 	name: str,
     46 
     47 	// The abbreviated name ("CEST")
     48 	abbr: str,
     49 
     50 	// Indicator of Daylight Saving Time
     51 	dst: bool, // true
     52 };
     53 
     54 // A [[timezone]] transition between two [[zone]]s.
     55 export type transition = struct {
     56 	when: time::instant,
     57 	zoneindex: size,
     58 };
     59 
     60 // A destructured dual std/dst POSIX timezone. See tzset(3).
     61 type tzname = struct {
     62 	std_name: str,
     63 	std_offset: time::duration,
     64 	dst_name: str,
     65 	dst_offset: time::duration,
     66 	dst_start: str,
     67 	dst_starttime: str,
     68 	dst_end: str,
     69 	dst_endtime: str,
     70 };
     71 
     72 // Frees a [[timezone]]. A [[locality]] argument can be passed.
     73 export fn timezone_free(tz: *timezone) void = {
     74 	free(tz.name);
     75 	for (let zone &.. tz.zones) {
     76 		zone_finish(zone);
     77 	};
     78 	free(tz.zones);
     79 	free(tz.transitions);
     80 	free(tz.posix_extend);
     81 	free(tz);
     82 };
     83 
     84 // Frees resources associated with a [[zone]].
     85 export fn zone_finish(z: *zone) void = {
     86 	free(z.name);
     87 	free(z.abbr);
     88 };
     89 
     90 // Creates an equivalent [[moment]] with a different [[locality]].
     91 //
     92 // If the moment's associated [[timescale]] and the target locality's timescale
     93 // are different, a conversion from one to the other via the TAI timescale will
     94 // be attempted. Any [[discontinuity]] occurrence will be returned. If a
     95 // discontinuity against TAI amongst the two timescales exist, consider
     96 // converting such instants manually.
     97 export fn in(loc: locality, m: moment) (moment | discontinuity) = {
     98 	let i = *(&m: *time::instant);
     99 	if (m.loc.timescale != loc.timescale) {
    100 		match (convert(i, m.loc.timescale, loc.timescale)) {
    101 		case analytical =>
    102 			return discontinuity;
    103 		case let i: time::instant =>
    104 			return new(loc, i);
    105 		};
    106 	};
    107 	return new(loc, i);
    108 };
    109 
    110 // Finds and returns a [[moment]]'s currently observed [[zone]].
    111 fn lookupzone(loc: locality, inst: time::instant) *zone = {
    112 	// TODO: https://todo.sr.ht/~sircmpwn/hare/643
    113 	if (len(loc.zones) == 0) {
    114 		abort("time::chrono: Timezone has no zones");
    115 	};
    116 
    117 	if (len(loc.zones) == 1) {
    118 		return &loc.zones[0];
    119 	};
    120 
    121 	let trs = loc.transitions[..];
    122 
    123 	if (len(trs) == 0 || time::compare(inst, trs[0].when) == -1) {
    124 		// TODO: special case
    125 		abort("lookupzone(): time is before known transitions");
    126 	};
    127 
    128 	// index of transition which inst is equal to or greater than.
    129 	const idx = -1 + sort::rbisect(
    130 		trs, size(transition), &inst, &cmpinstants,
    131 	);
    132 
    133 	const z = &loc.zones[trs[idx].zoneindex];
    134 
    135 	// if we've reached the end of the locality's transitions, try its
    136 	// posix_extend string
    137 	//
    138 	// TODO: Unfinished; complete.
    139 	if (idx == len(trs) - 1 && loc.posix_extend != "") {
    140 		void;
    141 	};
    142 
    143 	return z;
    144 };
    145 
    146 fn cmpinstants(a: const *opaque, b: const *opaque) int = {
    147 	let a = a: *transition;
    148 	let b = b: *time::instant;
    149 	return time::compare(a.when, *b): int;
    150 };
    151 
    152 // Creates a [[timezone]] with a single [[zone]]. Useful for fixed offsets.
    153 // For example, replicate the civil time Hawaii timezone on Earth:
    154 //
    155 // 	let hawaii = chrono::fixedzone(&chrono::utc, chrono::EARTH_DAY,
    156 // 		chrono::zone {
    157 // 			zoff = -10 * time::HOUR,
    158 // 			name = "Hawaiian Reef",
    159 // 			abbr = "HARE",
    160 // 			dst = false,
    161 // 		},
    162 // 	);
    163 //
    164 export fn fixedzone(ts: *timescale, daylen: time::duration, z: zone) timezone = {
    165 	return timezone {
    166 		name = z.name,
    167 		timescale = ts,
    168 		daylength = daylen,
    169 		zones = alloc([z])!,
    170 		transitions = [],
    171 		posix_extend = "",
    172 	};
    173 };
    174 
    175 // The local [[locality]]; the system or environment configured [[timezone]].
    176 //
    177 // This is set during the program's initialization. In order of preference, the
    178 // TZ environment variable is used, if set; the file at [[LOCALTIME_PATH]], if
    179 // present; or, as a last resort, [[UTC]] is used as a default.
    180 export const LOCAL: locality = &TZ_UTC;
    181 
    182 @init fn init_tz_local() void = {
    183 	let path = match (os::getenv("TZ")) {
    184 	case let path: str =>
    185 		// remove POSIX prefix ':'
    186 		yield if (strings::hasprefix(path, ':')) {
    187 			yield strings::sub(path, 1, strings::end);
    188 		} else {
    189 			yield path;
    190 		};
    191 	case void =>
    192 		yield match (os::realpath(LOCALTIME_PATH)) {
    193 		case let path: str =>
    194 			yield if (strings::hasprefix(path, TZDB_PATH)) {
    195 				yield strings::sub(
    196 					path, len(TZDB_PATH), strings::end,
    197 				);
    198 			} else {
    199 				yield path;
    200 			};
    201 		case =>
    202 			return;
    203 		};
    204 	};
    205 
    206 	match (tz(path)) {
    207 	case => void;
    208 	case let loc: locality =>
    209 		LOCAL = loc;
    210 	};
    211 };
    212 
    213 @fini fn free_tz_local() void = {
    214 	if (LOCAL != UTC) {
    215 		timezone_free(LOCAL);
    216 	};
    217 };
    218 
    219 // The UTC (Coordinated Universal Time) "Zulu" [[timezone]] as a [[locality]].
    220 export const UTC: locality = &TZ_UTC;
    221 
    222 const TZ_UTC: timezone = timezone {
    223 	name = "UTC",
    224 	timescale = &utc,
    225 	daylength = EARTH_DAY,
    226 	zones = [
    227 		zone {
    228 			zoff = 0 * time::SECOND,
    229 			name = "Universal Coordinated Time",
    230 			abbr = "UTC",
    231 			dst = false,
    232 		},
    233 	],
    234 	transitions = [],
    235 	posix_extend = "",
    236 };
    237 
    238 // The TAI (International Atomic Time) "Zulu" [[timezone]] as a [[locality]].
    239 export const TAI: locality = &TZ_TAI;
    240 
    241 const TZ_TAI: timezone = timezone {
    242 	name = "TAI",
    243 	timescale = &tai,
    244 	daylength = EARTH_DAY,
    245 	zones = [
    246 		zone {
    247 			zoff = 0 * time::SECOND,
    248 			name = "International Atomic Time",
    249 			abbr = "TAI",
    250 			dst = false,
    251 		},
    252 	],
    253 	transitions = [],
    254 	posix_extend = "",
    255 };
    256 
    257 // The GPS (Global Positioning System) "Zulu" [[timezone]] as a [[locality]].
    258 export const GPS: locality = &TZ_GPS;
    259 
    260 const TZ_GPS: timezone = timezone {
    261 	name = "GPS",
    262 	timescale = &gps,
    263 	daylength = EARTH_DAY,
    264 	zones = [
    265 		zone {
    266 			zoff = 0 * time::SECOND,
    267 			name = "Global Positioning System",
    268 			abbr = "GPS",
    269 			dst = false,
    270 		},
    271 	],
    272 	transitions = [],
    273 	posix_extend = "",
    274 };
    275 
    276 // The TT (Terrestrial Time) "Zulu" [[timezone]] as a [[locality]].
    277 export const TT: locality = &TZ_TT;
    278 
    279 const TZ_TT: timezone = timezone {
    280 	name = "TT",
    281 	timescale = &tt,
    282 	daylength = EARTH_DAY,
    283 	zones = [
    284 		zone {
    285 			zoff = 0 * time::SECOND,
    286 			name = "Terrestrial Time",
    287 			abbr = "TT",
    288 			dst = false,
    289 		},
    290 	],
    291 	transitions = [],
    292 	posix_extend = "",
    293 };
    294 
    295 // The MTC (Coordinated Mars Time) "Zulu" [[timezone]] as a [[locality]].
    296 export const MTC: locality = &TZ_MTC;
    297 
    298 const TZ_MTC: timezone = timezone {
    299 	name = "MTC",
    300 	timescale = &mtc,
    301 	daylength = MARS_SOL_MARTIAN,
    302 	zones = [
    303 		zone {
    304 			zoff = 0 * time::SECOND,
    305 			name = "Coordinated Mars Time",
    306 			abbr = "MTC",
    307 			dst = false,
    308 		},
    309 	],
    310 	transitions = [],
    311 	posix_extend = "",
    312 };