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