ref: 01be94abcf0a010b24bc7f7b177bf40c5c45b7f1
parent: c1c1c42c36717dea8dbca9e1bed8d794144ec6f1
parent: 35a72b699a732f3a331df2595970eb7b532f457d
author: Ori Bernstein <[email protected]>
date: Fri Nov 6 18:23:10 EST 2015
Merge ../libdate-merge
--- /dev/null
+++ b/lib/date/bld.proj
@@ -1,0 +1,9 @@
+lib date =
+ date.myr
+ fmt.myr
+ names.myr
+ parse.myr
+ types.myr
+ zoneinfo+posixy.myr
+;;
+
--- /dev/null
+++ b/lib/date/date.myr
@@ -1,0 +1,227 @@
+use std
+use "types.use"
+use "zoneinfo.use"
+
+pkg date =
+ /* useful constructors */
+ const utcnow : (-> instant)
+ const now : (tz : byte[:] -> instant)
+ const tozone : (d : instant, zone : byte[:] -> instant)
+ const mkdate : (y : int, m : int, day : int, zone : byte[:] -> instant)
+ const mkdatetime : (year : int, mon : int, day : int, \
+ h : int, m : int, s : int, zone : byte[:] -> instant)
+ const mkinstant : (tm : std.time, zone : byte[:] -> instant)
+
+ const localoff : (tm : std.time -> duration)
+ const tzoff : (tzname : byte[:], tm : std.time -> duration)
+ const tzname : (tzoff : int -> byte[:])
+ const isleap : (d : instant -> bool)
+
+ /* date differences */
+ const add : (d : instant, dt : duration -> instant)
+ const sub : (d : instant, dt : duration -> instant)
+ const addperiod : (d : instant, dt : period -> instant)
+ const subperiod : (d : instant, dt : period -> instant)
+
+ const duration : (a : instant, b : instant -> duration)
+
+ pkglocal const recalc : (inst : instant# -> std.time)
+;;
+
+const Days400y = 365*400 + 4*25 - 3
+const Days4y = 365*4 + 1
+const DayUsec = (24*60*60*1_000_000)
+const Mdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+
+const utcnow = {
+ -> mkinstant(std.now(), "")
+}
+
+const now = {tz : byte[:]
+ var tm
+
+ tm = std.now()
+ -> mkinstant(tm, tz)
+}
+
+const tozone = {d, tz
+ -> mkinstant(d.actual, tz)
+}
+
+const mkdate = {y, m, d, tz
+ -> mkinstant(recalc(&[.year=y, .mon=m, .day=d]), tz)
+}
+
+const mkdatetime = {year, mon, day, h, m, s, tz
+ -> mkinstant(recalc(&[
+ .year=year, .mon=mon, .day=day,
+ .h=h, .m=m, .s=s
+ ]), tz)
+}
+
+const mkinstant = {tm, tz
+ var j, y, m, d
+ var t, e
+ var inst
+
+ inst.actual = tm
+ /* time zones */
+ std.assert(tz.len <= inst._tzbuf.len, "time zone name too long\n")
+ match _zoneinfo.findtzoff(tz, tm)
+ | `std.Some o: inst.tzoff = o
+ | `std.None: std.fatal("no zone named {}\n", tz)
+ ;;
+ std.slcp(inst._tzbuf[:tz.len], tz)
+ inst.tzname = inst._tzbuf[:tz.len]
+ tm += inst.tzoff castto(std.time)
+
+ /* break up time */
+ t = tm % DayUsec /* time */
+ e = tm / DayUsec /* epoch day */
+
+ if t < 0
+ t += DayUsec
+ e -= 1
+ ;;
+
+ /* microseconds, seconds, minutes, hours */
+ inst.us = (t % 1_000_000) castto(int)
+ t /= 1_000_000
+ inst.s = (t % 60) castto(int)
+ t /= 60
+ inst.m = (t % 60) castto(int)
+ t /= 60
+ inst.h = t castto(int)
+
+ /* weekday */
+ inst.wday = ((e + 4) % 7) castto(int) /* the world started on Thursday */
+
+ /*
+ split up year, month, day.
+
+ Implemented according to "Algorithm 199, conversions between calendar
+ date and Julian day number", Robert G. Tantzen, Air Force Missile Development
+ Center, Holloman AFB, New Mex.
+
+ Lots of magic. Yer a wizard, 'arry.
+ */
+ j = (tm + 2440588 * DayUsec) / DayUsec
+ j -= 1721119
+
+ y = (4 * j - 1) / Days400y
+ j = 4 * j - 1 - Days400y * y
+ d = j / 4
+
+ j = (4 * d + 3) / Days4y
+ d = 4 * d + 3 - Days4y * j
+ d = (d + 4) / 4 ;
+
+ m = (5 * d - 3) / 153
+ d = 5 * d - 3 - 153 * m
+ d = (d + 5) / 5
+
+ y = 100 * y + j
+ if m < 10
+ m += 3
+ else
+ m -= 9
+ y++
+ ;;
+
+ /* there's no year 0 */
+ if y <= 0
+ y--
+ ;;
+ /* and if j negative, the day and month are also negative */
+ if m < 0
+ m += 12
+ ;;
+ if d < 0
+ d += Mdays[m - 1]
+ ;;
+
+ inst.year = y castto(int)
+ inst.mon = m castto(int)
+ inst.day = d castto(int)
+ -> inst
+}
+
+const localoff = {tm
+ -> tzoff("local", tm)
+}
+
+const tzoff = {tz, tm
+ match _zoneinfo.findtzoff(tz, tm)
+ | `std.Some dt: -> dt
+ | `std.None: std.fatal("unable to load zoneinfo\n")
+ ;;
+}
+
+const isleap = {d
+ -> d.year % 4 == 0 && (d.year % 100 != 0 || d.year % 400 == 0)
+}
+
+const add = {d, dt
+ -> mkinstant(d.actual + (dt castto(std.time)), d.tzname)
+}
+
+const sub = {d, dt
+ -> mkinstant(d.actual - (dt castto(std.time)), d.tzname)
+}
+
+const addperiod = {inst, p
+ match p
+ | `Year y: inst.year += y
+ | `Month m: inst.mon += m
+ | `Day d: inst.day += d
+ | `Hour h: inst.h += h
+ | `Minute m: inst.m += m
+ | `Second s: inst.s += s
+ ;;
+ -> mkinstant(recalc(&inst), inst.tzname)
+}
+
+const subperiod = {inst, p
+ match p
+ | `Year y: inst.year -= y
+ | `Month m: inst.mon -= m
+ | `Day d: inst.day -= d
+ | `Hour h: inst.h -= h
+ | `Minute m: inst.m -= m
+ | `Second s: inst.s -= s
+ ;;
+ -> mkinstant(recalc(&inst), inst.tzname)
+}
+
+const duration = {a, b
+ -> (b.actual - a.actual) castto(duration)
+}
+
+const recalc = {inst
+ var c, ya, j, tm
+ var y, m, d
+
+
+ if inst.mon > 2
+ m = (inst.mon - 3) castto(std.time)
+ y = inst.year castto(std.time)
+ else
+ m = (inst.mon + 9) castto(std.time)
+ y = (inst.year - 1) castto(std.time)
+ ;;
+ d = inst.day castto(std.time)
+
+ c = y / 100
+ ya = y - 100 * c
+ j = c * Days400y / 4 + \
+ Days4y * ya / 4 + \
+ (153 * m + 2)/5 + d - \
+ 719469
+ tm = j * DayUsec
+ tm += (inst.h castto(std.time)) * 3600*1_000_000
+ tm += (inst.m castto(std.time)) * 60*1_000_000
+ tm += (inst.s castto(std.time)) * 1_000_000
+ tm += (inst.us castto(std.time))
+ -> tm
+}
+
--- /dev/null
+++ b/lib/date/fmt.myr
@@ -1,0 +1,109 @@
+use std
+
+use "types.use"
+use "names.use"
+
+pkg date =
+ const sbfmt : (sb : std.strbuf#, args : std.valist#, opt : (byte[:], byte[:])[:] -> void)
+;;
+
+const __init__ = {
+ var d : instant
+
+ std.fmtinstall(std.typeof(d), sbfmt, [
+ ("D", false),
+ ("d", false),
+ ("t", false),
+ ("f", true),
+ ][:])
+}
+
+/* Always formats in proleptic Gregorian format */
+const sbfmt = {sb, ap, opts
+ var d : instant
+ var fmt
+
+ d = std.vanext(ap)
+ fmt = Datetimefmt;
+ for o in opts
+ match o
+ | ("d", ""): fmt = Datefmt
+ | ("t", ""): fmt = Timefmt
+ | ("D", ""): fmt = Datetimefmt
+ | ("f", opt): fmt = opt
+ | _: std.fatal("unknown option")
+ ;;
+ ;;
+ datefmt(sb, fmt, d)
+}
+
+const datefmt = {sb, fmt, d
+ var c
+ while fmt.len != 0
+ (c, fmt) = std.striter(fmt)
+ if c == '%'
+ (c, fmt) = std.striter(fmt)
+ match c
+ | 'a': std.sbfmt(sb, "{}", _names.abbrevday[d.day])
+ | 'A': std.sbfmt(sb, "{}", _names.fullday[d.day])
+ | 'b': std.sbfmt(sb, "{}", _names.abbrevmon[d.mon])
+ | 'B': std.sbfmt(sb, "{}", _names.fullmon[d.mon])
+ | 'c': datefmt(sb, "%Y-%m-%d %H:%M:%S %z", d)
+ | 'C': std.sbfmt(sb, "{p=0,w=2}", d.year % 100)
+ | 'd': std.sbfmt(sb, "{p=0,w=2}", d.day)
+ | 'D': datefmt(sb, "%m/%d/%y (wtf america)", d)
+ | 'e': std.sbfmt(sb, "{w=2}", d.day)
+ | 'F': datefmt(sb, "%Y-%m-%d", d)
+ /*
+ | 'G': s = std.sbfmt(sb, ...?
+ | 'g':
+ */
+ | 'h': std.sbfmt(sb, "{}", _names.abbrevmon[d.mon])
+ | 'H': std.sbfmt(sb, "{p=0,w=2}", d.h)
+ | 'I': std.sbfmt(sb, "{p=0,w=2}", d.h % 12)
+ | 'j': std.sbfmt(sb, "year day... unimplemented.")
+ | 'k': std.sbfmt(sb, "{}", d.h)
+ | 'l': std.sbfmt(sb, "{}", d.h % 12)
+ | 'm': std.sbfmt(sb, "{}", d.mon)
+ | 'M': std.sbfmt(sb, "{p=0,w=2}", d.m)
+ | 'n': std.sbfmt(sb, "\n")
+ | 'O': std.sbfmt(sb, "unsupported %O")
+ | 'p': std.sbfmt(sb, "{}", ["AM", "PM"][d.h/12])
+ | 'P': std.sbfmt(sb, "{}", ["am", "pm"][d.h/12])
+ | 'r': datefmt(sb, "%I:%M:%S %P", d)
+ | 'R': datefmt(sb, "%H:%M", d)
+ | 's': std.sbfmt(sb, "{}", d.actual / 1_000_000)
+ | 'S': std.sbfmt(sb, "{p=0,w=2}", d.s)
+ | 'T': datefmt(sb, "%H:%M:%S", d)
+ | 't': std.sbfmt(sb, "\t")
+ | 'u': std.sbfmt(sb, "{}", d.wday)
+ | 'U': std.sbfmt(sb, "week number... unimplemented.")
+ | 'x': datefmt(sb, Datefmt, d)
+ | 'X': datefmt(sb, Timefmt, d)
+ | 'y': std.sbfmt(sb, "{}", d.year % 100)
+ | 'Y': std.sbfmt(sb, "{}", d.year)
+ | 'z': timezone(sb, d.tzoff)
+ | 'Z': std.sbfmt(sb, "{}", d.tzname)
+ | '%': std.sbfmt(sb, "%")
+ | _: std.fatal("unknown format character {}\n", c)
+ ;;
+ else
+ std.sbfmt(sb, "{}", c)
+ ;;
+ ;;
+}
+
+const timezone = {sb, off
+ var h, m
+ var sep
+
+ sep = "+"
+ if off < 0
+ off = -off
+ sep = "-"
+ ;;
+ off /= 1_000_000
+ h = off / 3600
+ m = off % 3600
+ -> std.sbfmt(sb, "{}{p=0,w=2}{p=0,w=2}", sep, h, m)
+}
--- /dev/null
+++ b/lib/date/names.myr
@@ -1,0 +1,59 @@
+pkg _names =
+ const abbrevday
+ const fullday
+ const abbrevmon
+ const fullmon
+;;
+
+const abbrevday = [
+ "Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat"
+]
+
+const fullday = [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday"
+]
+
+const abbrevmon = [
+ "NONE",
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+]
+
+const fullmon = [
+ "NONE",
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+]
+
--- /dev/null
+++ b/lib/date/parse.myr
@@ -1,0 +1,286 @@
+use std
+
+use "types.use"
+use "names.use"
+use "date.use"
+use "zoneinfo.use"
+
+pkg date =
+ type parsefail = union
+ `Doublefmt char
+ `Badsep (char, char)
+ `Badfmt char
+ `Badzone byte[:]
+ `Badname byte[:]
+ `Badchar
+ `Badampm
+ `Shortint
+ `Badint
+ ;;
+
+ /* date i/o */
+ const parsefmt : (f : byte[:], s: byte[:] -> std.result(instant, parsefail))
+ const parsefmtl : (f : byte[:], s: byte[:] -> std.result(instant, parsefail))
+ const parsefmtz : (f : byte[:], s: byte[:], tz : byte[:] -> std.result(instant, parsefail))
+;;
+
+const UnixJulianDiff = 719468
+
+const parsefmt = {f, s; -> strparse(f, s, "", false)}
+const parsefmtl = {f, s; -> strparse(f, s, "local", true)}
+const parsefmtz = {f, s, tz; -> strparse(f, s, tz, true)}
+
+type parsedtz = union
+ `None
+ `Off duration
+ `Name byte[:]
+;;
+
+const __init__ = {
+ var fail : parsefail
+ std.fmtinstall(std.typeof(fail), failfmt, [][:])
+}
+
+const strparse = {f, s, tz, replace
+ var d, err
+ var seen
+
+ d = [.year = 0]
+ err = `std.None
+ seen = std.mkbs()
+ s = filldate(&d, f, s, seen, &err)
+ std.bsfree(seen)
+
+ d.actual -= d.tzoff castto(std.time)
+ match err
+ | `std.Some e: -> `std.Fail e
+ | `std.None: /* no error, we're ok */
+ ;;
+
+ if replace
+ d = mkinstant(d.actual, tz)
+ ;;
+
+ -> `std.Ok d
+}
+
+const filldate = {d, f, s, seen, err
+ var fc, sc, z, am
+
+ z = ""
+ am = `std.None
+ while f.len != 0
+ (fc, f) = std.striter(f)
+ if fc == '%'
+ (fc, f) = std.striter(f)
+ if std.bshas(seen, fc)
+ err# = `std.Some `Doublefmt fc
+ -> s
+ ;;
+ std.bsput(seen, fc)
+ match fc
+ /* named things */
+ | 'a': s = indexof(&d.day, s, _names.abbrevday, err)
+ | 'A': s = indexof(&d.day, s, _names.fullday, err)
+ | 'b': s = indexof(&d.mon, s, _names.abbrevmon, err)
+ | 'B': s = indexof(&d.mon, s, _names.fullmon, err)
+ | 'c': s = filldate(d, "%Y-%m-%d", s, seen, err)
+ | 'C':
+ s = intval(&d.year, s, 2, 2, err)
+ d.year += 1900
+ | 'd': s = intval(&d.day, s, 2, 2, err)
+ | 'D': s = filldate(d, "%m/%d/%y", s, seen, err)
+ | 'e': s = intval(&d.day, s, 1, 2, err)
+ | 'F': s = filldate(d, "%y-%m-%d", s, seen, err)
+ | 'h': s = indexof(&d.day, s, _names.abbrevmon, err)
+ | 'H': s = intval(&d.h, s, 1, 2, err)
+ | 'I': s = intval(&d.h, s, 1, 2, err)
+ | 'k': s = intval(&d.h, s, 1, 2, err)
+ | 'l': s = intval(&d.h, s, 1, 2, err)
+ | 'm': s = intval(&d.mon, s, 1, 2, err)
+ | 'M': s = intval(&d.m, s, 1, 2, err)
+ | 'n': s = matchstr(s, "\n", err)
+ | 'p': s = matchampm(d, s, &am, err)
+ | 'P': s = matchampm(d, s, &am, err)
+ | 'r': s = filldate(d, "%H:%M:%S %P", s, seen, err)
+ | 'R': s = filldate(d, "%H:%M %P", s, seen, err)
+ | 's': s = intval(&d.actual, s, 1, 64, err)
+ | 'S': s = intval(&d.s, s, 1, 2, err)
+ | 't': s = eatspace(s)
+ | 'u': s = intval(&d.wday, s, 1, 1, err)
+ | 'x': s = filldate(d, Datefmt, s, seen, err)
+ | 'X': s = filldate(d, Timefmt, s, seen, err)
+ | 'y': s = intval(&d.year, s, 1, 2, err)
+ d.year += 1900
+ | 'Y':
+ s = intval(&d.year, s, 1, 4, err)
+ | 'z': s = tzoffset(&d.tzoff, s, err)
+ | 'Z': (s, z) = tzstring(d, s, err)
+ | '%': s = matchstr(s, "%", err)
+ | _: std.fatal("unknown format character %c\n", fc)
+ ;;
+ else
+ (sc, s) = std.striter(s)
+ if std.isspace(fc) && std.isspace(sc)
+ s = eatspace(s)
+ elif sc != fc
+ err# = `std.Some `Badsep (fc, sc)
+ -> s
+ ;;
+ ;;
+ match err#
+ | `std.Some _: -> s
+ | `std.None:
+ ;;
+ ;;
+ d.actual = recalc(d)
+ if z.len > 0
+ match _zoneinfo.findtzoff(z, d.actual)
+ | `std.Some o: d.tzoff = o
+ | `std.None: err# = `std.Some `Badzone z
+ ;;
+ ;;
+
+ match am
+ | `std.None:
+ | `std.Some true:
+ d.h %= 12
+ | `std.Some false:
+ d.h %= 12
+ d.h += 12
+
+ ;;
+ -> s
+}
+
+const eatspace = {s
+ var c
+
+ while std.isspace(std.decode(s))
+ (c, s) = std.striter(s)
+ ;;
+ -> s
+}
+
+const indexof = {dst, s, set, err
+ for var i = 0; i < set.len; i++
+ if s.len >= set[i].len && std.streq(s, set[i])
+ dst# = i
+ -> s
+ ;;
+ ;;
+ err# = `std.Some `Badname s
+ dst# = 0
+ -> s
+}
+
+const tzoffset = {dst, s, err
+ var sgn
+ var tzoff
+
+ if s.len < 1
+ err# = `std.Some `Badzone s
+ -> ""
+ ;;
+ if std.sleq(s[:1], "-")
+ sgn = -1
+ elif std.sleq(s[:1], "+")
+ sgn = 1
+ else
+ err# = `std.Some `Badzone s
+ -> s
+ ;;
+ s = intval(&tzoff, s[1:], 2, 4, err)
+ dst# = sgn * (tzoff / 100) * 3600 * 1_000_000 + (tzoff % 100) * 60 * 1_000_000
+ -> s
+}
+
+const tzstring = {d, s, err
+ var c, n
+
+ while true
+ c = std.decode(s[n:])
+ if c != '/' && !std.isalnum(c)
+ break
+ ;;
+ n += std.charlen(c)
+ ;;
+ if n < d._tzbuf.len
+ std.slcp(d._tzbuf[:n], s[:n])
+ else
+ err# = `std.Some `Badzone s[:n]
+ ;;
+ -> (s[n:], s[:n])
+}
+
+
+const matchstr = {s, str, err
+ if s.len <= str.len || !std.sleq(s[:str.len], str)
+ err# = `std.Some `Badchar
+ -> s
+ ;;
+ -> s[str.len:]
+}
+
+const matchampm = {d, s, am, err
+ if s.len < 2
+ err# = `std.Some `Badampm
+ -> s
+ ;;
+ if std.sleq(s[:2], "am") || std.sleq(s[:2], "AM")
+ am# = `std.Some true
+ -> s[2:]
+ elif std.sleq(s[:2], "pm") || std.sleq(s[:2], "PM")
+ am# = `std.Some false
+ -> s[2:]
+ else
+ err# = `std.Some `Badampm
+ -> s
+ ;;
+}
+generic intval = {dst : @a::(numeric,integral)#, s : byte[:], \
+ min : @a::(numeric,integral), max : @a::(numeric,integral), \
+ err : std.option(parsefail)# -> byte[:]
+ var i, c, num
+
+ num = s
+ for i = 0; i < min; i++
+ (c, s) = std.striter(s)
+ if !std.isdigit(c)
+ err# = `std.Some `Shortint
+ -> s
+ ;;
+ ;;
+
+ for i = min ; i < max; i++
+ c = std.decode(s)
+ if !std.isdigit(c)
+ break
+ ;;
+ s = s[std.charlen(c):]
+ ;;
+
+ num = num[:i]
+ match std.intparse(num)
+ | `std.Some v:
+ dst# = v
+ -> s
+ | `std.None:
+ err# = `std.Some `Badint
+ -> s
+ ;;
+}
+
+const failfmt = {sb, ap, opt
+ match std.vanext(ap)
+ | `Doublefmt chr: std.sbfmt(sb, "saw duplicate format char '{}'", chr)
+ | `Badsep (e, f): std.sbfmt(sb, "expected separator '{}', found '{}'", e, f)
+ | `Badfmt chr: std.sbfmt(sb, "invalid format character '{}'", chr)
+ | `Badzone zone: std.sbfmt(sb, "unknown time zone '{}'", zone)
+ | `Badname name: std.sbfmt(sb, "could not find name '{}'", name)
+ | `Badchar: std.sbfmt(sb, "unexpected character in parsed string")
+ | `Badampm: std.sbfmt(sb, "invalid am/pm specifier")
+ | `Shortint: std.sbfmt(sb, "integer had too few characters")
+ | `Badint: std.sbfmt(sb, "integer could not be parsed")
+ ;;
+}
--- /dev/null
+++ b/lib/date/test/fmt.myr
@@ -1,0 +1,80 @@
+use std
+use date
+
+const main = {
+ var buf : byte[1024]
+ var d, s
+
+ /* epoch */
+ d = date.mkinstant(0, "")
+ eq("1970-1-01 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+ d = date.mkinstant(24*3600*1_000_000, "")
+ eq("1970-1-02 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* epoch + 12 hours */
+ d = date.mkinstant(12*3600*1_000_000, "")
+ eq("1970-1-01 12:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* epoch - 6 hours */
+ d = date.mkinstant(-6*3600*1_000_000, "")
+ eq("1969-12-31 18:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* epoch - 12 hours */
+ d = date.mkinstant(-12*3600*1_000_000, "")
+ eq("1969-12-31 12:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* more or less random: Fri 29 Aug 2014 07:47:43 PM UTC*/
+ d = date.mkinstant(1_409_341_663*1_000_000, "")
+ eq("2014-8-29 19:47:43 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* large negative time stamp */
+ d = date.mkinstant(-50000000000*1_000_000, "")
+ eq("385-7-25 07:06:40 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* date in the bc */
+ d = date.mkinstant(-70000000000*1_000_000, "")
+ eq("-249-11-19 19:33:20 +0000", std.bfmt(buf[:], "{D}", d))
+
+ /* test addition and subtraction of dates */
+ d = date.mkinstant(-1, "")
+ d = date.addperiod(d, `date.Hour 1)
+ eq("1970-1-01 00:59:59 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.mkinstant(0, "")
+ d = date.addperiod(d, `date.Hour 24)
+ eq("1970-1-02 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.mkinstant(0, "")
+ d = date.addperiod(d, `date.Day 1)
+ eq("1970-1-02 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.subperiod(d, `date.Day 1)
+ eq("1970-1-01 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.subperiod(d, `date.Year 1)
+ eq("1969-1-01 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.addperiod(d, `date.Day 365)
+ eq("1970-1-01 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.addperiod(d, `date.Year 2)
+ eq("1972-1-01 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+ d = date.addperiod(d, `date.Day 365)
+ eq("1972-12-31 00:00:00 +0000", std.bfmt(buf[:], "{D}", d))
+
+ d = date.mkinstant(12*3601*1_000_000, "")
+ for var i = 0; i < 50; i++
+ d = date.addperiod(d, `date.Day 1)
+ d = date.addperiod(d, `date.Second 1)
+ s = std.fmt("1970-{}-{p=0,w=2} 12:{p=0,w=2}:{p=0,w=2} +0000", \
+ (i+1)/31 + 1, (i+1)%31+1, (i+13)/60, (i+13)%60)
+ eq(s, std.bfmt(buf[:], "{D}", d))
+ std.slfree(s)
+ ;;
+}
+
+const eq = {expected, actual
+ if !std.sleq(expected, actual)
+ std.fatal("expected date {}, got {}\n", expected, actual)
+ ;;
+}
--- /dev/null
+++ b/lib/date/test/parse.myr
@@ -1,0 +1,46 @@
+use std
+use date
+
+const main = {
+ var buf : byte[1024]
+
+ /* should be unix epoch */
+ match date.parsefmt("%Y-%m-%d %H:%M:%S %z", "1969-12-31 16:00:00 -0800")
+ | `std.Ok d:
+ std.assert(d.actual == 0, "got wrong date")
+ eq(std.bfmt(buf[:], "{D}", d), "1969-12-31 16:00:00 -0800")
+ | `std.Fail m:
+ std.fatal("failed to parse date: {}\n", m)
+ ;;
+
+ match date.parsefmt("%Y-%m-%d", "1969 12 31")
+ | `std.Ok d:
+ std.fatal("should have errored")
+ | `std.Fail m:
+ eq(std.bfmt(buf[:], "{}", m), "expected separator '-', found ' '")
+ ;;
+
+ /* parse into utc */
+ match date.parsefmtz("%Y-%m-%d %H:%M:%S %z", "1969-12-31 16:00:00 -0800", "")
+ | `std.Ok d:
+ std.assert(d.actual == 0, "got wrong date")
+ eq(std.bfmt(buf[:], "{D}", d), "1970-1-01 00:00:00 +0000")
+ | `std.Fail m:
+ std.fatal("failed to parse date: {}\n", m)
+ ;;
+
+ /*Fri 29 Aug 2014 07:47:43 PM UTC*/
+ match date.parsefmt("%Y-%m-%d %z", "1932-10-23 +0500")
+ | `std.Ok d:
+ std.assert(d.actual == -1173675600 * 1_000_000, "wrong timestamp")
+ eq(std.bfmt(buf[:], "{D}", d), "1932-10-23 00:00:00 +0500")
+ | `std.Fail m:
+ std.fatal("Failed to parse date: {}", m)
+ ;;
+}
+
+const eq = {actual, expected
+ if !std.sleq(expected, actual)
+ std.fatal("expected date {}, got {}\n", expected, actual)
+ ;;
+}
--- /dev/null
+++ b/lib/date/types.myr
@@ -1,0 +1,36 @@
+use std
+
+pkg date =
+ type instant = struct
+ actual : std.time /* epoch time in microseconds */
+ tzoff : duration /* timezone offset in microseconds */
+ year : int /* year, starting at 0 (ie, 1 BCE) */
+ mon : int /* month, [1..12] */
+ day : int /* day, [1..31] */
+ wday : int /* weekday, [0..6] */
+ h : int /* hour: [0..23] */
+ m : int /* minute: [0..59] */
+ s : int /* second: [0..59] */
+ us : int /* microsecond: [0..999,999] */
+ tzname : byte[:] /* current time zone name */
+ _tzbuf : byte[32] /* current time zone name storage */
+ ;;
+
+ type duration = std.time
+
+ type period = union
+ `Year int
+ `Month int
+ `Day int
+ `Hour int
+ `Minute int
+ `Second int
+ ;;
+ const Datetimefmt
+ const Timefmt
+ const Datefmt
+
+;;
+const Datetimefmt = "%Y-%m-%d %H:%M:%S %z"
+const Timefmt = "%h:%m:{} %z"
+const Datefmt = "%Y-%m-%d %z"
--- /dev/null
+++ b/lib/date/zoneinfo+posixy.myr
@@ -1,0 +1,178 @@
+use std
+use sys
+
+use "types.use"
+
+pkg _zoneinfo =
+ type zifile
+ const findtzoff : (tz : byte[:], tm : std.time -> std.option(date.duration))
+ const load : (file : byte[:] -> zifile#)
+ const free : (f : zifile# -> void)
+;;
+
+type zifile = struct
+ time : int32[:]
+ timetype: byte[:]
+ ttinfo : ttinfo[:]
+ abbrev : byte[:]
+ leap : int32[2][:]
+ isstd : byte[:]
+ isgmt : byte[:]
+;;
+
+type ttinfo = struct
+ gmtoff : int32
+ isdst : byte
+ abbrind : byte
+;;
+
+const zonepath = [
+ "/usr/share/zoneinfo",
+ "/share/zoneinfo",
+ "/etc/zoneinfo"
+]
+
+const findtzoff = {tz, tm -> std.option(date.duration)
+ var path
+ var zone
+ var cur
+ var sb
+ var ds
+ var i
+
+ /* load zone */
+ if std.sleq(tz, "") || std.sleq(tz, "UTC")
+ -> `std.Some 0
+ elif std.sleq(tz, "local")
+ path = std.sldup("/etc/localtime")
+ else
+ for z in zonepath
+ path = std.pathcat(z, tz)
+ if sys.stat(path, &sb) == 0
+ goto found
+ ;;
+ std.slfree(path)
+ ;;
+ std.slfree(path)
+ -> `std.None
+ ;;
+:found
+ zone = load(path)
+ std.slfree(path)
+
+ /* find applicable gmt offset */
+ cur = (tm / 1_000_000) castto(int32)
+ if zone.time.len == 0
+ -> `std.None
+ ;;
+ for i = 0; i < zone.time.len && cur < zone.time[i]; i++
+ /* nothing */
+ ;;
+ ds = zone.ttinfo[zone.timetype[i]].gmtoff
+ free(zone)
+ -> `std.Some (ds castto(date.duration)) * 1_000_000
+}
+
+const load = {file
+ var nisgmt, nisstd, nleap, ntime, ntype, nchar
+ var i, f, p
+
+ /* check magic */
+ match std.slurp(file)
+ | `std.Ok d: p = d
+ | `std.Fail m:
+ -> std.zalloc()
+ ;;
+
+ if !std.sleq(p[:4], "TZif\0")
+ std.put("%s is not a zone info file\n", file)
+ -> std.zalloc()
+ ;;
+
+ /* skip to data */
+ p = p[20:]
+ (nisgmt, p) = fetchbe32(p)
+ (nisstd, p) = fetchbe32(p)
+ (nleap, p) = fetchbe32(p)
+ (ntime, p) = fetchbe32(p)
+ (ntype, p) = fetchbe32(p)
+ (nchar, p) = fetchbe32(p)
+
+
+ f = std.alloc()
+ f.time = std.slalloc(ntime castto(std.size))
+ for i = 0; i < ntime; i++
+ (f.time[i], p) = fetchbe32(p)
+ ;;
+
+ f.timetype = std.slalloc(ntime castto(std.size))
+ for i = 0; i < ntime; i++
+ (f.timetype[i], p) = fetchbe8(p)
+ ;;
+
+ f.ttinfo = std.slalloc(ntype castto(std.size))
+ for i = 0; i < ntype; i++
+ p = fetchttinfo(p, &f.ttinfo[i])
+ ;;
+
+ f.abbrev = std.slalloc(nchar castto(std.size))
+ for i = 0; i < nchar; i++
+ (f.abbrev[i], p) = fetchbe8(p)
+ ;;
+
+ f.leap = std.slalloc(nleap castto(std.size))
+ for i = 0; i < nleap; i++
+ (f.leap[i][0], p) = fetchbe32(p)
+ (f.leap[i][1], p) = fetchbe32(p)
+ ;;
+
+ f.isstd = std.slalloc(nisstd castto(std.size))
+ for i = 0; i < nisstd; i++
+ (f.isstd[i], p) = fetchbe8(p)
+ ;;
+
+ f.isgmt = std.slalloc(nisgmt castto(std.size))
+ for i = 0; i < nisgmt; i++
+ (f.isgmt[i], p) = fetchbe8(p)
+ ;;
+
+ -> f
+}
+
+const free = {zi
+ std.slfree(zi.time)
+ std.slfree(zi.timetype)
+ std.slfree(zi.ttinfo)
+ std.slfree(zi.abbrev)
+ std.slfree(zi.leap)
+ std.slfree(zi.isstd)
+ std.slfree(zi.isgmt)
+ std.free(zi)
+}
+
+const fetchbe32 = {sl
+ var v
+
+ std.assert(sl.len >= 4, "Slice too small to fetch int32 from")
+ v = (sl[0] castto(int32)) << 24 | \
+ (sl[1] castto(int32)) << 16 | \
+ (sl[2] castto(int32)) << 8 | \
+ (sl[3] castto(int32)) << 0
+ -> (v, sl[4:])
+}
+
+const fetchbe8 = {sl
+ var v
+
+ std.assert(sl.len >= 1, "Slice too small to fetch int8 from")
+ v = sl[0]
+ -> (v, sl[1:])
+}
+
+
+const fetchttinfo = {sl, dst : ttinfo#
+ (dst.gmtoff, sl) = fetchbe32(sl)
+ (dst.isdst, sl) = fetchbe8(sl)
+ (dst.abbrind, sl) = fetchbe8(sl)
+ -> sl
+}