hare

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

timezone.ha (7421B)


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