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