hare

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

timezone.ha (8442B)


      1 // License: MPL-2.0
      2 // (c) 2021-2022 Byron Torres <b@torresjrjr.com>
      3 use bufio;
      4 use io;
      5 use os;
      6 use path;
      7 use strings;
      8 use time;
      9 
     10 // The locality of a [[moment]]. Contains information about how to present a
     11 // moment's chronological values.
     12 export type locality = *timezone;
     13 
     14 // A timezone; a political or general region with a ruleset regarding offsets
     15 // for calculating localized civil time.
     16 export type timezone = struct {
     17 	// The textual identifier ("Europe/Amsterdam")
     18 	name: str,
     19 
     20 	// The base timescale (chrono::utc)
     21 	timescale: *timescale,
     22 
     23 	// The duration of a day in this timezone (24 * time::HOUR)
     24 	daylength: time::duration,
     25 
     26 	// The possible temporal zones a locality with this timezone can observe
     27 	// (CET, CEST, ...)
     28 	zones: []zone,
     29 
     30 	// The transitions between this timezone's zones
     31 	transitions: []transition,
     32 
     33 	// A timezone specifier in the POSIX "expanded" TZ format.
     34 	// See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
     35 	//
     36 	// Used for extending calculations beyond the last known transition.
     37 	posix_extend: str,
     38 };
     39 
     40 // A [[timezone]] state, with an offset for calculating localized civil time.
     41 export type zone = struct {
     42 	// The offset from the normal timezone (2 * time::HOUR)
     43 	zoffset: time::duration,
     44 
     45 	// The full descriptive name ("Central European Summer Time")
     46 	name: str,
     47 
     48 	// The abbreviated name ("CEST")
     49 	abbr: str,
     50 
     51 	// Indicator of Daylight Saving Time
     52 	dst: bool, // true
     53 };
     54 
     55 // A [[timezone]] transition between two [[zone]]s.
     56 export type transition = struct {
     57 	when: time::instant,
     58 	zoneindex: int,
     59 };
     60 
     61 // A destructured dual std/dst POSIX timezone. See tzset(3).
     62 type tzname = struct {
     63 	std_name: str,
     64 	std_offset: time::duration,
     65 	dst_name: str,
     66 	dst_offset: time::duration,
     67 	dst_start: str,
     68 	dst_starttime: str,
     69 	dst_end: str,
     70 	dst_endtime: str,
     71 };
     72 
     73 // Creates an equivalent [[moment]] with a different [[locality]].
     74 //
     75 // If the old and new localities have different timescales, a direct conversion
     76 // between them will be tried, and will abort if unsuccessful. To avoid this,
     77 // consider manually converting moments to instants, and those instants between
     78 // timescales.
     79 export fn in(loc: locality, m: moment) moment = {
     80 	if (m.loc.timescale != loc.timescale) {
     81 		const i = to_instant(m);
     82 		const i = match (m.loc.timescale.to_tai(i)) {
     83 		case let i: time::instant =>
     84 			yield i;
     85 		case time::error =>
     86 			abort("time::chrono::in(): direct timescale conversion failed");
     87 		};
     88 		const i = match (loc.timescale.from_tai(i)) {
     89 		case let i: time::instant =>
     90 			yield i;
     91 		case time::error =>
     92 			abort("time::chrono::in(): direct timescale conversion failed");
     93 		};
     94 		const m = from_instant(i, loc);
     95 		return m;
     96 	};
     97 
     98 	assert(m.time < loc.daylength, "Internal error: time excedes daylength");
     99 	return new(loc, m.date, m.time)!; // resets .zone
    100 };
    101 
    102 export fn transform(m: moment, zo: time::duration) moment = {
    103 	const daylen = m.loc.daylength;
    104 
    105 	const t = m.time + zo;
    106 	const mtime = (if (t >= 0) t else t + daylen) % daylen;
    107 
    108 	const d = (t / daylen): int;
    109 	const mdate = m.date + (if (t >= 0) d else d - 1);
    110 
    111 	m.time = mtime;
    112 	m.date = mdate;
    113 	return m;
    114 };
    115 
    116 // Finds, sets and returns a [[moment]]'s currently observed zone.
    117 export fn lookupzone(m: *moment) zone = {
    118 	// TODO: https://todo.sr.ht/~sircmpwn/hare/643
    119 	if (len(m.loc.zones) == 0) {
    120 		// TODO: what to do? not ideal to assume UTC
    121 		abort("lookup(): timezones should have at least one zone");
    122 	};
    123 
    124 	if (len(m.loc.zones) == 1) {
    125 		m.zone = m.loc.zones[0];
    126 		return m.zone;
    127 	};
    128 
    129 	const inst = to_instant(*m);
    130 
    131 	if (
    132 		len(m.loc.transitions) == 0
    133 		|| time::compare(inst, m.loc.transitions[0].when) == -1
    134 	) {
    135 		// TODO: special case
    136 		abort("lookupzone(): time is before known transitions");
    137 	};
    138 
    139 	let lo = 0z;
    140 	let hi = len(m.loc.transitions);
    141 	for (hi - lo > 1) {
    142 		const mid = lo + (hi - lo) / 2;
    143 		const middle = m.loc.transitions[mid].when;
    144 		switch (time::compare(inst, middle)) {
    145 		case -1 =>
    146 			hi = mid;
    147 		case 0 =>
    148 			lo = mid; break;
    149 		case 1 =>
    150 			lo = mid;
    151 		case =>
    152 			abort("Unreachable");
    153 		};
    154 	};
    155 
    156 	m.zone = m.loc.zones[m.loc.transitions[lo].zoneindex];
    157 
    158 	// if we've reached the end of the locality's transitions, try its
    159 	// posix_extend string
    160 	//
    161 	// TODO: Unfinished; complete.
    162 	if (lo == len(m.loc.transitions) - 1 && m.loc.posix_extend != "") {
    163 		void;
    164 	};
    165 
    166 	return m.zone;
    167 };
    168 
    169 // Creates a [[timezone]] with a single [[zone]]. Useful for fixed offsets.
    170 // For example, replicate the civil time Hawaii timezone on Earth:
    171 //
    172 // 	let hawaii = chrono::fixedzone(&chrono::utc, chrono::EARTH_DAY,
    173 // 		chrono::zone {
    174 // 			zoffset = -10 * time::HOUR,
    175 // 			name = "Hawaiian Reef",
    176 // 			abbr = "HARE",
    177 // 			dst = false,
    178 // 		},
    179 // 	);
    180 //
    181 export fn fixedzone(ts: *timescale, daylen: time::duration, z: zone) timezone = {
    182 	return timezone {
    183 		name = z.name,
    184 		timescale = ts,
    185 		daylength = daylen,
    186 		zones = alloc([z]),
    187 		transitions = [],
    188 		posix_extend = "",
    189 	};
    190 };
    191 
    192 // The system's [[locality]]; the system's local [[timezone]].
    193 //
    194 // This is set during a program's initialisation, where the TZ environment
    195 // variable is tried, otherwise the /etc/localtime file is tried, otherwise a
    196 // default is used.
    197 //
    198 // The default timezone is equivalent to that of [[UTC]], with "Local" being the
    199 // name of both the timezone and its single zero-offset zone.
    200 export const LOCAL: locality = &TZ_LOCAL;
    201 
    202 def LOCAL_NAME: str = "Local";
    203 
    204 let TZ_LOCAL: timezone = timezone {
    205 	name = LOCAL_NAME,
    206 	timescale = &utc,
    207 	daylength = EARTH_DAY,
    208 	zones = [
    209 		zone {
    210 			zoffset = 0 * time::SECOND,
    211 			name = LOCAL_NAME,
    212 			abbr = "",
    213 			dst = false,
    214 		},
    215 	],
    216 	transitions = [],
    217 	posix_extend = "",
    218 };
    219 
    220 @fini fn free_tzdata() void = {
    221 	free(TZ_LOCAL.transitions);
    222 	switch(TZ_LOCAL.name) {
    223 	case LOCAL_NAME => void;
    224 	case =>
    225 		free(TZ_LOCAL.zones);
    226 	};
    227 };
    228 
    229 @init fn set_local_timezone() void = {
    230 	match (os::getenv("TZ")) {
    231 	case let zone: str =>
    232 		TZ_LOCAL = match (tz(zone)) {
    233 		case let tz: timezone =>
    234 			yield tz;
    235 		case =>
    236 			return;
    237 		};
    238 	case void =>
    239 		const filepath = match (os::readlink(LOCALTIME_PATH)) {
    240 		case let fp: str =>
    241 			yield fp;
    242 		case =>
    243 			yield LOCALTIME_PATH;
    244 		};
    245 
    246 		const file = match (os::open(filepath)) {
    247 		case let f: io::file =>
    248 			yield f;
    249 		case =>
    250 			return;
    251 		};
    252 		defer io::close(file)!;
    253 
    254 		if (strings::hasprefix(filepath, ZONEINFO_PREFIX)) {
    255 			TZ_LOCAL.name = strings::trimprefix(
    256 				filepath, ZONEINFO_PREFIX,
    257 			);
    258 		};
    259 
    260 		static let buf: [os::BUFSIZ]u8 = [0...];
    261 		const file = bufio::buffered(file, buf, []);
    262 		match (parse_tzif(&file, &TZ_LOCAL)) { case => void; };
    263 	};
    264 };
    265 
    266 // The UTC (Coordinated Universal Time) "Zulu" [[timezone]] as a [[locality]].
    267 export const UTC: locality = &TZ_UTC;
    268 
    269 const TZ_UTC: timezone = timezone {
    270 	name = "UTC",
    271 	timescale = &utc,
    272 	daylength = EARTH_DAY,
    273 	zones = [
    274 		zone {
    275 			zoffset = 0 * time::SECOND,
    276 			name = "Universal Coordinated Time",
    277 			abbr = "UTC",
    278 			dst = false,
    279 		},
    280 	],
    281 	transitions = [],
    282 	posix_extend = "",
    283 };
    284 
    285 // The TAI (International Atomic Time) "Zulu" [[timezone]] as a [[locality]].
    286 export const TAI: locality = &TZ_TAI;
    287 
    288 const TZ_TAI: timezone = timezone {
    289 	name = "",
    290 	timescale = &tai,
    291 	daylength = EARTH_DAY,
    292 	zones = [
    293 		zone {
    294 			zoffset = 0 * time::SECOND,
    295 			name = "International Atomic Time",
    296 			abbr = "TAI",
    297 			dst = false,
    298 		},
    299 	],
    300 	transitions = [],
    301 	posix_extend = "",
    302 };
    303 
    304 // The GPS (Global Positioning System) "Zulu" [[timezone]] as a [[locality]].
    305 export const GPS: locality = &TZ_GPS;
    306 
    307 const TZ_GPS: timezone = timezone {
    308 	name = "",
    309 	timescale = &gps,
    310 	daylength = EARTH_DAY,
    311 	zones = [
    312 		zone {
    313 			zoffset = 0 * time::SECOND,
    314 			name = "Global Positioning System",
    315 			abbr = "GPS",
    316 			dst = false,
    317 		},
    318 	],
    319 	transitions = [],
    320 	posix_extend = "",
    321 };
    322 
    323 // The TT (Terrestrial Time) "Zulu" [[timezone]] as a [[locality]].
    324 export const TT: locality = &TZ_TT;
    325 
    326 const TZ_TT: timezone = timezone {
    327 	name = "",
    328 	timescale = &tt,
    329 	daylength = EARTH_DAY,
    330 	zones = [
    331 		zone {
    332 			zoffset = 0 * time::SECOND,
    333 			name = "Terrestrial Time",
    334 			abbr = "TT",
    335 			dst = false,
    336 		},
    337 	],
    338 	transitions = [],
    339 	posix_extend = "",
    340 };
    341 
    342 // The MTC (Coordinated Mars Time) "Zulu" [[timezone]] as a [[locality]].
    343 export const MTC: locality = &TZ_MTC;
    344 
    345 const TZ_MTC: timezone = timezone {
    346 	name = "",
    347 	timescale = &mtc,
    348 	daylength = MARS_SOL_MARTIAN,
    349 	zones = [
    350 		zone {
    351 			zoffset = 0 * time::SECOND,
    352 			name = "Coordinated Mars Time",
    353 			abbr = "MTC",
    354 			dst = false,
    355 		},
    356 	],
    357 	transitions = [],
    358 	posix_extend = "",
    359 };