time.bend

bend-kit-time@0.1.0.0 · 686 lines · 0 definitions

1# Clocks, Duration and Instant, Gregorian dates, RFC 3339 and HTTP-date text, and TZif time zones. Source: https://github.com/paymog/bend-kit/tree/main/time
2import Base
3import bend-kit-int@0.2.0.0/int.bend as Int
4import bend-kit-fmt@0.1.0.0/fmt.bend as Fmt
5# bend-kit-bytes@0.3.0.0
6import bend-kit-bytes@0.3.0.0/bytes.bend as Bytes
7
8# secs + nanos / 10^9 seconds, with nanos below 10^9: -0.5 s is Duration{-1, 500000000}.
9type Duration is Data:
10  Duration{secs: Int.I64, nanos: U32}
11
12# A Duration since 1970-01-01T00:00:00Z. Days are 86400 s: there are no leap seconds.
13type Instant is Data:
14  Instant{secs: Int.I64, nanos: U32}
15
16# A proleptic Gregorian date. The conversions and formats cover years 0 to 9999.
17type Date is Data:
18  Date{year: U32, month: U32, day: U32}
19
20type DateTime is Data:
21  DateTime{date: Date, hour: U32, min: U32, sec: U32, nanos: U32}
22
23def i64(x: U32) -> Int.I64:
24  Int.I64.from_u32(x)
25
26# The signed 32-bit value of x, widened.
27def i64s(x: U32) -> Int.I64:
28  match x:
29    case U32{w}:
30      Int.I64{Int.Int.sext(64n, 32n, w)}
31
32# hi * 2^32 + lo, as the bits of an I64.
33def i64.of(hi: U32, lo: U32) -> Int.I64:
34  Int.I64.or(Int.I64.shl(i64(hi), 32n), i64(lo))
35
36# Duration
37# --------
38
39def Duration.zero() -> Duration:
40  Duration{i64(0), 0}
41
42def Duration.of_secs(s: Int.I64) -> Duration:
43  Duration{s, 0}
44
45def Duration.of_ms(+ms: U32) -> Duration:
46  Duration{i64((ms / 1000 : U32)), ((ms % 1000 : U32) * 1000000 : U32)}
47
48# Whole milliseconds, rounded down.
49def Duration.to_ms(d: Duration) -> Int.I64:
50  match d:
51    case Duration{s, n}:
52      Int.I64.add(Int.I64.mul(s, i64(1000)), i64((n / 1000000 : U32)))
53
54def Duration.carry(c: Bool, s: Int.I64, +n: U32) -> Duration:
55  match c:
56    case True{}:
57      Duration{Int.I64.add(s, i64(1)), (n - 1000000000 : U32)}
58    case False{}:
59      Duration{s, n}
60
61def Duration.add(a: Duration, b: Duration) -> Duration:
62  match a b:
63    case Duration{sa, na} Duration{sb, nb}:
64      +n = (na + nb : U32)
65      Duration.carry(U32.is_ge(n, 1000000000), Int.I64.add(sa, sb), n)
66
67# n is na - nb, wrapped when na < nb.
68def Duration.borrow(c: Bool, s: Int.I64, +n: U32) -> Duration:
69  match c:
70    case True{}:
71      Duration{Int.I64.sub(s, i64(1)), (n + 1000000000 : U32)}
72    case False{}:
73      Duration{s, n}
74
75def Duration.sub(a: Duration, b: Duration) -> Duration:
76  match a b:
77    case Duration{sa, +na} Duration{sb, +nb}:
78      Duration.borrow(U32.is_lt(na, nb), Int.I64.sub(sa, sb), (na - nb : U32))
79
80def Duration.neg(d: Duration) -> Duration:
81  Duration.sub(Duration.zero(), d)
82
83def Duration.cmp.then(c: Cmp, +na: U32, +nb: U32) -> Cmp:
84  match c:
85    case EQ{}:
86      U32.cmp(na, nb)
87    case LT{}:
88      LT{}
89    case GT{}:
90      GT{}
91
92def Duration.cmp(a: Duration, b: Duration) -> Cmp:
93  match a b:
94    case Duration{sa, na} Duration{sb, nb}:
95      Duration.cmp.then(Int.I64.cmp(sa, sb), na, nb)
96
97# Instant
98# -------
99
100def Instant.epoch() -> Instant:
101  Instant{i64(0), 0}
102
103def Instant.of_unix(s: Int.I64) -> Instant:
104  Instant{s, 0}
105
106def Instant.of(d: Duration) -> Instant:
107  match d:
108    case Duration{s, n}:
109      Instant{s, n}
110
111def Instant.since_epoch(i: Instant) -> Duration:
112  match i:
113    case Instant{s, n}:
114      Duration{s, n}
115
116def Instant.add(i: Instant, d: Duration) -> Instant:
117  Instant.of(Duration.add(Instant.since_epoch(i), d))
118
119# a - b.
120def Instant.since(a: Instant, b: Instant) -> Duration:
121  Duration.sub(Instant.since_epoch(a), Instant.since_epoch(b))
122
123def Instant.cmp(a: Instant, b: Instant) -> Cmp:
124  Duration.cmp(Instant.since_epoch(a), Instant.since_epoch(b))
125
126# Clocks
127# ------
128
129# (secs hi, secs lo, nanos) of CLOCK_MONOTONIC, from an unspecified start.
130def mono.raw() -> IO(U32 & U32 & U32):
131  import "./effs/time.c"
132  import "./effs/time.js"
133
134# (secs hi, secs lo, nanos) of CLOCK_REALTIME, since the Unix epoch.
135def wall.raw() -> IO(U32 & U32 & U32):
136  import "./effs/time.c"
137  import "./effs/time.js"
138
139def raw.dur(r: U32 & U32 & U32) -> Duration:
140  (hi, lo, n) = r
141  Duration{i64.of(hi, lo), n}
142
143# Time since an unspecified start that never goes back. Use it to measure spans.
144def mono() -> IO(Duration):
145  do IO<Duration>:
146    r : U32 & U32 & U32 <- mono.raw()
147    return raw.dur(r)
148
149# The wall clock. It can jump when the system time is set.
150def now() -> IO(Instant):
151  do IO<Instant>:
152    r : U32 & U32 & U32 <- wall.raw()
153    return Instant.of(raw.dur(r))
154
155# Dates
156# -----
157
158def Date.is_leap(+y: U32) -> Bool:
159  Bool.and(U32.is_eq((y % 4 : U32), 0),
160    Bool.or(U32.is_ne((y % 100 : U32), 0), U32.is_eq((y % 400 : U32), 0)))
161
162# 0 for a month outside 1 to 12.
163def Date.days_in_month(+y: U32, +m: U32) -> U32:
164  Bool.pick(U32, U32.is_eq(m, 2), Bool.pick(U32, Date.is_leap(y), 29, 28),
165    Bool.pick(U32, Bool.and(U32.is_ge(m, 1), U32.is_le(m, 12)),
166      (30 + ((m + (m >> 3n)) .&. 1) : U32), 0))
167
168def Date.valid(d: Date) -> Bool:
169  match d:
170    case Date{+y, +m, +dd}:
171      Bool.and(U32.is_le(y, 9999),
172        Bool.and(U32.is_ge(dd, 1), U32.is_le(dd, Date.days_in_month(y, m))))
173
174# z counts days from -0400-03-01, so every date from year 0 has z > 0 in a U32.
175# The civil-days algorithm is Howard Hinnant's, with the year starting in March.
176def Z.EPOCH() -> U32:
177  865565
178
179def Date.to_z.at(+y: U32, +mp: U32, +d: U32) -> U32:
180  +era = (y / 400 : U32)
181  +yoe = (y - era * 400 : U32)
182  +doy = ((153 * mp + 2) / 5 + d - 1 : U32)
183  (era * 146097 + yoe * 365 + yoe / 4 - yoe / 100 + doy : U32)
184
185def Date.to_z.go(jf: Bool, +y: U32, +m: U32, +d: U32) -> U32:
186  match jf:
187    case True{}:
188      Date.to_z.at((y + 399 : U32), (m + 9 : U32), d)
189    case False{}:
190      Date.to_z.at((y + 400 : U32), (m - 3 : U32), d)
191
192def Date.to_z(d: Date) -> U32:
193  match d:
194    case Date{+y, +m, +dd}:
195      Date.to_z.go(U32.is_le(m, 2), y, m, dd)
196
197def Date.of_z.fin(lo: Bool, +y: U32, +mp: U32, +d: U32) -> Date:
198  match lo:
199    case True{}:
200      Date{(y - 400 : U32), (mp + 3 : U32), d}
201    case False{}:
202      Date{(y - 399 : U32), (mp - 9 : U32), d}
203
204def Date.of_z(+z: U32) -> Date:
205  +era = (z / 146097 : U32)
206  +doe = (z - era * 146097 : U32)
207  +yoe = ((doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 : U32)
208  +doy = (doe - (365 * yoe + yoe / 4 - yoe / 100) : U32)
209  +mp = ((5 * doy + 2) / 153 : U32)
210  Date.of_z.fin(U32.is_lt(mp, 10), (yoe + era * 400 : U32), mp,
211    (doy - (153 * mp + 2) / 5 + 1 : U32))
212
213# Days since 1970-01-01.
214def Date.to_days(d: Date) -> Int.I64:
215  Int.I64.sub(i64(Date.to_z(d)), i64(Z.EPOCH()))
216
217def Date.of_z.if(ok: Bool, +z: U32) -> Maybe<&2, Date>:
218  match ok:
219    case True{}:
220      Some{Date.of_z(z)}
221    case False{}:
222      None{}
223
224# The date days after 1970-01-01, or None outside 0000-01-01 to 9999-12-31.
225def Date.of_days(days: Int.I64) -> Maybe<&2, Date>:
226  +z = Int.I64.add(days, i64(Z.EPOCH()))
227  Date.of_z.if(Bool.and(Int.I64.is_ge(z, i64(146037)), Int.I64.is_le(z, i64(3798461))),
228    Int.I64.to_u32(z))
229
230# 1 for Monday to 7 for Sunday, as in ISO 8601.
231def Date.weekday(d: Date) -> U32:
232  ((Date.to_z(d) + 2) % 7 + 1 : U32)
233
234# Date and time
235# -------------
236
237def DateTime.valid(t: DateTime) -> Bool:
238  match t:
239    case DateTime{d, h, mi, s, n}:
240      Bool.and(Date.valid(d), Bool.and(U32.is_le(h, 23),
241        Bool.and(U32.is_le(mi, 59), Bool.and(U32.is_le(s, 59), U32.is_lt(n, 1000000000)))))
242
243# The instant of a UTC date and time. It does not check t; see DateTime.valid.
244def DateTime.to_instant(t: DateTime) -> Instant:
245  match t:
246    case DateTime{d, h, mi, s, n}:
247      Instant{Int.I64.add(Int.I64.mul(Date.to_days(d), i64(86400)),
248        i64((h * 3600 + mi * 60 + s : U32))), n}
249
250def DateTime.at(m: Maybe<&2, Date>, +sod: U32, n: U32) -> Maybe<&2, DateTime>:
251  match m:
252    case None{}:
253      None{}
254    case Some{d}:
255      Some{DateTime{d, (sod / 3600 : U32), (sod / 60 % 60 : U32), (sod % 60 : U32), n}}
256
257# r is secs % 86400, which has the sign of secs.
258def DateTime.floor(neg: Bool, q: Int.I64, r: Int.I64, n: U32) -> Maybe<&2, DateTime>:
259  match neg:
260    case True{}:
261      DateTime.at(Date.of_days(Int.I64.sub(q, i64(1))), (Int.I64.to_u32(r) + 86400 : U32), n)
262    case False{}:
263      DateTime.at(Date.of_days(q), Int.I64.to_u32(r), n)
264
265# The UTC date and time of i, or None outside years 0 to 9999.
266def DateTime.of_instant(i: Instant) -> Maybe<&2, DateTime>:
267  match i:
268    case Instant{+s, n}:
269      +r = Int.I64.mod(s, i64(86400))
270      DateTime.floor(Int.I64.is_neg(r), Int.I64.div(s, i64(86400)), r, n)
271
272# Text
273# ----
274
275def pad(+x: U32, w: Nat) -> String:
276  Fmt.Fmt.pad_left(U32.show(x), w, '0')
277
278# The digits of n / 10^(9 - w), with trailing zeros cut. n is not 0.
279def frac.go(f: Nat, z: Bool, +n: U32, +w: Nat) -> String:
280  match f:
281    case 0n:
282      pad(n, w)
283    case 1n+g:
284      match z:
285        case True{}:
286          +m = (n / 10 : U32)
287          frac.go(g, U32.is_eq((m % 10 : U32), 0), m, (w - 1n : Nat))
288        case False{}:
289          pad(n, w)
290
291def frac(+n: U32) -> String:
292  Bool.pick(String, U32.is_eq(n, 0), "", SCon{'.', frac.go(8n, U32.is_eq((n % 10 : U32), 0), n, 9n)})
293
294def DateTime.rfc3339(t: DateTime) -> String:
295  match t:
296    case DateTime{Date{y, mo, d}, h, mi, s, n}:
297      String.concat([pad(y, 4n), "-", pad(mo, 2n), "-", pad(d, 2n), "T", pad(h, 2n), ":",
298        pad(mi, 2n), ":", pad(s, 2n), frac(n), "Z"])
299
300def fmt.map(~f: DateTime -> String, m: Maybe<&2, DateTime>) -> Maybe<&2, String>:
301  match m:
302    case None{}:
303      None{}
304    case Some{t}:
305      Some{f(t)}
306
307# RFC 3339 in UTC, such as 1985-04-12T23:20:50.52Z. The fraction has no trailing zeros, and is left out when 0.
308def rfc3339(i: Instant) -> Maybe<&2, String>:
309  fmt.map(~DateTime.rfc3339, DateTime.of_instant(i))
310
311def WEEKDAYS() -> List<&2, String>:
312  ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
313
314def MONTHS() -> List<&2, String>:
315  ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
316
317def name(xs: List<&2, String>, +i: U32) -> String:
318  Maybe.default(&2, String, List.get(&2, String, xs, U32.to_nat(i)), "")
319
320def DateTime.http_date(t: DateTime) -> String:
321  match t:
322    case DateTime{+date, h, mi, s, n}:
323      Date{y, mo, d} = date
324      String.concat([name(WEEKDAYS(), (Date.weekday(date) - 1 : U32)), ", ", pad(d, 2n), " ",
325        name(MONTHS(), (mo - 1 : U32)), " ", pad(y, 4n), " ", pad(h, 2n), ":", pad(mi, 2n), ":",
326        pad(s, 2n), " GMT"])
327
328# IMF-fixdate (RFC 9110 §5.6.7), such as Sun, 06 Nov 1994 08:49:37 GMT. It drops the fraction of a second.
329def http_date(i: Instant) -> Maybe<&2, String>:
330  fmt.map(~DateTime.http_date, DateTime.of_instant(i))
331
332# Parsers
333# -------
334
335# A parser state: the numbers read so far, newest first, and the text left.
336type P is Data:
337  P{vals: List<&2, U32>, rest: String}
338
339def is_digit(+c: U32) -> Bool:
340  Bool.and(U32.is_ge(c, 48), U32.is_le(c, 57))
341
342def digits.go(n: Nat, s: String, +acc: U32, ok: Bool) -> Maybe<&1, U32 & String>:
343  match n:
344    case 0n:
345      match ok:
346        case True{}:
347          Some{(acc, s)}
348        case False{}:
349          None{}
350    case 1n+k:
351      match s:
352        case SNil{}:
353          None{}
354        case SCon{Chr{+c}, t}:
355          digits.go(k, t, (acc * 10 + c - 48 : U32), Bool.and(ok, is_digit(c)))
356
357def push(vs: List<&2, U32>, r: Maybe<&1, U32 & String>) -> Maybe<&2, P>:
358  match r:
359    case None{}:
360      None{}
361    case Some{(v, t)}:
362      Some{P{Con{v, vs}, t}}
363
364# Exactly n digits.
365def num(n: Nat, p: P) -> Maybe<&2, P>:
366  match p:
367    case P{vs, s}:
368      push(vs, digits.go(n, s, 0, True{}))
369
370def lit.if(ok: Bool, vs: List<&2, U32>, t: String) -> Maybe<&2, P>:
371  match ok:
372    case True{}:
373      Some{P{vs, t}}
374    case False{}:
375      None{}
376
377# One char that is a or b.
378def lit2(+a: U32, +b: U32, p: P) -> Maybe<&2, P>:
379  match p:
380    case P{vs, s}:
381      match s:
382        case SNil{}:
383          None{}
384        case SCon{Chr{+c}, t}:
385          lit.if(Bool.or(U32.is_eq(c, a), U32.is_eq(c, b)), vs, t)
386
387def lit(+c: U32, p: P) -> Maybe<&2, P>:
388  lit2(c, c, p)
389
390def text(+w: String, p: P) -> Maybe<&2, P>:
391  match p:
392    case P{vs, +s}:
393      +n = String.length(w)
394      lit.if(String.eq(String.take(s, n), w), vs, String.drop(s, n))
395
396def at.head(+s: String) -> Bool:
397  match s:
398    case SNil{}:
399      False{}
400    case SCon{Chr{+c}, t}:
401      is_digit(c)
402
403# Digits after the first nine are read and dropped. d says whether s starts with a digit.
404def secfrac.go(s: String, d: Bool, +mul: U32, +acc: U32, +k: U32) -> U32 & U32 & String:
405  match s:
406    case SNil{}:
407      (k, acc, SNil{})
408    case SCon{Chr{+c}, +t}:
409      match d:
410        case True{}:
411          secfrac.go(t, at.head(t), (mul / 10 : U32), (acc + (c - 48) * mul : U32), (k + 1 : U32))
412        case False{}:
413          (k, acc, SCon{Chr{c}, t})
414
415def secfrac.fin(vs: List<&2, U32>, r: U32 & U32 & String) -> Maybe<&2, P>:
416  (k, n, t) = r
417  lit.if(U32.is_gt(k, 0), Con{n, vs}, t)
418
419def secfrac.if(dot: Bool, vs: List<&2, U32>, +s: String) -> Maybe<&2, P>:
420  match dot:
421    case True{}:
422      +t = String.drop(s, 1n)
423      secfrac.fin(vs, secfrac.go(t, at.head(t), 100000000, 0, 0))
424    case False{}:
425      Some{P{Con{0, vs}, s}}
426
427# An optional '.' and one or more digits, as nanoseconds.
428def secfrac(p: P) -> Maybe<&2, P>:
429  match p:
430    case P{vs, +s}:
431      secfrac.if(String.starts_with(s, "."), vs, s)
432
433def zone.num(+neg: U32, vs: List<&2, U32>, t: String) -> Maybe<&2, P>:
434  do Maybe<&2, P>:
435    a : P <- num(2n, P{Con{neg, vs}, t})
436    b : P <- lit(58, a)
437    num(2n, b)
438
439def zone.if(z: Bool, plus: Bool, minus: Bool, vs: List<&2, U32>, t: String) -> Maybe<&2, P>:
440  match z:
441    case True{}:
442      Some{P{Con{0, Con{0, Con{0, vs}}}, t}}
443    case False{}:
444      match plus:
445        case True{}:
446          zone.num(0, vs, t)
447        case False{}:
448          match minus:
449            case True{}:
450              zone.num(1, vs, t)
451            case False{}:
452              None{}
453
454# Z, or +hh:mm or -hh:mm, as (sign, hh, mm) with sign 1 for west of UTC.
455def zone(p: P) -> Maybe<&2, P>:
456  match p:
457    case P{vs, s}:
458      match s:
459        case SNil{}:
460          None{}
461        case SCon{Chr{+c}, t}:
462          zone.if(Bool.or(U32.is_eq(c, 90), U32.is_eq(c, 122)), U32.is_eq(c, 43), U32.is_eq(c, 45), vs, t)
463
464def index.go(xs: List<&2, String>, +w: String, +i: U32) -> U32:
465  match xs:
466    case Nil{}:
467      i
468    case Con{x, rest}:
469      Bool.pick(U32, String.eq(x, w), i, index.go(rest, w, (i + 1 : U32)))
470
471# One of names, as its index.
472def word(+names: List<&2, String>, p: P) -> Maybe<&2, P>:
473  match p:
474    case P{vs, +s}:
475      +i = index.go(names, String.take(s, 3n), 0)
476      lit.if(U32.is_lt(i, U32.from_nat(List.length(&2, String, names))), Con{i, vs}, String.drop(s, 3n))
477
478def mk.if(ok: Bool, t: DateTime) -> Maybe<&2, Instant>:
479  match ok:
480    case True{}:
481      Some{DateTime.to_instant(t)}
482    case False{}:
483      None{}
484
485def mk(+t: DateTime) -> Maybe<&2, Instant>:
486  mk.if(DateTime.valid(t), t)
487
488def rfc3339.shift(m: Maybe<&2, Instant>, +west: U32, +hh: U32, +mm: U32) -> Maybe<&2, Instant>:
489  match m:
490    case None{}:
491      None{}
492    case Some{i}:
493      +off = Duration.of_secs(i64((hh * 3600 + mm * 60 : U32)))
494      Bool.pick(Maybe<&2, Instant>, Bool.and(U32.is_le(hh, 23), U32.is_le(mm, 59)),
495        Some{Instant.add(i, Bool.pick(Duration, U32.is_eq(west, 1), off, Duration.neg(off)))}, None{})
496
497def rfc3339.fin(p: P) -> Maybe<&2, Instant>:
498  match p:
499    case P{Con{mm, Con{hh, Con{west, Con{n, Con{s, Con{mi, Con{h, Con{d, Con{mo, Con{y, Nil{}}}}}}}}}}}, SNil{}}:
500      rfc3339.shift(mk(DateTime{Date{y, mo, d}, h, mi, s, n}), west, hh, mm)
501    case _:
502      None{}
503
504# An RFC 3339 date-time (§5.6), such as 1985-04-12T23:20:50.52Z or 1996-12-19T16:39:57-08:00.
505# T and Z may be lowercase. A leap second (:60) is refused, as Instant has none.
506def rfc3339.parse(s: String) -> Maybe<&2, Instant>:
507  do Maybe<&2, Instant>:
508    a : P <- num(4n, P{Nil{}, s})
509    b : P <- lit(45, a)
510    c : P <- num(2n, b)
511    d : P <- lit(45, c)
512    e : P <- num(2n, d)
513    f : P <- lit2(84, 116, e)
514    g : P <- num(2n, f)
515    h : P <- lit(58, g)
516    i : P <- num(2n, h)
517    j : P <- lit(58, i)
518    k : P <- num(2n, j)
519    l : P <- secfrac(k)
520    m : P <- zone(l)
521    rfc3339.fin(m)
522
523def http_date.fin(p: P) -> Maybe<&2, Instant>:
524  match p:
525    case P{Con{s, Con{mi, Con{h, Con{y, Con{mo, Con{d, Con{wd, Nil{}}}}}}}}, SNil{}}:
526      mk(DateTime{Date{y, (mo + 1 : U32), d}, h, mi, s, 0})
527    case _:
528      None{}
529
530# An IMF-fixdate (RFC 9110 §5.6.7), such as Sun, 06 Nov 1994 08:49:37 GMT.
531# The day name must be one of the seven, but it is not checked against the date.
532def http_date.parse(s: String) -> Maybe<&2, Instant>:
533  do Maybe<&2, Instant>:
534    a : P <- word(WEEKDAYS(), P{Nil{}, s})
535    b : P <- text(", ", a)
536    c : P <- num(2n, b)
537    d : P <- lit(32, c)
538    e : P <- word(MONTHS(), d)
539    f : P <- lit(32, e)
540    g : P <- num(4n, f)
541    h : P <- lit(32, g)
542    i : P <- num(2n, h)
543    j : P <- lit(58, i)
544    k : P <- num(2n, j)
545    l : P <- lit(58, k)
546    m : P <- num(2n, l)
547    n : P <- text(" GMT", m)
548    http_date.fin(n)
549
550# Time zones
551# ----------
552
553# A local time type: seconds east of UTC, whether it is daylight time, and its abbreviation.
554type Ttype is Data:
555  Ttype{utoff: Int.I64, dst: Bool, abbr: String}
556
557# From at on, local time follows types[idx].
558type Trans is Data:
559  Trans{at: Int.I64, idx: U32}
560
561# A TZif zone (RFC 8536): transitions in time order, local time types, and the footer TZ string ("" in version 1).
562type Zone is Data:
563  Zone{trans: List<&2, Trans>, types: List<&2, Ttype>, tz: String}
564
565# n bytes of a byte string, big-endian.
566def be(n: Nat, s: String, +acc: U32) -> U32:
567  match n:
568    case 0n:
569      acc
570    case 1n+k:
571      match s:
572        case SNil{}:
573          acc
574        case SCon{Chr{c}, t}:
575          be(k, t, ((acc << 8n) .|. c : U32))
576
577def be32(+s: String, n: Nat) -> U32:
578  be(4n, String.drop(s, n), 0)
579
580def u8(+s: String, n: Nat) -> U32:
581  be(1n, String.drop(s, n), 0)
582
583def tzif.size(t8: Bool) -> Nat:
584  match t8:
585    case True{}:
586      8n
587    case False{}:
588      4n
589
590def tzif.time(t8: Bool, +s: String) -> Int.I64:
591  match t8:
592    case True{}:
593      i64.of(be32(s, 0n), be32(s, 4n))
594    case False{}:
595      i64s(be32(s, 0n))
596
597# ts holds the transition times and ks their type indexes.
598def tzif.trans(n: Nat, +t8: Bool, +ts: String, +ks: String) -> List<&2, Trans>:
599  match n:
600    case 0n:
601      Nil{}
602    case 1n+k:
603      Con{Trans{tzif.time(t8, ts), u8(ks, 0n)},
604        tzif.trans(k, t8, String.drop(ts, tzif.size(t8)), String.drop(ks, 1n))}
605
606# The NUL-terminated abbreviation at byte i of chars.
607def tzif.abbr(chars: String, +i: U32) -> String:
608  name(String.split(String.drop(chars, U32.to_nat(i)), Chr{0}), 0)
609
610def tzif.types(n: Nat, +s: String, +chars: String) -> List<&2, Ttype>:
611  match n:
612    case 0n:
613      Nil{}
614    case 1n+k:
615      Con{Ttype{i64s(be32(s, 0n)), U32.is_ne(u8(s, 4n), 0), tzif.abbr(chars, u8(s, 5n))},
616        tzif.types(k, String.drop(s, 6n), chars)}
617
618def tzif.if(ok: Bool, z: Zone) -> Maybe<&2, Zone>:
619  match ok:
620    case True{}:
621      Some{z}
622    case False{}:
623      None{}
624
625# Header counts, in RFC 8536 order: isut, isstd, leap, time, type, char.
626def tzif.count(+h: String, i: Nat) -> Nat:
627  U32.to_nat(be32(h, (20n + 4n * i : Nat)))
628
629# The data block's size in bytes, for header h.
630def tzif.bsize(+t8: Bool, +h: String) -> Nat:
631  +tc = tzif.count(h, 3n)
632  (tc * tzif.size(t8) + tc + tzif.count(h, 4n) * 6n + tzif.count(h, 5n) +
633    tzif.count(h, 2n) * (tzif.size(t8) + 4n) + tzif.count(h, 1n) + tzif.count(h, 0n) : Nat)
634
635# h starts at a header; its data block follows it. Version 2+ blocks have 8-byte times and a footer.
636def tzif.block(+t8: Bool, +h: String) -> Maybe<&2, Zone>:
637  +d = String.drop(h, 44n)
638  +tc = tzif.count(h, 3n)
639  +ty = tzif.count(h, 4n)
640  +at = (tc * tzif.size(t8) + tc : Nat)
641  +foot = String.drop(d, tzif.bsize(t8, h))
642  tzif.if(Bool.and(String.starts_with(h, "TZif"), Bool.and(Nat.is_gt(ty, 0n),
643      Nat.is_le((44n + tzif.bsize(t8, h) : Nat), String.length(h)))),
644    Zone{tzif.trans(tc, t8, d, String.drop(d, (tc * tzif.size(t8) : Nat))),
645      tzif.types(ty, String.drop(d, at), String.take(String.drop(d, (at + ty * 6n : Nat)), tzif.count(h, 5n))),
646      Bool.pick(String, t8, name(String.split(foot, '\n'), 1), "")})
647
648def tzif.go(v2: Bool, +s: String) -> Maybe<&2, Zone>:
649  match v2:
650    case True{}:
651      tzif.block(True{}, String.drop(s, (44n + tzif.bsize(False{}, s) : Nat)))
652    case False{}:
653      tzif.block(False{}, s)
654
655# A TZif file, such as one from /usr/share/zoneinfo. Versions 2 and up use their 64-bit block.
656# ponytail: reads the bytes as a byte String; walk Bytes directly if zone files get big.
657def tzif.parse(b: Bytes.Bytes) -> Maybe<&2, Zone>:
658  +s = Bytes.to_string(b)
659  tzif.go(U32.is_ge(u8(s, 4n), 50), s)
660
661# The type index at secs: the last transition at or before it, or type 0 before the first.
662# ponytail: linear scan; bisect an Array if lookups get hot.
663def Zone.find(ts: List<&2, Trans>, +secs: Int.I64, +cur: U32) -> U32:
664  match ts:
665    case Nil{}:
666      cur
667    case Con{Trans{t, k}, rest}:
668      Zone.find(rest, secs, Bool.pick(U32, Int.I64.is_le(t, secs), k, cur))
669
670# The local time type at i.
671# ponytail: past the last transition the last type holds; evaluate the footer TZ rule for slim files and years past 2037.
672def Zone.at(z: Zone, i: Instant) -> Maybe<&2, Ttype>:
673  match z i:
674    case Zone{ts, types, tz} Instant{s, n}:
675      List.get(&2, Ttype, types, U32.to_nat(Zone.find(ts, s, 0)))
676
677def Zone.local.at(m: Maybe<&2, Ttype>, i: Instant) -> Maybe<&2, DateTime>:
678  match m:
679    case None{}:
680      None{}
681    case Some{Ttype{off, dst, abbr}}:
682      DateTime.of_instant(Instant.add(i, Duration.of_secs(off)))
683
684# The local date and time at i in zone z.
685def Zone.local(z: Zone, +i: Instant) -> Maybe<&2, DateTime>:
686  Zone.local.at(Zone.at(z, i), i)