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