shithub: mc

Download patch

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