~sircmpwn/helios-devel

This patchset contain two patchs related to time::chrono and time::date.

The first one is a plain rsync from the Hare stdlib, and the second one
is usefull to list what I stripped for them to works in Mercury.
#1037500 .build.yml failed
mercury/patches/.build.yml: FAILED in 1m7s

[rtc: new driver][0] v2 from [Willow Barraco][1]

[0]: https://lists.sr.ht/~sircmpwn/helios-devel/patches/43430
[1]: mailto:contact@willowbarraco.fr

✗ #1037500 FAILED mercury/patches/.build.yml https://builds.sr.ht/~sircmpwn/job/1037500
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/helios-devel/patches/43430/mbox | git am -3
Learn more about email & git

[PATCH mercury v2 1/3] rtc: new driver Export this patch

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 Makefile              |  16 +++
 cmd/rtc/main.ha       | 318 ++++++++++++++++++++++++++++++++++++++++++
 cmd/rtc/manifest.ini  |  10 ++
 cmd/time/Makefile     |   8 ++
 cmd/time/main.ha      |  34 +++++
 dev/clock_gen.ha      |  39 ++++++
 etc/driver.d/00-rtc   |   1 +
 proto/Makefile        |  13 ++
 proto/dev/clock.ipc   |   9 ++
 serv/dev/clock_gen.ha |  69 +++++++++
 10 files changed, 517 insertions(+)
 create mode 100644 cmd/rtc/main.ha
 create mode 100644 cmd/rtc/manifest.ini
 create mode 100644 cmd/time/Makefile
 create mode 100644 cmd/time/main.ha
 create mode 100644 dev/clock_gen.ha
 create mode 120000 etc/driver.d/00-rtc
 create mode 100644 proto/dev/clock.ipc
 create mode 100644 serv/dev/clock_gen.ha

diff --git a/Makefile b/Makefile
index 6abfc67..2a9deb4 100644
--- a/Makefile
+++ b/Makefile
@@ -58,6 +58,21 @@ sbin/drv/serial: protos cmd/serial/manifest.o
bootstrap.tar: sbin/drv/serial
.PHONY: sbin/drv/serial

cmd/rtc/manifest.o: cmd/rtc/manifest.ini
	objcopy -S -Ibinary -Oelf64-x86-64 \
		--rename-section .data=.manifest,contents \
		$< $@
sbin/drv/rtc: protos cmd/rtc/manifest.o
	mkdir -p sbin/drv
	# XXX: This is a bit messy
	LDLINKFLAGS=cmd/rtc/manifest.o \
	HAREPATH=. \
		$(HARE) build -t$(HAREARCH) \
		-X^ -T+$(HAREARCH) \
		-o sbin/drv/rtc cmd/rtc/
bootstrap.tar: sbin/drv/rtc
.PHONY: sbin/drv/rtc

cmd/pcibus/manifest.o: cmd/pcibus/manifest.ini
	objcopy -S -Ibinary -Oelf64-x86-64 \
		--rename-section .data=.manifest,contents \
@@ -137,6 +152,7 @@ include cmd/sleep/Makefile
include cmd/testcons/Makefile
include cmd/threads/Makefile
include cmd/writefile/Makefile
include cmd/time/Makefile

bootstrap.tar:
	tar -cvf bootstrap.tar bin etc sbin README.md
diff --git a/cmd/rtc/main.ha b/cmd/rtc/main.ha
new file mode 100644
index 0000000..7027644
--- /dev/null
+++ b/cmd/rtc/main.ha
@@ -0,0 +1,318 @@
use helios;
use helios::{pollcap, pollflags};
use sys;
use serv::dev;
use time;
use time::chrono;
use time::date;

def IOPORT: helios::cap = 0;

type mode = enum {
	BIN,
	BCD,
};

type format = enum {
	TWELVE,
	TWENTYFOUR,
};

type rtc = struct {
	dev::clock,
	initial_date: date::date,
	initial_time: time::instant,
};

type rtccalendar = struct {
	sec: u8,
	min: u8,
	hour: u8,
	day: u8,
	month: u8,
	year: u8,
	century: u8,
	mode: mode,
	format: format,
};

export fn main() void = {
	let ep: helios::cap = 0;
	const registry = helios::service(sys::DEVREGISTRY_ID);
	sys::devregistry_new(registry, dev::CLOCK_ID, &ep);
	helios::destroy(registry)!;

	const rtc = rtc {
		_iface = &rtc_impl,
		_endpoint = ep,
		initial_date = read_date(),
		initial_time = time::now(time::clock::MONOTONIC),
	};

	dev::ready();

	for (true) {
		dev::clock_dispatch(&rtc);
	};
};

fn read_rtc_mode() mode = {
	helios::ioport_out8(IOPORT, 0x70, 0x0B)!;
	const b = helios::ioport_in8(IOPORT, 0x71)!;

	if (b & 4 != 0) {
		return mode::BIN;
	};

	return mode::BCD;
};

fn read_rtc_format() format = {
	helios::ioport_out8(IOPORT, 0x70, 0x0B)!;
	const b = helios::ioport_in8(IOPORT, 0x71)!;

	if (b & 2 != 0) {
		return format::TWENTYFOUR;
	};

	return format::TWELVE;
};

fn read_rtc_calendar() rtccalendar = {
	let calendar = rtccalendar {
		mode = read_rtc_mode(),
		format = read_rtc_format(),
		...
	};

	const nmi = helios::ioport_in8(IOPORT, 0x70)! & 0x80;

	for (true) {
		helios::ioport_out8(IOPORT, 0x70, 0x0A | nmi)!;
		if (helios::ioport_in8(IOPORT, 0x71)! & (1u8 << 7) == 0) {
			break;
		};
	};

	helios::ioport_out8(IOPORT, 0x70, 0x00 | nmi)!;
	calendar.sec = helios::ioport_in8(IOPORT, 0x71)!;

	helios::ioport_out8(IOPORT, 0x70, 0x02 | nmi)!;
	calendar.min = helios::ioport_in8(IOPORT, 0x71)!;

	helios::ioport_out8(IOPORT, 0x70, 0x04 | nmi)!;
	calendar.hour = helios::ioport_in8(IOPORT, 0x71)!;

	helios::ioport_out8(IOPORT, 0x70, 0x07 | nmi)!;
	calendar.day = helios::ioport_in8(IOPORT, 0x71)!;

	helios::ioport_out8(IOPORT, 0x70, 0x08 | nmi)!;
	calendar.month = helios::ioport_in8(IOPORT, 0x71)!;

	helios::ioport_out8(IOPORT, 0x70, 0x09 | nmi)!;
	calendar.year = helios::ioport_in8(IOPORT, 0x71)!;

	helios::ioport_out8(IOPORT, 0x70, 0x32 | nmi)!;
	calendar.century = helios::ioport_in8(IOPORT, 0x71)!;

	return calendar;
};

fn eqcalendar(ca: rtccalendar, cb: rtccalendar) bool = {
	return ca.sec == cb.sec
		&& ca.min == cb.min
		&& ca.hour == cb.hour
		&& ca.day == cb.day
		&& ca.month == cb.month
		&& ca.year == cb.year
		&& ca.century == cb.century;
};

fn bcdtobin(value: u8) u8 = {
	return ((value & 0xF0) >> 1) + ((value & 0xF0) >> 3) + (value & 0xF);
};

// a better way?
fn bintobcd(value: u8) u8 = {
	let new = value: u16;

	for (let i = 0; i < 8; i = i+1) {
		if ((new & 0xF000) >> 12 >= 5) {
			new = new + 0x3000;
		};
		if ((new & 0x0F00) >> 8 >= 5) {
			new = new + 0x300;
		};
		new = new << 1;
	};

	return (new >> 8): u8;
};

// This make sure the rtc is valid, but also convert it to 24h format and binary
// mode
fn read_clean_calendar() rtccalendar = {
	let pcal = read_rtc_calendar();
	let cal = read_rtc_calendar();

	for (!eqcalendar(pcal, cal)) {
		pcal = cal;
		cal = read_rtc_calendar();
	};

	// We mask this before converting to binary
	const pm: u8 = 0;
	if (cal.format == format::TWELVE) {
		pm = cal.hour & 0x80;
		cal.hour = cal.hour & ~0x80;
	};

	if (cal.mode == mode::BCD) {
		cal.sec = bcdtobin(cal.sec);
		cal.min = bcdtobin(cal.min);
		cal.hour = bcdtobin(cal.hour);
		cal.day = bcdtobin(cal.day);
		cal.month = bcdtobin(cal.month);
		cal.year = bcdtobin(cal.year);
		cal.century = bcdtobin(cal.century);
		cal.mode = mode::BIN;
	};

	if (cal.format == format::TWELVE) {
		if (cal.hour == 12) {
			cal.hour = 0;
		};
		if (pm != 0) {
			cal.hour += 12;
		};
		cal.format = format::TWENTYFOUR;
	};

	return cal;
};

fn read_date() date::date = {
	const cal = read_clean_calendar();

	return date::new(
		chrono::LOCAL,
		0,
		cal.century: int * 100 + cal.year: int,
		cal.month: int,
		cal.day: int,
		cal.hour: int,
		cal.min: int,
		cal.sec: int,
	)!;
};

fn write_calendar(rtc: *rtc, cal: rtccalendar) void = {
	let pm = false;

	const format = read_rtc_format();
	const mode = read_rtc_mode();

	if (format == format::TWELVE
		&& cal.format == format::TWENTYFOUR
	) {
		if (cal.hour >= 12) {
			pm = true;
			cal.hour = cal.hour - 12;
		} else if (cal.hour == 0) {
			cal.hour = 12;
		};
	};

	if (mode == mode::BCD
		&& cal.mode == mode::BIN
	) {
		cal.sec = bintobcd(cal.sec);
		cal.min = bintobcd(cal.min);
		cal.hour = bintobcd(cal.hour);
		cal.day = bintobcd(cal.day);
		cal.month = bintobcd(cal.month);
		cal.year = bintobcd(cal.year);
		cal.century = bintobcd(cal.century);
		cal.mode = mode::BCD;
	};

	if (format == format::TWELVE
		&& cal.format == format::TWENTYFOUR
	) {
		if (pm) {
			cal.hour = cal.hour & 0x80;
		};
		cal.format = format::TWELVE;
	};

	const nmi = helios::ioport_in8(IOPORT, 0x70)! & 0x80;

	helios::ioport_out8(IOPORT, 0x70, 0x00 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.sec)!;

	helios::ioport_out8(IOPORT, 0x70, 0x02 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.min)!;

	helios::ioport_out8(IOPORT, 0x70, 0x04 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.hour)!;

	helios::ioport_out8(IOPORT, 0x70, 0x07 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.day)!;

	helios::ioport_out8(IOPORT, 0x70, 0x08 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.month)!;

	helios::ioport_out8(IOPORT, 0x70, 0x09 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.year)!;

	helios::ioport_out8(IOPORT, 0x70, 0x32 | nmi)!;
	helios::ioport_out8(IOPORT, 0x71, cal.century)!;
};

fn write_datetime(rtc: *rtc, dt: date::date) void = {
	const cal = rtccalendar {
		sec = date::second(&dt): u8,
		min = date::minute(&dt): u8,
		hour = date::hour(&dt): u8,
		day = date::day(&dt): u8,
		month = date::month(&dt): u8,
		year = (date::year(&dt) % 100): u8,
		century = (date::year(&dt) / 100): u8,
		mode = mode::BIN,
		format = format::TWENTYFOUR,
	};
	write_calendar(rtc, cal);
};

const rtc_impl = dev::clock_iface {
	get_time = &rtc_get_time,
	set_time = &rtc_set_time,
};

fn rtc_get_time(obj: *dev::clock) u64 = {
	let rtc = obj: *rtc;

	const delta_time = time::diff(
		rtc.initial_time,
		time::now(time::clock::MONOTONIC)
	);

	const new_date = date::from_instant(
		chrono::LOCAL,
		time::add(*(&rtc.initial_date: *time::instant), delta_time)
	);

	return time::unix(*(&new_date: *time::instant)): u64;
};

fn rtc_set_time(obj: *dev::clock, time: u64) void = {
	let rtc = obj: *rtc;

	const instant = time::from_unix(time: i64);
	const dt = date::from_instant(chrono::LOCAL, instant);

	write_datetime(rtc, dt);

	rtc.initial_date = dt;
	rtc.initial_time = time::now(time::clock::MONOTONIC);
};
diff --git a/cmd/rtc/manifest.ini b/cmd/rtc/manifest.ini
new file mode 100644
index 0000000..1db3c8a
--- /dev/null
+++ b/cmd/rtc/manifest.ini
@@ -0,0 +1,10 @@
[driver]
name=rtc
desc=RTC driver for x86_64 PCs

[capabilities]
0:ioport = min=70, max=72
_:cspace = self

[services]
devregistry=
diff --git a/cmd/time/Makefile b/cmd/time/Makefile
new file mode 100644
index 0000000..e263454
--- /dev/null
+++ b/cmd/time/Makefile
@@ -0,0 +1,8 @@
bin/time: protos
	mkdir -p bin
	HAREPATH=. \
		$(HARE) build -t$(HAREARCH) \
		-X^ -T+$(HAREARCH) \
		-o bin/time cmd/time/
bootstrap.tar: bin/time
.PHONY: bin/time
diff --git a/cmd/time/main.ha b/cmd/time/main.ha
new file mode 100644
index 0000000..2f57416
--- /dev/null
+++ b/cmd/time/main.ha
@@ -0,0 +1,34 @@
use errors;
use fmt;
use helios;
use sys;
use dev;
use strconv;
use os;

export fn main() void = {
	const devmgr = helios::service(sys::DEVMGR_ID);

	let rtc: helios::cap = 0;
	match (sys::devmgr_open(devmgr, dev::CLOCK_ID, 0, &rtc)) {
		case void =>
		yield;
	case errors::noentry =>
		fmt::println("No such device")!;
		return;
	};

	if (1 == len(os::args)) {
		const unix_epoch = dev::clock_get_time(rtc);
		fmt::printfln("{}", unix_epoch)!;
	} else if (2 == len(os::args)) {
		const time = match(strconv::stoi64(os::args[1])) {
		case let time: i64 =>
			yield time: u64;
		case =>
			return;
		};

		dev::clock_set_time(rtc, time);
	};
};
diff --git a/dev/clock_gen.ha b/dev/clock_gen.ha
new file mode 100644
index 0000000..7b69aec
--- /dev/null
+++ b/dev/clock_gen.ha
@@ -0,0 +1,39 @@
// This file was generated by ipcgen; do not modify by hand
use helios;
use rt;

// ID for the clock IPC interface.
export def CLOCK_ID: u32 = 0x4C943BE4;

// Labels for operations against clock objects.
export type clock_label = enum u64 {
	GET_TIME = CLOCK_ID << 16u64 | 1,
	SET_TIME = CLOCK_ID << 16u64 | 2,
};

export fn clock_get_time(
	ep: helios::cap,
) u64 = {
	const (tag, a1) = helios::call(ep,
		(clock_label::GET_TIME << 16) | (0 << 8) | 0,
	);
	switch (rt::label(tag)) {
	case 0 =>
		return a1: u64;
	};
};

export fn clock_set_time(
	ep: helios::cap,
	time: u64,
) void = {
	const (tag, a1) = helios::call(ep,
		(clock_label::SET_TIME << 16) | (0 << 8) | 1,
		time: u64,
	);
	switch (rt::label(tag)) {
	case 0 =>
		return a1: void;
	};
};

diff --git a/etc/driver.d/00-rtc b/etc/driver.d/00-rtc
new file mode 120000
index 0000000..28572e0
--- /dev/null
+++ b/etc/driver.d/00-rtc
@@ -0,0 +1 @@
/sbin/drv/rtc
\ No newline at end of file
diff --git a/proto/Makefile b/proto/Makefile
index 1be89c9..ec34c58 100644
--- a/proto/Makefile
+++ b/proto/Makefile
@@ -3,6 +3,7 @@ CLIENT_PROTOS=\
	dev/block_gen.ha \
	dev/console_gen.ha \
	dev/keyboard_gen.ha \
	dev/clock_gen.ha \
	dev/serial_gen.ha \
	fs/fs_gen.ha \
	io/file_gen.ha \
@@ -16,6 +17,7 @@ SERVER_PROTOS=\
	serv/dev/block_gen.ha \
	serv/dev/console_gen.ha \
	serv/dev/keyboard_gen.ha \
	serv/dev/clock_gen.ha \
	serv/dev/serial_gen.ha \
	serv/fs/fs_gen.ha \
	serv/io/file_gen.ha \
@@ -79,6 +81,17 @@ serv/dev/serial_gen.ha: proto/dev/serial.ipc proto/io/file.ipc
	@mkdir -p serv/dev/
	$(IPCGEN) -Iproto/io/file.ipc server < proto/dev/serial.ipc > $@

# dev::clock
PROTO_DEV_CLOCK=dev/clock_gen.ha serv/dev/clock_gen.ha

dev/clock_gen.ha: proto/dev/clock.ipc
	@mkdir -p dev/
	$(IPCGEN) client < proto/dev/clock.ipc > $@

serv/dev/clock_gen.ha: proto/dev/clock.ipc
	@mkdir -p serv/dev/
	$(IPCGEN) server < proto/dev/clock.ipc > $@

# fs::fs
PROTO_FS_FS=fs/fs_gen.ha serv/fs/fs_gen.ha

diff --git a/proto/dev/clock.ipc b/proto/dev/clock.ipc
new file mode 100644
index 0000000..a79fe3a
--- /dev/null
+++ b/proto/dev/clock.ipc
@@ -0,0 +1,9 @@
namespace dev;

interface clock {
	# Return time in seconds.
	call get_time() u64;

	# Set time in seconds.
	call set_time(time: u64) void;
};
diff --git a/serv/dev/clock_gen.ha b/serv/dev/clock_gen.ha
new file mode 100644
index 0000000..5bf918d
--- /dev/null
+++ b/serv/dev/clock_gen.ha
@@ -0,0 +1,69 @@
// This file was generated by ipcgen; do not modify by hand
use errors;
use helios;
use rt;

// ID for the clock IPC interface.
export def CLOCK_ID: u32 = 0x4C943BE4;

// Implementation callback for clock::get_time; see [[clock_iface]].
export type fn_clock_get_time = fn(object: *clock) u64;
// Implementation callback for clock::set_time; see [[clock_iface]].
export type fn_clock_set_time = fn(object: *clock, time: u64) void;

// Implementation of a [[clock]] object.
export type clock_iface = struct {
	get_time: *fn_clock_get_time,
	set_time: *fn_clock_set_time,
};

// Labels for operations against clock objects.
export type clock_label = enum u64 {
	GET_TIME = CLOCK_ID << 16u64 | 1,
	SET_TIME = CLOCK_ID << 16u64 | 2,
};

// Instance of an clock object. Users may subtype this object to add
// instance-specific state.
export type clock = struct {
	_iface: *clock_iface,
	_endpoint: helios::cap,
};

// Dispatches a recv operation for an [[clock]] object
export fn clock_dispatch(
	object: *clock,
) void = {
	rt::ipcbuf.tag = 0 << 8;
	const (tag, a1) = helios::recv(object._endpoint);
	switch (rt::label(tag)) {
	case clock_label::GET_TIME =>
		const rval = object._iface.get_time(
			object,
		);
		match (helios::reply((0 << 16) | (0 << 8) | 0, rval: u64)) {
		case void =>
			yield;
		case errors::invalid_cslot =>
			yield; // callee stored the reply
		case errors::error =>
			abort(); // TODO
		};
	case clock_label::SET_TIME =>
		object._iface.set_time(
			object,
			a1: u64,
		);
		match (helios::reply((0 << 16) | (0 << 8) | 0)) {
		case void =>
			yield;
		case errors::invalid_cslot =>
			yield; // callee stored the reply
		case errors::error =>
			abort(); // TODO
		};
	case =>
		abort(); // TODO
	};
};

-- 
2.41.0

[PATCH mercury v2 2/3] Backport time::date and time::chrono as they are on Hare stdlib Export this patch

Signed-off-by: Willow Barraco <contact@willowbarraco.fr>
---
 time/chrono/README        |  13 +
 time/chrono/arithmetic.ha |  76 +++++
 time/chrono/chronology.ha | 159 ++++++++++
 time/chrono/error.ha      |  44 +++
 time/chrono/leapsec.ha    |  89 ++++++
 time/chrono/timescale.ha  | 390 +++++++++++++++++++++++++
 time/chrono/timezone.ha   | 350 ++++++++++++++++++++++
 time/chrono/tzdb.ha       | 318 ++++++++++++++++++++
 time/date/README          |  20 ++
 time/date/date.ha         | 285 ++++++++++++++++++
 time/date/daydate.ha      | 597 ++++++++++++++++++++++++++++++++++++++
 time/date/daytime.ha      |  32 ++
 time/date/error.ha        |  18 ++
 time/date/format.ha       | 296 +++++++++++++++++++
 time/date/locality.ha     |  13 +
 time/date/observe.ha      | 251 ++++++++++++++++
 time/date/parithm.ha      | 395 +++++++++++++++++++++++++
 time/date/parse.ha        | 499 +++++++++++++++++++++++++++++++
 time/date/period.ha       |  69 +++++
 time/date/reckon.ha       | 483 ++++++++++++++++++++++++++++++
 time/date/tarithm.ha      |  11 +
 time/date/virtual.ha      | 225 ++++++++++++++
 22 files changed, 4633 insertions(+)
 create mode 100644 time/chrono/README
 create mode 100644 time/chrono/arithmetic.ha
 create mode 100644 time/chrono/chronology.ha
 create mode 100644 time/chrono/error.ha
 create mode 100644 time/chrono/leapsec.ha
 create mode 100644 time/chrono/timescale.ha
 create mode 100644 time/chrono/timezone.ha
 create mode 100644 time/chrono/tzdb.ha
 create mode 100644 time/date/README
 create mode 100644 time/date/date.ha
 create mode 100644 time/date/daydate.ha
 create mode 100644 time/date/daytime.ha
 create mode 100644 time/date/error.ha
 create mode 100644 time/date/format.ha
 create mode 100644 time/date/locality.ha
 create mode 100644 time/date/observe.ha
 create mode 100644 time/date/parithm.ha
 create mode 100644 time/date/parse.ha
 create mode 100644 time/date/period.ha
 create mode 100644 time/date/reckon.ha
 create mode 100644 time/date/tarithm.ha
 create mode 100644 time/date/virtual.ha

diff --git a/time/chrono/README b/time/chrono/README
new file mode 100644
index 0000000..12fcdad
--- /dev/null
+++ b/time/chrono/README
@@ -0,0 +1,13 @@
The time::chrono module provides timescale utilities, and the foundations for
chronology with the [[moment]] type, an abstract, extendable date/time object.
For the Gregorian chronology, see the [[time::date]] module.

Hare defines a chronology as a system for naming and ordering moments in time.
In practice, it is the combination of a calendar and wall clock. This module
implements a simple date & time chronology with [[moment]], which observes
certain chronological values according to its [[locality]]. The observer
functions [[daydate]], [[daytime]], and [[mzone]] obtain these values.
Use [[in]] to localize a moment to another [[locality]]; consult [[tz]].

The [[timescale]] interface facilitates leap-second aware [[convert]]ion of
[[time::instant]]s. The [[tai]] timescale is the default intermediary timescale.
diff --git a/time/chrono/arithmetic.ha b/time/chrono/arithmetic.ha
new file mode 100644
index 0000000..47ca1ab
--- /dev/null
+++ b/time/chrono/arithmetic.ha
@@ -0,0 +1,76 @@
// License: MPL-2.0
// (c) 2023 Byron Torres <b@torresjrjr.com>
use time;

// Compares two [[moment]]s. Returns -1 if a precedes b, 0 if a and b are
// simultaneous, or +1 if b precedes a.
//
// The moments are compared as [[time::instant]]s; their observed chronological
// values are ignored.
//
// If the moments' associated [[timescale]]s are different, they will be
// converted to [[tai]] instants first. Any [[discontinuity]] occurence will be
// returned. If a discontinuity against TAI amongst the two timescales exist,
// consider converting such instants manually.
export fn compare(a: *moment, b: *moment) (i8 | discontinuity) = {
	const (ia, ib) = convertpair(a, b)?;
	return time::compare(ia, ib);
};

// Returns true if moments a & b are equivalent; otherwise, returns false.
//
// The moments are compared as [[time::instant]]s; their observed chronological
// values are ignored.
//
// If the moments' associated [[timescale]]s are different, they will be
// converted to [[tai]] instants first. Any [[discontinuity]] occurence will be
// returned. If a discontinuity against TAI amongst the two timescales exist,
// consider converting such instants manually.
export fn eq(a: *moment, b: *moment) (bool | discontinuity) = {
	return 0 == compare(a, b)?;
};

// Returns the [[time::duration]] between two [[moment]]s, from a to b.
//
// The moments are compared as [[time::instant]]s; their observed chronological
// values are ignored.
//
// If the moments' associated [[timescale]]s are different, they will be
// converted to [[tai]] instants first. Any [[discontinuity]] occurence will be
// returned. If a discontinuity against TAI amongst the two timescales exist,
// consider converting such instants manually.
export fn diff(a: *moment, b: *moment) (time::duration | discontinuity) = {
	const (ia, ib) = convertpair(a, b)?;
	return time::diff(ia, ib);
};

// Adds a [[time::duration]] to a [[moment]] with [[time::add]].
export fn add(m: *moment, x: time::duration) moment = {
	return new(m.loc, time::add(*(m: *time::instant), x));
};

fn convertpair(
	a: *moment,
	b: *moment,
) ((time::instant, time::instant) | discontinuity) = {
	let ia = *(a: *time::instant);
	let ib = *(b: *time::instant);

	if (a.loc.timescale != b.loc.timescale) {
		match (convert(ia, a.loc.timescale, &tai)) {
		case let i: time::instant =>
			ia = i;
		case =>
			return discontinuity;
		};

		match (convert(ib, b.loc.timescale, &tai)) {
		case let i: time::instant =>
			ib = i;
		case =>
			return discontinuity;
		};
	};

	return (ia, ib);
};
diff --git a/time/chrono/chronology.ha b/time/chrono/chronology.ha
new file mode 100644
index 0000000..e86764a
--- /dev/null
+++ b/time/chrono/chronology.ha
@@ -0,0 +1,159 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use time;

// Invalid [[moment]].
export type invalid = !void;

// A moment in time within a [[locality]]. Create one with [[new]]. This type
// extends the [[time::instant]] type and couples it with a [[timescale]] via
// its [[locality]] field.
//
// This object should be treated as private and immutable. Directly mutating its
// fields causes undefined behavour when used with module functions. Likewise,
// interrogating the fields' type and value (e.g. using match statements) is
// also improper.
//
// Moments observe a daydate, time-of-day, and [[zone]], which are evaluated,
// cached and obtained with the observer functions [[daydate]], [[daytime]], and
// [[mzone]]. These values are derived from the embedded instant and locality
// information, and thus are guaranteed to be valid.
export type moment = struct {
	// The embedded [[time::instant]].
	time::instant,

	// The [[locality]] with which to interpret this moment.
	loc: locality,

	// The observed [[zone]].
	zone: nullable *zone,

	// The observed daydate (scalar day number)
	// since an abitrary epoch (e.g. the Unix epoch 1970-01-01).
	daydate: (void | i64),

	// The observed time-of-day (amount of daytime progressed in a day).
	daytime: (void | time::duration),
};

// Creates a new [[moment]]. Uses a given [[time::instant]] with a [[timescale]]
// associated with a given [[locality]].
export fn new(loc: locality, i: time::instant) moment = {
	return moment {
		sec = i.sec,
		nsec = i.nsec,
		loc = loc,
		zone = null,
		daydate = void,
		daytime = void,
	};
};

// Observes a [[moment]]'s observed [[zone]].
export fn mzone(m: *moment) zone = {
	match (m.zone) {
	case let z: *zone =>
		return *z;
	case null =>
		const z = _lookupzone(m.loc, *(m: *time::instant));
		m.zone = z;
		return *z;
	};
};

// Observes a [[moment]]'s observed daydate (day number since epoch).
//
// For moments with [[locality]]s based on the [[utc]], [[tai]], [[gps]], and
// similar timescales, their epoch date should be interpreted as the Unix epoch
// (1970 Janurary 1st). Other timescales may suggest their own interpretations
// applicable to other chronologies.
export fn daydate(m: *moment) i64 = {
	match (m.daydate) {
	case let dd: i64 =>
		return dd;
	case void =>
		const (dd, dt) = calc_datetime(
			m.loc, *(m: *time::instant), mzone(m).zoff,
		);
		m.daytime = dt;
		m.daydate = dd;
		return dd;
	};
};

// Observes a [[moment]]'s observed time-of-day (amount of daytime progressed in
// a day) as a [[time::duration]].
export fn daytime(m: *moment) time::duration = {
	match (m.daytime) {
	case let dt: time::duration =>
		return dt;
	case void =>
		const (dd, dt) = calc_datetime(
			m.loc, *(m: *time::instant), mzone(m).zoff,
		);
		m.daytime = dt;
		m.daydate = dd;
		return dt;
	};
};

// Calculates the observed daydate and time-of-day of a [[time::instant]] in a
// [[locality]] at a particular zone offset.
fn calc_datetime(
	loc: locality,
	inst: time::instant,
	zoff: time::duration,
) (i64, time::duration) = {
	const i = time::add(inst, zoff);
	const day = loc.daylength;
	const daysec = day / time::SECOND;
	const dd = if (i.sec >= 0) i.sec / daysec else (i.sec + 1) / daysec - 1;
	const dt = ((i.sec % daysec + daysec) * time::SECOND + i.nsec) % day;
	return (dd, dt);
};

// Creates a [[moment]] from a given [[locality]], zone offset, daydate, and
// time-of-day.
export fn from_datetime(
	loc: locality,
	zo: time::duration,
	dd: i64,
	dt: time::duration,
) moment = {
	const inst = calc_instant(loc.daylength, zo, dd, dt);
	return moment {
		sec = inst.sec,
		nsec = inst.nsec,
		loc = loc,
		zone = null,
		daydate = dd,
		daytime = dt,
	};
};

fn calc_instant(
	day: time::duration, // length of a day
	zo: time::duration,  // zone offset
	dd: i64,             // date since epoch
	dt: time::duration,  // time since start of day
) time::instant = {
	const daysec = (day / time::SECOND): i64;
	const dayrem = day % time::SECOND;
	let i = time::instant {
		sec = dd * daysec,
		nsec = 0,
	};
	i = time::add(i, dd * dayrem);
	i = time::add(i, dt);
	i = time::add(i, -zo);
	return i;
};

// The duration of a day on Earth, in terrestrial (SI) seconds.
export def EARTH_DAY: time::duration = 86400 * time::SECOND;

// The duration of a solar day on Mars, in Martian seconds.
export def MARS_SOL_MARTIAN: time::duration = 86400 * time::SECOND;

// The duration of a solar day on Mars, in terrestrial (SI) seconds.
export def MARS_SOL_TERRESTRIAL: time::duration = 88775244147000 * time::NANOSECOND;
diff --git a/time/chrono/error.ha b/time/chrono/error.ha
new file mode 100644
index 0000000..78876f5
--- /dev/null
+++ b/time/chrono/error.ha
@@ -0,0 +1,44 @@
// License: MPL-2.0
// (c) 2022 Byron Torres <b@torresjrjr.com>
use encoding::utf8;
use fmt;
use fs;
use io;

// All possible errors returned from [[time::chrono]].
export type error = !(
	invalid
	| invalidtzif
	| tzdberror
	| discontinuity
	| analytical
);

// Converts an [[error]] into a human-friendly string.
export fn strerror(err: error) const str = {
	match (err) {
	case invalid =>
		return "Invalid moment";
	case invalidtzif =>
		return "Invalid TZif data";
	case let err: tzdberror =>
		match (err) {
		case let err: fs::error =>
			return fmt::asprintf(
				"Timezone database error: {}",
				fs::strerror(err),
			);
		case let err: io::error =>
			return fmt::asprintf(
				"Timezone database error: {}",
				io::strerror(err),
			);
		case invalidtzif =>
			return "Timezone database error: Invalid TZif data";
		};
	case discontinuity =>
		return "A timescale discontinuity caused a misconversion";
	case analytical =>
		return "The analyical result of a conversion at a timescale discontinuity";
	};
};
diff --git a/time/chrono/leapsec.ha b/time/chrono/leapsec.ha
new file mode 100644
index 0000000..40102cc
--- /dev/null
+++ b/time/chrono/leapsec.ha
@@ -0,0 +1,89 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use bufio;
use encoding::utf8;
use fs;
use io;
use os;
use strconv;
use strings;

// Hare uses raw leap second information when dealing with the UTC and TAI
// timescales. This information is source from a standard file installed at
// /usr/share/zoneinfo/leap-seconds.list, which itself is fetched from and
// periodically maintained at various observatories.
//
// https://data.iana.org/time-zones/code/leap-seconds.list
// https://www.ietf.org/timezones/data/leap-seconds.list
// ftp://ftp.nist.gov/pub/time/leap-seconds.list
// ftp://ftp.boulder.nist.gov/pub/time/leap-seconds.list
//
// This is in contrast to previous systems which rely on TZif files, which are
// installed typically at /usr/share/zoneinfo, as part of the "Olson" IANA
// Timezone database. These files couple timezone and leap second data.
//
// Depending on a system's installation, leap second information may be
// deliberately left out of the TZif files, or duplicated throughout. This
// design also inhibits our ambitions for dealing with multiple, dynamic
// timescales. Therefore, we have decided to take an alternative approach.

// Error initializing the [[utc]] [[timescale]].
type utciniterror = !(fs::error | io::error | encoding::utf8::invalid);

// The number of seconds between the years 1900 and 1970.
//
// This number is hypothetical since timekeeping before atomic clocks was not
// accurate enough to account for small changes in time.
export def SECS_1900_1970: i64 = 2208988800;

// The filepath of the system's "leap-seconds.list" file, which contains UTC/TAI
// leap second data.
export def UTC_LEAPSECS_FILE: str = "/usr/share/zoneinfo/leap-seconds.list";

// UTC/TAI leap second data; UTC timestamps and their offsets from TAI.
// Sourced from [[UTC_LEAPSECS_FILE]].
let utc_leapsecs: [](i64, i64) = [];

let utc_isinitialized: bool = false;

@fini fn free_utc() void = {
	free(utc_leapsecs);
};

fn init_utc_leapsecs() (void | utciniterror) = {
	const file = os::open(UTC_LEAPSECS_FILE)?;
	defer io::close(file)!;
	parse_utc_leapsecs(file, &utc_leapsecs)?;
};

// Parse UTC/TAI leap second data from [[UTC_LEAPSECS_FILE]].
// See file for format details.
fn parse_utc_leapsecs(
	h: io::handle,
	leapsecs: *[](i64, i64),
) (void | encoding::utf8::invalid | io::error) = {
	for (true) {
		const line = match (bufio::scanline(h)) {
		case let err: io::error =>
			return err;
		case io::EOF =>
			return;
		case let line: []u8 =>
			yield strings::fromutf8(line)?;
		};
		defer free(line);
		if (strings::hasprefix(line, '#')) {
			continue;
		};
		const pair = strings::splitn(line, "\t", 3);
		defer free(pair);
		if (len(pair) < 2) {
			continue;
		};
		const a = strconv::stoi64(pair[0])!;
		const b = strconv::stoi64(pair[1])!;
		const a = a - SECS_1900_1970;
		const pair = (a: i64, b: i64);
		append(utc_leapsecs, pair);
	};
};
diff --git a/time/chrono/timescale.ha b/time/chrono/timescale.ha
new file mode 100644
index 0000000..cfa76e7
--- /dev/null
+++ b/time/chrono/timescale.ha
@@ -0,0 +1,390 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use time;

// Represents a scale of time; a time standard. See [[convert]].
export type timescale = struct {
	name: str,
	abbr: str,
	convto: *tsconverter,
	convfrom: *tsconverter,
};

export type tsconverter = fn(ts: *timescale, i: time::instant) ([]time::instant | void);

// A discontinuity between two [[timescale]]s caused a one-to-one
// [[time::instant]] conversion to fail.
export type discontinuity = !void;

// The analytical result of a [[time::instant]] conversion between two
// [[timescale]]s at a point of [[discontinuity]].
//
// An empty slice represents a nonexistent conversion result.
// A populated (>1) slice represents an ambiguous conversion result.
export type analytical = ![]time::instant;

// Converts a [[time::instant]] from one [[timescale]] to the next exhaustively.
// The final conversion result is returned. For each active pair of timescales,
// if neither implements conversion from the first to the second, a two-step
// intermediary TAI conversion will occur. If given zero or one timescales, the
// given instant is returned.
export fn convert(i: time::instant, tscs: *timescale...) (time::instant | analytical) = {
	let ts: []time::instant = [i];
	let tmps: []time::instant = [];

	for (let j = 1z; j < len(tscs); j += 1) {
		let a = tscs[j - 1];
		let b = tscs[j];

		for (let k = 0z; k < len(ts); k += 1) {
			const t = ts[k];

			// try .convto
			match (a.convto(b, t)) {
			case let convs: []time::instant =>
				append(tmps, convs...);
				continue;
			case void => void;
			};

			// try .convfrom
			match (b.convfrom(a, t)) {
			case let convs: []time::instant =>
				append(tmps, convs...);
				continue;
			case void => void;
			};

			// default to TAI intermediary
			const convs = a.convto(&tai, t) as []time::instant;
			for (let l = 0z; l < len(convs); l += 1) {
				append(tmps, (
					b.convfrom(&tai, convs[l]) as []time::instant
				)...);
			};
		};

		// TODO: sort and deduplicate 'ts' here
		ts = tmps;
		tmps = [];
	};

	return if (len(ts) == 1) ts[0] else ts;
};


// International Atomic Time
//
// The realisation of proper time on Earth's geoid.
// Continuous (no leap seconds).
export const tai: timescale = timescale {
	name = "International Atomic Time",
	abbr = "TAI",
	convto = &tai_convto,
	convfrom = &tai_convfrom,
};

fn tai_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &tai =>
		return [i];
	case =>
		return void;
	};
};


fn tai_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &tai =>
		return [i];
	case =>
		return void;
	};
};


// TODO: Write proper conversion functions for all timescales.
//
// Ticket: https://todo.sr.ht/~sircmpwn/hare/642
//
// For UTC, conversion functions are to return two or no instants, depending on
// any leap second events, and use a proper leap second table. See leapsec.ha.


// Coordinated Universal Time
//
// Used as the basis of civil timekeeping.
// Based on TAI; time-dependent offset.
// Discontinuous (has leap seconds).
//
// During a program's initialization, this timescale initializes by loading its
// UTC/TAI leap second data from [[UTC_LEAPSECS_FILE]]; otherwise, fails
// silently. If failed, any attempt to consult UTC leapsec data (e.g. calling
// [[convert]] on UTC) causes an abort. This includes [[in]].
export const utc: timescale = timescale {
	name = "Coordinated Universal Time",
	abbr = "UTC",
	convto = &utc_convto,
	convfrom = &utc_convfrom,
};

fn utc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &utc =>
		return [i];
	case &tai =>
		if (!utc_isinitialized) {
			match (init_utc_leapsecs()) {
			case void =>
				utc_isinitialized = true;
			case =>
				abort("utc timescale uninitialized");
			};
		};

		const idx = lookup_leaps(&utc_leapsecs, time::unix(i));
		const ofst = utc_leapsecs[idx].1;

		if (time::unix(i) == utc_leapsecs[idx].0) {
			void;
		};

		const i = time::instant {
			sec = i.sec + 37,
			nsec = i.nsec,
		};

		return [i];
	case =>
		return void;
	};
};

fn utc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &utc =>
		return [i];
	case &tai =>
		if (!utc_isinitialized) {
			match (init_utc_leapsecs()) {
			case void =>
				utc_isinitialized = true;
			case =>
				abort("utc timescale uninitialized");
			};
		};

		const idx = lookup_leaps(&utc_leapsecs, time::unix(i));
		const ofst = utc_leapsecs[idx].1;

		if (time::unix(i) == utc_leapsecs[idx].0) {
			void;
		};

		const i = time::instant {
			sec = i.sec - 37,
			nsec = i.nsec,
		};

		return [i];
	case =>
		return void;
	};
};

fn lookup_leaps(list: *[](i64, i64), t: i64) size = {
	let lo = 0z, hi = len(list);
	for (hi - lo > 1) {
		const mid = lo + (hi - lo) / 2;
		const middle = list[mid].0;
		const cmp = time::compare(
			time::from_unix(t),
			time::from_unix(middle),
		);
		switch (cmp) {
		case -1 =>
			hi = mid;
		case 0 =>
			lo = mid; break;
		case 1 =>
			lo = mid;
		case =>
			abort("Unreachable");
		};
	};
	return lo;
};


// Global Positioning System Time
//
// Used for GPS coordination.
// Based on TAI; constant -19 second offset.
// Continuous (no leap seconds).
export const gps: timescale = timescale {
	name = "Global Positioning System Time",
	abbr = "GPS",
	convto = &gps_convto,
	convfrom = &gps_convfrom,
};

// The constant offset between GPS-Time (Global Positioning System Time) and TAI
// (International Atomic Time). Used by [[gps]].
def GPS_OFFSET: time::duration = -19 * time::SECOND;

fn gps_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &gps =>
		return [i];
	case &tai =>
		return [time::add(i, -GPS_OFFSET)];
	case =>
		void;
	};
};

fn gps_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &gps =>
		return [i];
	case &tai =>
		return [time::add(i, GPS_OFFSET)];
	case =>
		void;
	};
};


// Terrestrial Time
//
// Used for astronomical timekeeping.
// Based on TAI; constant +32.184 offset.
// Continuous (no leap seconds).
export const tt: timescale = timescale {
	name = "Terrestrial Time",
	abbr = "TT",
	convto = &tt_convto,
	convfrom = &tt_convfrom,
};

// The constant offset between TT (Terrestrial Time) and TAI (International
// Atomic Time). Used by [[tt]].
def TT_OFFSET: time::duration = 32184 * time::MILLISECOND; // 32.184 seconds

fn tt_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &tt =>
		return [i];
	case &tai =>
		return [time::add(i, -TT_OFFSET)];
	case =>
		void;
	};
};


fn tt_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &tt =>
		return [i];
	case &tai =>
		return [time::add(i, TT_OFFSET)];
	case =>
		void;
	};
};

// Arthur David Olson had expressed support for Martian time in his timezone
// database project <https://data.iana.org/time-zones/theory.html>:
//
// > The tz database does not currently support Mars time, but it is documented
// > here in the hopes that support will be added eventually.

// Coordinated Mars Time
//
// Used for timekeeping on Mars.
// Based on TT; constant factor.
// Continuous (no leap seconds).
export const mtc: timescale = timescale {
	name = "Coordinated Mars Time",
	abbr = "MTC",
	convto = &mtc_convto,
	convfrom = &mtc_convfrom,
};

// Factor f, where Martian-time * f == Earth-time.
def FACTOR_TERRESTRIAL_MARTIAN: f64 = 1.0274912517;

// [[time::duration]] in Earth-time between the Unix epoch of 1970 Jan 1st
// midnight, and the Earth-Mars convergence date of 2000 Jan 6th midnight.
def DELTA_UNIXEPOCH_JANSIX: time::duration = 10962 * 24 * time::HOUR;

// [[time::duration]] in Mars-time between the Mars Sol Date epoch corresponding
// to the Gregorian Earth date 1873 Dec 29th, and the Earth-Mars convergence
// date of 2000 Jan 6.
def DELTA_MARSEPOCH_JANSIX: time::duration = 44796 * 24 * time::HOUR;

// [[time::duration]] in Mars-time between the midnights of 2000 Jan 6th on
// Earth and Mars. Earth's midnight occurred first.
def DELTA_JANSIX_ADJUSTMENT: time::duration = 82944 * time::MILLISECOND;

fn mtc_convto(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &mtc =>
		return [i];
	case &tai =>
		// Change epoch from that of the Mars Sol Date
		// to the Earth-Mars convergence date 2000 Jan 6th.
		let i = time::add(i, -DELTA_MARSEPOCH_JANSIX);

		// Slightly adjust epoch for the actual Martian midnight.
		// Earth's midnight occurred before Mars'.
		i = time::add(i, DELTA_JANSIX_ADJUSTMENT);

		// Scale from Mars-time to Earth-time.
		i = time::mult(i, FACTOR_TERRESTRIAL_MARTIAN);

		// Change epoch to the Unix epoch 1970 Jan 1st (Terrestrial Time).
		i = time::add(i, DELTA_UNIXEPOCH_JANSIX);

		// Get the TAI time.
		// assertion since TT and TAI are continuous.
		const ts = tt.convto(&tai, i) as []time::instant;

		return ts;
	case =>
		void;
	};

};

fn mtc_convfrom(ts: *timescale, i: time::instant) ([]time::instant | void) = {
	switch (ts) {
	case &mtc =>
		return [i];
	case &tai =>
		// Get the "Terrestrial Time".
		// assertion since TT and TAI are continuous.
		let i = (tt.convfrom(&tai, i) as []time::instant)[0];

		// Change epoch from the Unix epoch 1970 Jan 1st (Terrestrial Time)
		// to the Earth-Mars convergence date 2000 Jan 6th midnight.
		i = time::add(i, -DELTA_UNIXEPOCH_JANSIX);

		// Scale from Earth-time to Mars-time.
		i = time::mult(i, 1.0 / FACTOR_TERRESTRIAL_MARTIAN);

		// Slightly adjust epoch for the actual Martian midnight.
		// Earth's midnight occurred before Mars'.
		i = time::add(i, -DELTA_JANSIX_ADJUSTMENT);

		// Change epoch to that of the Mars Sol Date.
		i = time::add(i, DELTA_MARSEPOCH_JANSIX);

		return [i];
	case =>
		void;
	};

};
diff --git a/time/chrono/timezone.ha b/time/chrono/timezone.ha
new file mode 100644
index 0000000..7f03034
--- /dev/null
+++ b/time/chrono/timezone.ha
@@ -0,0 +1,350 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use bufio;
use io;
use os;
use path;
use strings;
use time;

// The locality of a [[moment]]. Contains information about how to calculate a
// moment's observed chronological values.
export type locality = *timezone;

// A timezone; a political or otherwise theoretical region with a ruleset
// regarding offsets for calculating localized date/time.
export type timezone = struct {
	// The textual identifier ("Europe/Amsterdam")
	name: str,

	// The base timescale (time::chrono::utc)
	timescale: *timescale,

	// The duration of a day in this timezone (24 * time::HOUR)
	daylength: time::duration,

	// The possible temporal zones a locality with this timezone can observe
	// (CET, CEST, ...)
	zones: []zone,

	// The transitions between this timezone's zones
	transitions: []transition,

	// A timezone specifier in the POSIX "expanded" TZ format.
	// See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
	//
	// Used for extending calculations beyond the last known transition.
	posix_extend: str,
};

// A [[timezone]] state, with an offset for calculating localized date/time.
export type zone = struct {
	// The offset from the normal timezone (2 * time::HOUR)
	zoff: time::duration,

	// The full descriptive name ("Central European Summer Time")
	name: str,

	// The abbreviated name ("CEST")
	abbr: str,

	// Indicator of Daylight Saving Time
	dst: bool, // true
};

// A [[timezone]] transition between two [[zone]]s.
export type transition = struct {
	when: time::instant,
	zoneindex: int,
};

// A destructured dual std/dst POSIX timezone. See tzset(3).
type tzname = struct {
	std_name: str,
	std_offset: time::duration,
	dst_name: str,
	dst_offset: time::duration,
	dst_start: str,
	dst_starttime: str,
	dst_end: str,
	dst_endtime: str,
};

// Frees a [[timezone]]. A [[locality]] argument can be passed.
export fn timezone_free(tz: *timezone) void = {
	free(tz.name);
	for (let i = 0z; i < len(tz.zones); i += 1) {
		zone_finish(&tz.zones[i]);
	};
	free(tz.zones);
	free(tz.transitions);
	free(tz.posix_extend);
	free(tz);
};

// Frees resources associated with a [[zone]].
export fn zone_finish(z: *zone) void = {
	free(z.name);
	free(z.abbr);
};

// Creates an equivalent [[moment]] with a different [[locality]].
//
// If the moment's associated [[timescale]] and the target locality's timescale
// are different, a conversion from one to the other via the TAI timescale will
// be attempted. Any [[discontinuity]] occurrence will be returned. If a
// discontinuity against TAI amongst the two timescales exist, consider
// converting such instants manually.
export fn in(loc: locality, m: moment) (moment | discontinuity) = {
	let i = *(&m: *time::instant);
	if (m.loc.timescale != loc.timescale) {
		match (convert(i, m.loc.timescale, loc.timescale)) {
		case analytical =>
			return discontinuity;
		case let i: time::instant =>
			return new(loc, i);
		};
	};
	return new(loc, i);
};

// Finds and returns a [[moment]]'s currently observed [[zone]].
fn _lookupzone(loc: locality, inst: time::instant) *zone = {
	// TODO: https://todo.sr.ht/~sircmpwn/hare/643
	if (len(loc.zones) == 0) {
		abort("time::chrono: Timezone has no zones");
	};

	if (len(loc.zones) == 1) {
		return &loc.zones[0];
	};

	if (
		len(loc.transitions) == 0
		|| time::compare(inst, loc.transitions[0].when) == -1
	) {
		// TODO: special case
		abort("lookupzone(): time is before known transitions");
	};

	let lo = 0z;
	let hi = len(loc.transitions);
	for (hi - lo > 1) {
		const mid = lo + (hi - lo) / 2;
		const middle = loc.transitions[mid].when;
		switch (time::compare(inst, middle)) {
		case -1 =>
			hi = mid;
		case 0 =>
			lo = mid; break;
		case 1 =>
			lo = mid;
		case =>
			abort("Unreachable");
		};
	};

	const z = &loc.zones[loc.transitions[lo].zoneindex];

	// if we've reached the end of the locality's transitions, try its
	// posix_extend string
	//
	// TODO: Unfinished; complete.
	if (lo == len(loc.transitions) - 1 && loc.posix_extend != "") {
		void;
	};

	return z;
};

// Creates a [[timezone]] with a single [[zone]]. Useful for fixed offsets.
// For example, replicate the civil time Hawaii timezone on Earth:
//
// 	let hawaii = chrono::fixedzone(&chrono::utc, chrono::EARTH_DAY,
// 		chrono::zone {
// 			zoff = -10 * time::HOUR,
// 			name = "Hawaiian Reef",
// 			abbr = "HARE",
// 			dst = false,
// 		},
// 	);
//
export fn fixedzone(ts: *timescale, daylen: time::duration, z: zone) timezone = {
	return timezone {
		name = z.name,
		timescale = ts,
		daylength = daylen,
		zones = alloc([z]),
		transitions = [],
		posix_extend = "",
	};
};

// The system's [[locality]]; the system's local [[timezone]].
//
// This is set during a program's initialisation, where the TZ environment
// variable is tried, otherwise the /etc/localtime file is tried, otherwise a
// default is used.
//
// The default timezone is equivalent to that of [[UTC]], with "Local" being the
// name of both the timezone and its single zero-offset zone.
export const LOCAL: locality = &TZ_LOCAL;

def TZ_LOCAL_NAME: str = "Local";

let TZ_LOCAL: timezone = timezone {
	name = TZ_LOCAL_NAME,
	timescale = &utc,
	daylength = EARTH_DAY,
	zones = [
		zone {
			zoff = 0 * time::SECOND,
			name = TZ_LOCAL_NAME,
			abbr = "",
			dst = false,
		},
	],
	transitions = [],
	posix_extend = "",
};

@init fn init_tz_local() void = {
	match (os::getenv("TZ")) {
	case let timezone: str =>
		match (tz(timezone)) {
		case let loc: locality =>
			TZ_LOCAL = *loc;
		case =>
			return;
		};
	case void =>
		const filepath = match (os::readlink(LOCALTIME_PATH)) {
		case let fp: str =>
			yield fp;
		case =>
			yield LOCALTIME_PATH;
		};

		const file = match (os::open(filepath)) {
		case let f: io::file =>
			yield f;
		case =>
			return;
		};
		defer io::close(file)!;

		if (strings::hasprefix(filepath, ZONEINFO_PREFIX)) {
			TZ_LOCAL.name = strings::trimprefix(
				filepath, ZONEINFO_PREFIX,
			);
		};

		static let buf: [os::BUFSIZ]u8 = [0...];
		const file = bufio::init(file, buf, []);
		load_tzif(&file, &TZ_LOCAL): void;
	};
};

@fini fn free_tz_local() void = {
	free(TZ_LOCAL.transitions);
	switch(TZ_LOCAL.name) {
	case TZ_LOCAL_NAME => void;
	case =>
		free(TZ_LOCAL.zones);
	};
};

// The UTC (Coordinated Universal Time) "Zulu" [[timezone]] as a [[locality]].
export const UTC: locality = &TZ_UTC;

const TZ_UTC: timezone = timezone {
	name = "UTC",
	timescale = &utc,
	daylength = EARTH_DAY,
	zones = [
		zone {
			zoff = 0 * time::SECOND,
			name = "Universal Coordinated Time",
			abbr = "UTC",
			dst = false,
		},
	],
	transitions = [],
	posix_extend = "",
};

// The TAI (International Atomic Time) "Zulu" [[timezone]] as a [[locality]].
export const TAI: locality = &TZ_TAI;

const TZ_TAI: timezone = timezone {
	name = "TAI",
	timescale = &tai,
	daylength = EARTH_DAY,
	zones = [
		zone {
			zoff = 0 * time::SECOND,
			name = "International Atomic Time",
			abbr = "TAI",
			dst = false,
		},
	],
	transitions = [],
	posix_extend = "",
};

// The GPS (Global Positioning System) "Zulu" [[timezone]] as a [[locality]].
export const GPS: locality = &TZ_GPS;

const TZ_GPS: timezone = timezone {
	name = "GPS",
	timescale = &gps,
	daylength = EARTH_DAY,
	zones = [
		zone {
			zoff = 0 * time::SECOND,
			name = "Global Positioning System",
			abbr = "GPS",
			dst = false,
		},
	],
	transitions = [],
	posix_extend = "",
};

// The TT (Terrestrial Time) "Zulu" [[timezone]] as a [[locality]].
export const TT: locality = &TZ_TT;

const TZ_TT: timezone = timezone {
	name = "TT",
	timescale = &tt,
	daylength = EARTH_DAY,
	zones = [
		zone {
			zoff = 0 * time::SECOND,
			name = "Terrestrial Time",
			abbr = "TT",
			dst = false,
		},
	],
	transitions = [],
	posix_extend = "",
};

// The MTC (Coordinated Mars Time) "Zulu" [[timezone]] as a [[locality]].
export const MTC: locality = &TZ_MTC;

const TZ_MTC: timezone = timezone {
	name = "MTC",
	timescale = &mtc,
	daylength = MARS_SOL_MARTIAN,
	zones = [
		zone {
			zoff = 0 * time::SECOND,
			name = "Coordinated Mars Time",
			abbr = "MTC",
			dst = false,
		},
	],
	transitions = [],
	posix_extend = "",
};
diff --git a/time/chrono/tzdb.ha b/time/chrono/tzdb.ha
new file mode 100644
index 0000000..0ffa23b
--- /dev/null
+++ b/time/chrono/tzdb.ha
@@ -0,0 +1,318 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use bufio;
use bytes;
use encoding::utf8;
use endian;
use errors;
use fs;
use io;
use os;
use path;
use strings;
use time;

// Error concerning the Timezone database.
export type tzdberror = !(invalidtzif | fs::error | io::error);

// Invalid TZif data.
export type invalidtzif = !void;

// Finds, loads, and allocates a [[timezone]] from the system's Timezone
// database, normally located at /usr/share/zoneinfo, and returns it as a
// [[locality]]. Each call returns a new instance. The caller must free the
// return value.
//
// All localities provided default to the [[utc]] [[timescale]] and
// [[EARTH_DAY]] day-length.
export fn tz(name: str) (locality | tzdberror) = {
	const filepath = path::init(ZONEINFO_PREFIX, name)!;
	const fpath = path::string(&filepath);
	const file = os::open(fpath)?;

	static let buf: [os::BUFSIZ]u8 = [0...];
	const bufstrm = bufio::init(file, buf, []);

	let loc = alloc(timezone {
		name = strings::dup(name),
		timescale = &utc,
		daylength = EARTH_DAY,
		...
	});
	match (load_tzif(&bufstrm, loc)) {
	case void =>
		io::close(&bufstrm)?;
		io::close(file)?;
		return loc;
	case invalidtzif =>
		io::close(&bufstrm): void;
		io::close(file): void;
		return invalidtzif;
	case let err: io::error =>
		io::close(&bufstrm): void;
		io::close(file): void;
		return err;
	};
};

// Loads data of the TZif "Time Zone Information Format", and initialises the
// fields "zones", "transitions", and "posix_extend" of the given [[timezone]].
//
// See: https://datatracker.ietf.org/doc/html/rfc8536
fn load_tzif(h: io::handle, tz: *timezone) (void | invalidtzif | io::error) = {
	const buf1: [1]u8 = [0...];
	const buf4: [4]u8 = [0...];
	const buf8: [8]u8 = [0...];
	const buf15: [15]u8 = [0...];

	// test for magic "TZif"
	mustread(h, buf4)?;
	if (!bytes::equal(buf4, ['T': u8, 'Z': u8, 'i': u8, 'f': u8])) {
		return invalidtzif;
	};

	// read version
	mustread(h, buf1)?;
	const version = switch (buf1[0]) {
	case 0 =>
		yield 1;
	case '2' =>
		yield 2;
	case '3' =>
		yield 3;
	case =>
		return invalidtzif;
	};

	// skip padding
	mustread(h, buf15)?;

	// read counts
	mustread(h, buf4)?; let isutcnt = endian::begetu32(buf4);
	mustread(h, buf4)?; let isstdcnt = endian::begetu32(buf4);
	mustread(h, buf4)?; let leapcnt = endian::begetu32(buf4);
	mustread(h, buf4)?; let timecnt = endian::begetu32(buf4);
	mustread(h, buf4)?; let typecnt = endian::begetu32(buf4);
	mustread(h, buf4)?; let charcnt = endian::begetu32(buf4);

	let is64 = false;
	if (version > 1) {
		is64 = true;

		// skip to the version 2 data
		const skip = (
			// size of version 1 data block
			timecnt * 4
			+ timecnt
			+ typecnt * 6
			+ charcnt
			+ leapcnt * 8
			+ isstdcnt
			+ isutcnt
			// size of version 2 header
			+ 20
		);
		for (let i = 0z; i < skip; i += 1) {
			mustread(h, buf1)?;
		};

		// read version 2 counts
		mustread(h, buf4)?; isutcnt = endian::begetu32(buf4);
		mustread(h, buf4)?; isstdcnt = endian::begetu32(buf4);
		mustread(h, buf4)?; leapcnt = endian::begetu32(buf4);
		mustread(h, buf4)?; timecnt = endian::begetu32(buf4);
		mustread(h, buf4)?; typecnt = endian::begetu32(buf4);
		mustread(h, buf4)?; charcnt = endian::begetu32(buf4);
	};

	if (typecnt == 0 || charcnt == 0) {
		return invalidtzif;
	};

	if (!(isutcnt == 0 || isutcnt == typecnt)
			&& (isstdcnt == 0 && isstdcnt == typecnt)) {
		return invalidtzif;
	};

	const timesz = if (is64) 8 else 4;

	// read data

	const transition_times: []i64 = [];
	if (is64) {
		readitems8(h, &transition_times, timecnt)?;
	} else {
		readitems4(h, &transition_times, timecnt)?;
	};
	defer free(transition_times);
	const zone_indicies: []u8 = [];
	readbytes(h, &zone_indicies, timecnt)?;
	defer free(zone_indicies);
	const zonedata: []u8 = [];
	readbytes(h, &zonedata, typecnt * 6)?;
	defer free(zonedata);
	const abbrdata: []u8 = [];
	readbytes(h, &abbrdata, charcnt)?;
	defer free(abbrdata);
	const leapdata: []u8 = [];
	readbytes(h, &leapdata, leapcnt * (timesz: u32 + 4))?;
	defer free(leapdata);
	const stdwalldata: []u8 = [];
	readbytes(h, &stdwalldata, isstdcnt)?;
	defer free(stdwalldata);
	const normlocaldata: []u8 = [];
	readbytes(h, &normlocaldata, isutcnt)?;
	defer free(normlocaldata);
	// read footer

	let footerdata: []u8 = [];
	defer free(footerdata);
	mustread(h, buf1)?;
	if (buf1[0] != 0x0A) { // '\n' newline
		return invalidtzif;
	};
	for (true) {
		mustread(h, buf1)?;
		if (buf1[0] == 0x0A) { // '\n' newline
			break;
		};
		if (buf1[0] == 0x0) { // cannot contain NUL
			return invalidtzif;
		};
		append(footerdata, buf1...);
	};
	const posix_extend = strings::dup(match (strings::fromutf8(footerdata)) {
	case let s: str =>
		yield s;
	case encoding::utf8::invalid =>
		return invalidtzif;
	});

	// assemble structured data

	// assemble zones
	let zones: []zone = [];
	for (let i = 0z; i < typecnt; i += 1) {
		const idx = i * 6;
		const zone = zone { ... };

		// offset
		const zoff = endian::begetu32(zonedata[idx..idx + 4]): i32;
		if (zoff == -2147483648) { // -2^31
			return invalidtzif;
		};
		zone.zoff = zoff * time::SECOND;

		// daylight saving time indicator
		zone.dst = switch (zonedata[idx + 4]) {
		case 1u8 =>
			yield true;
		case 0u8 =>
			yield false;
		case =>
			return invalidtzif;
		};

		// abbreviation
		const abbridx = zonedata[idx + 5];
		if (abbridx < 0 || abbridx > (charcnt - 1)) {
			return invalidtzif;
		};
		let bytes: []u8 = [];
		for (let j = abbridx; j < len(abbrdata); j += 1) {
			if (abbrdata[j] == 0x0) {
				bytes = abbrdata[abbridx..j];
				break;
			};
		};
		if (len(bytes) == 0) { // no NUL encountered
			return invalidtzif;
		};
		const abbr = match (strings::fromutf8(bytes)) {
		case let s: str =>
			yield s;
		case encoding::utf8::invalid =>
			return invalidtzif;
		};
		zone.abbr = strings::dup(abbr);

		append(zones, zone);
	};

	// assemble transitions
	let transitions: []transition = [];
	for (let i = 0z; i < timecnt; i += 1) {
		const zoneindex = zone_indicies[i]: int;
		if (zoneindex < 0 || zoneindex > (typecnt: int - 1)) {
			return invalidtzif;
		};

		const tx = transition {
			when = time::instant {
				sec = transition_times[i],
				...
			},
			zoneindex = zoneindex,
		};

		// stdwalldata and normlocaldata have been omitted,
		// until they show their utility.

		append(transitions, tx);
	};

	// commit and return data
	tz.zones = zones;
	tz.transitions = transitions;
	tz.posix_extend = posix_extend;
};

fn mustread(h: io::handle, buf: []u8) (void | invalidtzif | io::error) = {
	match (io::readall(h, buf)) {
	case let err: io::error =>
		return err;
	case io::EOF =>
		return invalidtzif;
	case size =>
		return;
	};
};

fn readbytes(
	h: io::handle,
	items: *[]u8,
	n: size,
) (void | invalidtzif | io::error) = {
	const buf: [1]u8 = [0];
	for (let i = 0z; i < n; i += 1) {
		mustread(h, buf)?;
		const it = buf[0];
		append(items, it);
	};
};

fn readitems8(
	h: io::handle,
	items: *[]i64,
	n: size,
) (void | invalidtzif | io::error) = {
	const buf: [8]u8 = [0...];
	for (let i = 0z; i < n; i += 1) {
		mustread(h, buf)?;
		const it = endian::begetu64(buf): i64;
		append(items, it);
	};
};

fn readitems4(
	h: io::handle,
	items: *[]i64,
	n: size,
) (void | invalidtzif | io::error) = {
	const buf: [4]u8 = [0...];
	for (let i = 0z; i < n; i += 1) {
		mustread(h, buf)?;
		const it = endian::begetu32(buf): i64;
		append(items, it);
	};
};
diff --git a/time/date/README b/time/date/README
new file mode 100644
index 0000000..317a284
--- /dev/null
+++ b/time/date/README
@@ -0,0 +1,20 @@
The time::date module implements the common international Gregorian chronology,
based on the astronomically numbered proleptic Gregorian calendar, as per ISO
8601, and the common 24 hour clock. It provides [[date]], a representation of
civil date/time and a optimized extension of the [[time::chrono::moment]] type.
The [[time::chrono]] module has many useful functions which interoperate with
dates. Any [[time::chrono]] function which accepts *moment also accepts *date.

Dates are created using [[new]], [[now]], [[nowutc]], or a "from_" function.
Alternatively, the [[virtual]]/[[realize]] interface can handle uncertain or
invalid date/time information, and construct new dates incrementally and safely.
The observer functions ([[year]], [[hour]], etc.) evaluate a date's observed
chronological values, adjusted for its associated [[time::chrono::locality]].
Use [[in]] to localize a date to another locality; consult [[time::chrono::tz]].
See [[parse]] and [[format]] for working with date/time strings.

Date arithmetic operations are categorized into "timescalar" or "chronological".
Timescalar uses [[time::duration]]; see [[add]], [[time::chrono::diff]].
Chronological uses [[period]]; see [[reckon]], [[pdiff]], [[unitdiff]],
[[truncate]]. Note that calendrical arithmetic is highly irregular due to field
overflows and timezone discontinuities, so think carefully about what you want.
diff --git a/time/date/date.ha b/time/date/date.ha
new file mode 100644
index 0000000..09d85bd
--- /dev/null
+++ b/time/date/date.ha
@@ -0,0 +1,285 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use errors;
use time;
use time::chrono;

// Invalid [[date]].
export type invalid = !chrono::invalid;

// A date/time object; a [[time::chrono::moment]] wrapper optimized for the
// Gregorian chronology, and by extension a [[time::instant]] wrapper.
//
// This object should be treated as private and immutable. Directly mutating its
// fields causes undefined behaviour when used with module functions. Likewise,
// interrogating the fields' type and value (e.g. using match statements) is
// also improper.
//
// A date observes various chronological values, cached in its fields. To
// evaluate and obtain these values, use the various observer functions
// ([[year]], [[hour]], etc.). These values are derived from the embedded moment
// information, and thus are guaranteed to be valid.
//
// See [[virtual]] for an public, mutable, intermediary representation of a
// date, which waives guarantees of validity.
export type date = struct {
	chrono::moment,

	era:         (void | int),
	year:        (void | int),
	month:       (void | int),
	day:         (void | int),
	yearday:     (void | int),
	isoweekyear: (void | int),
	isoweek:     (void | int),
	week:        (void | int),
	sundayweek:  (void | int),
	weekday:     (void | int),

	hour:        (void | int),
	minute:      (void | int),
	second:      (void | int),
	nanosecond:  (void | int),
};

fn init() date = date {
	sec         = 0,
	nsec        = 0,
	loc         = chrono::UTC,
	zone        = null,
	daydate     = void,
	daytime     = void,

	era         = void,
	year        = void,
	month       = void,
	day         = void,
	yearday     = void,
	isoweekyear = void,
	isoweek     = void,
	week        = void,
	sundayweek  = void,
	weekday     = void,

	hour        = void,
	minute      = void,
	second      = void,
	nanosecond  = void,
};

// Evaluates and populates all of a [[date]]'s fields.
fn all(d: *date) *date = {
	_era(d);
	_year(d);
	_month(d);
	_day(d);
	_yearday(d);
	_isoweekyear(d);
	_isoweek(d);
	_week(d);
	_sundayweek(d);
	_weekday(d);

	_hour(d);
	_minute(d);
	_second(d);
	_nanosecond(d);

	return d;
};

// Creates a new [[date]]. A maximum of 7 optional field arguments can be given:
// year, month, day-of-month, hour, minute, second, nanosecond. 8 or more causes
// an abort.
//
// 	// 0000-01-01 00:00:00.000000000 +0000 UTC UTC
// 	date::new(time::chrono::UTC, 0);
//
// 	// 2019-12-27 20:07:08.000031415 +0000 UTC UTC
// 	date::new(time::chrono::UTC, 0,  2019, 12, 27,  20, 07, 08, 31415);
//
// 	// 2019-12-27 21:00:00.000000000 +0100 CET Europe/Amsterdam
// 	date::new(time::chrono::tz("Europe/Amsterdam")!, 1 * time::HOUR,
// 		2019, 12, 27,  21);
//
// 'zo' is the zone offset from the normal timezone (in most cases, UTC). For
// example, the "Asia/Tokyo" timezone has a single zoff of +9 hours, but the
// "Australia/Sydney" timezone has zoffs +10 hours and +11 hours, as they
// observe Daylight Saving Time.
//
// If specified (non-void), 'zo' must match one of the timezone's observed
// zoffs, or will fail. See [[time::chrono::fixedzone]] for custom timezones.
//
// You may omit the zoff. If the givem timezone has a single zone, [[new]]
// will use that zone's zoff. Otherwise [[new]] will try to infer the zoff
// from the multiple zones. This will fail during certain timezone transitions,
// where certain dates are ambiguous or nonexistent. For example:
//
// - In the Europe/Amsterdam timezone, at 1995 March 26th,
//   the local time 02:30 was never observed,
//   as the clock jumped forward 1 hour from 02:00 CET to 03:00 CEST.
//
// - In the Europe/Amsterdam timezone, at 1995 September 24th,
//   the local time 02:30 was observed twice (00:30 UTC & 01:30 UTC),
//   as the clock jumped back 1 hour from 03:00 CEST to 02:00 CET.
export fn new(
	loc: chrono::locality,
	zo: (time::duration | void),
	fields: int...
) (date | invalid) = {
	// TODO:
	// - revise examples
	// - Implement as described.
	// - fix calls with `years <= -4715`.
	//   https://todo.sr.ht/~sircmpwn/hare/565
	let _fields: [_]int = [
		0, 1, 1,    // year month day
		0, 0, 0, 0, // hour min sec nsec
	];

	assert(len(fields) <= len(_fields),
		"time::date::new(): Too many field arguments");
	_fields[..len(fields)] = fields;

	let v = newvirtual();

	v.vloc       = loc;
	v.zoff       = zo;
	v.year       = _fields[0];
	v.month      = _fields[1];
	v.day        = _fields[2];
	v.hour       = _fields[3];
	v.minute     = _fields[4];
	v.second     = _fields[5];
	v.nanosecond = _fields[6];

	let d = (realize(v, loc) as (date | invalid))?;

	// check if input values are actually observed
	if (
		zo as time::duration != chrono::mzone(&d).zoff
		|| _fields[0] != _year(&d)
		|| _fields[1] != _month(&d)
		|| _fields[2] != _day(&d)
		|| _fields[3] != _hour(&d)
		|| _fields[4] != _minute(&d)
		|| _fields[5] != _second(&d)
		|| _fields[6] != _nanosecond(&d)
	) {
		return invalid;
	};

	return d;
};

// Returns a [[date]] of the current system time using
// [[time::clock::REALTIME]], in the [[time::chrono::LOCAL]] locality.
export fn now() date = {
	return from_instant(chrono::LOCAL, time::now(time::clock::REALTIME));
};

// Returns a [[date]] of the current system time using
// [[time::clock::REALTIME]], in the [[time::chrono::UTC]] locality.
export fn nowutc() date = {
	return from_instant(chrono::UTC, time::now(time::clock::REALTIME));
};

// Creates a [[date]] from a [[time::chrono::moment]].
export fn from_moment(m: chrono::moment) date = {
	const d = init();
	d.loc = m.loc;
	d.sec = m.sec;
	d.nsec = m.nsec;
	d.daydate = m.daydate;
	d.daytime = m.daytime;
	d.zone = m.zone;
	return d;
};

// Creates a [[date]] from a [[time::instant]]
// in a [[time::chrono::locality]].
export fn from_instant(loc: chrono::locality, i: time::instant) date = {
	return from_moment(chrono::new(loc, i));
};

// Creates a [[date]] from a string, parsed according to a layout format.
// See [[parse]] and [[format]]. At least a complete calendar date has to be
// provided. The if hour, minute, second, nanosecond, or zone offset are not
// provided, they default to 0.
//
// 	let new = date::from_str(
// 		date::STAMP_NOZL,
// 		"2019-12-27 22:07:08.000000000 +0100 CET Europe/Amsterdam",
// 		locs...
// 	)!;
//
// The date's [[time::chrono::locality]] will be selected from the provided
// locality arguments. The 'name' field of these localities will be matched
// against the parsed result for the %L specifier. If %L is not specified, or if
// no locality is provided, [[time::chrono::UTC]] is used.
export fn from_str(
	layout: str,
	s: str,
	locs: time::chrono::locality...
) (date | parsefail | insufficient | invalid) = {
	const v = newvirtual();
	v.zoff = 0;
	v.hour = 0;
	v.minute = 0;
	v.second = 0;
	v.nanosecond = 0;
	parse(&v, layout, s)?;
	return realize(v, locs...)?;
};

@test fn from_str() void = {
	let testcases: [_](str, str, []chrono::locality, (date | error)) = [
		(STAMP_NOZL, "2001-02-03 15:16:17.123456789 +0000 UTC UTC", [],
			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17, 123456789)!),
		(STAMP, "2001-02-03 15:16:17", [],
			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!),
		(RFC3339, "2001-02-03T15:16:17+0000", [],
			new(chrono::UTC, 0, 2001, 2, 3, 15, 16, 17)!),
		("%F", "2009-06-30", [],
			new(chrono::UTC, 0, 2009, 6, 30)!),
		("%F %L", "2009-06-30 GPS", [chrono::TAI, chrono::GPS],
			new(chrono::GPS, 0, 2009, 6, 30)!),
		("%F %T", "2009-06-30 01:02:03", [],
			new(chrono::UTC, 0, 2009, 6, 30, 1, 2, 3)!),
		("%FT%T%Z", "2009-06-30T18:30:00Z", [],
			new(chrono::UTC, 0, 2009, 6, 30, 18, 30)!),
		("%FT%T.%N%Z", "2009-06-30T18:30:00.987654321Z", [],
			new(chrono::UTC, 0, 2009, 6, 30, 18, 30, 0, 987654321)!),
		// TODO: for the tests overhaul, when internal test timezones
		// are available, check for %L
		//("%FT%T%z %L", "2009-06-30T18:30:00+0200 Europe/Amsterdam", [amst],
		//	new(amst, 2 * time::HOUR, 2009, 6, 30, 18, 30)!),

		("%Y", "a", [], 'a': parsefail),
		("%X", "2008", [], '2': parsefail),
	];

	let buf: [64]u8 = [0...];
	for (let i = 0z; i < len(testcases); i += 1) {
		const t = testcases[i];
		const expect = t.3;
		const actual = from_str(t.0, t.1, t.2...);

		match (expect) {
		case let e: date =>
			assert(actual is date, "wanted 'date', got 'error'");
			assert(chrono::eq(&(actual as date), &e)!,
				"incorrect 'date' value");
		case let e: parsefail =>
			assert(actual is parsefail,
				"wanted 'parsefail', got other");
		case insufficient =>
			assert(actual is insufficient,
				"wanted 'insufficient', got other");
		case invalid =>
			assert(actual is invalid,
				"wanted 'invalid', got other");
		};
	};
};
diff --git a/time/date/daydate.ha b/time/date/daydate.ha
new file mode 100644
index 0000000..d1ab736
--- /dev/null
+++ b/time/date/daydate.ha
@@ -0,0 +1,597 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
// (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net>
use errors;
use time::chrono;

// Hare internally uses the Unix epoch (1970-01-01) for calendrical logic. Here
// we provide useful constant for working with the astronomically numbered
// proleptic Gregorian calendar, as offsets from the Hare epoch.

// The Hare epoch of the Julian Day Number.
export def EPOCHAL_JULIAN: i64 = -2440588;

// The Hare epoch of the Gregorian Common Era.
export def EPOCHAL_GREGORIAN: i64 = -719164;

// Calculates whether a year is a leap year.
export fn isleapyear(y: int) bool = {
	return if (y % 4 != 0) false
	else if (y % 100 != 0) true
	else if (y % 400 != 0) false
	else true;
};

// Calculates whether a given year, month, and day-of-month, is a valid date.
fn is_valid_ymd(y: int, m: int, d: int) bool = {
	return m >= 1 && m <= 12 && d >= 1 &&
		d <= calc_month_daycnt(y, m);
};

// Calculates whether a given year, and day-of-year, is a valid date.
fn is_valid_yd(y: int, yd: int) bool = {
	return yd >= 1 && yd <= calc_year_daycnt(y);
};

// Calculates the number of days in the given month of the given year.
fn calc_month_daycnt(y: int, m: int) int = {
	const days_per_month: [_]int = [
		31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
	];
	if (m == 2) {
		return if (isleapyear(y)) 29 else 28;
	} else {
		return days_per_month[m - 1];
	};
};

// Calculates the number of days in a given year.
fn calc_year_daycnt(y: int) int = {
	return if (isleapyear(y)) 366 else 365;
};

// Calculates the day-of-week of January 1st, given a year.
fn calc_janfirstweekday(y: int) int = {
	const y = (y % 400) + 400; // keep year > 0 (using Gregorian cycle)
	// Gauss' algorithm
	const wd = (5 * ((y - 1) % 4)
		+ 4 * ((y - 1) % 100)
		+ 6 * ((y - 1) % 400)
	) % 7;
	return wd;
};

// Calculates the era, given a year.
fn calc_era(y: int) int = {
	return if (y >= 0) {
		yield 1; // CE "Common Era"
	} else {
		yield 0; // BCE "Before Common Era"
	};
};

// Calculates the year, month, and day-of-month, given an epochal day.
fn calc_ymd(e: i64) (int, int, int) = {
	// Algorithm adapted from:
	// https://en.wikipedia.org/wiki/Julian_day#Julian_or_Gregorian_calendar_from_Julian_day_number
	//
	// Alternate methods of date calculation should be explored.
	const J = e - EPOCHAL_JULIAN;

	// TODO: substitute numbers where possible
	const b = 274277;
	const c = -38;
	const j = 1401;
	const m = 2;
	const n = 12;
	const p = 1461;
	const r = 4;
	const s = 153;
	const u = 5;
	const v = 3;
	const w = 2;
	const y = 4716;

	const f = J + j + (((4 * J + b) / 146097) * 3) / 4 + c;
	const a = r * f + v;
	const g = (a % p) / r;
	const h = u * g + w;

	const D = (h % s) / u + 1;
	const M = ((h / s + m) % n) + 1;
	const Y = (a / p) - y + (n + m - M) / n;

	return (Y: int, M: int, D: int);
};

// Calculates the day-of-year, given a year, month, and day-of-month.
fn calc_yearday(y: int, m: int, d: int) int = {
	const months_firsts: [_]int = [
		0, 31, 59,
		90, 120, 151,
		181, 212, 243,
		273, 304, 334,
	];

	if (m >= 3 && isleapyear(y)) {
		return months_firsts[m - 1] + d + 1;
	} else {
		return months_firsts[m - 1] + d;
	};
};

// Calculates the ISO week-numbering year,
// given a year, month, day-of-month, and day-of-week.
fn calc_isoweekyear(y: int, m: int, d: int, wd: int) int = {
	if (
		// if the date is within a week whose Thursday
		// belongs to the previous Gregorian year
		m == 1 && (
			(d == 1 && (wd == 4 || wd == 5 || wd == 6))
			|| (d == 2 && (wd == 5 || wd == 6))
			|| (d == 3 && wd == 6)
		)
	) {
		return y - 1;
	} else if (
		// if the date is within a week whose Thursday
		// belongs to the next Gregorian year
		m == 12 && (
			(d == 29 && wd == 0)
			|| (d == 30 && (wd == 0 || wd == 1))
			|| (d == 31 && (wd == 0 || wd == 1 || wd == 2))
		)
	) {
		return y + 1;
	} else {
		return y;
	};
};

// Calculates the ISO week,
// given a year, week, day-of-week, and day-of-year.
fn calc_isoweek(y: int, w: int) int = {
	switch (calc_janfirstweekday(y)) {
	case 0 =>
		return w;
	case 1, 2, 3 =>
		return w + 1;
	case 4 =>
		return if (w != 0) w else 53;
	case 5 =>
		return if (w != 0) w else {
			yield if (isleapyear(y - 1)) 53 else 52;
		};
	case 6 =>
		return if (w != 0) w else 52;
	case =>
		abort("Unreachable");
	};
};

// Calculates the week within a Gregorian year [0..53],
// given a day-of-year and day-of-week.
// All days in a year before the year's first Monday belong to week 0.
fn calc_week(yd: int, wd: int) int = {
	return (yd + 6 - wd) / 7;
};

// Calculates the week within a Gregorian year [0..53],
// given a day-of-year and day-of-week.
// All days in a year before the year's first Sunday belong to week 0.
fn calc_sundayweek(yd: int, wd: int) int = {
	return (yd + 6 - ((wd + 1) % 7)) / 7;
};

// Calculates the day-of-week, given a epochal day,
// from Monday=0 to Sunday=6.
fn calc_weekday(e: i64) int = {
	const wd = ((e + 3) % 7): int;
	return (wd + 7) % 7;
};

// Calculates the daydate,
// given a year, month, and day-of-month.
fn calc_daydate__ymd(y: int, m: int, d: int) (i64 | invalid) = {
	if (!is_valid_ymd(y, m, d)) {
		return invalid;
	};
	// Algorithm adapted from:
	// https://en.wikipedia.org/wiki/Julian_day
	//
	// TODO: Review, cite, verify, annotate.
	const jdn = (
		(1461 * (y + 4800 + (m - 14) / 12)) / 4
		+ (367 * (m - 2 - 12 * ((m - 14) / 12))) / 12
		- (3 * ((y + 4900 + (m - 14) / 12) / 100)) / 4
		+ d
		- 32075
	);
	const e = jdn + EPOCHAL_JULIAN;
	return e;
};

// Calculates the daydate,
// given a year, week, and day-of-week.
fn calc_daydate__ywd(y: int, w: int, wd: int) (i64 | invalid) = {
	const jan1wd = calc_janfirstweekday(y);
	const yd = wd - jan1wd + 7 * w;
	return calc_daydate__yd(y, yd)?;
};

// Calculates the daydate,
// given a year and day-of-year.
fn calc_daydate__yd(y: int, yd: int) (i64 | invalid) = {
	if (yd < 1 || yd > calc_year_daycnt(y)) {
		return invalid;
	};
	return calc_daydate__ymd(y, 1, 1)? + yd - 1;
};

@test fn calc_daydate__ymd() void = {
	const cases = [
		(( -768,  2,  5),  -999999, false),
		((   -1, 12, 31),  -719529, false),
		((    0,  1,  1),  -719528, false),
		((    0,  1,  2),  -719527, false),
		((    0, 12, 31),  -719163, false),
		((    1,  1,  1),  -719162, false),
		((    1,  1,  2),  -719161, false),
		(( 1965,  3, 23),    -1745, false),
		(( 1969, 12, 31),       -1, false),
		(( 1970,  1,  1),        0, false),
		(( 1970,  1,  2),        1, false),
		(( 1999, 12, 31),    10956, false),
		(( 2000,  1,  1),    10957, false),
		(( 2000,  1,  2),    10958, false),
		(( 2038,  1, 18),    24854, false),
		(( 2038,  1, 19),    24855, false),
		(( 2038,  1, 20),    24856, false),
		(( 2243, 10, 17),   100000, false),
		(( 4707, 11, 28),   999999, false),
		(( 4707, 11, 29),  1000000, false),
		((29349,  1, 25),  9999999, false),

		(( 1970,-99,-99),  0, true),
		(( 1970, -9, -9),  0, true),
		(( 1970, -1, -1),  0, true),
		(( 1970,  0,  0),  0, true),
		(( 1970,  0,  1),  0, true),
		(( 1970,  1, 99),  0, true),
		(( 1970, 99, 99),  0, true),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const params = cases[i].0;
		const expect = cases[i].1;
		const should_error = cases[i].2;
		const actual = calc_daydate__ymd(
			params.0, params.1, params.2,
		);

		if (should_error) {
			assert(actual is invalid, "invalid date accepted");
		} else {
			assert(actual is i64, "valid date not accepted");
			assert(actual as i64 == expect, "date miscalculation");
		};
	};
};

@test fn calc_daydate__ywd() void = {
	const cases = [
		(( -768,  0, 4), -1000034),
		(( -768,  5, 4), -999999),
		((   -1, 52, 5), -719529),
		((    0,  0, 6), -719528),
		((    0,  0, 7), -719527),
		((    0, 52, 7), -719163),
		((    1,  0, 1), -719162),
		((    1,  0, 2), -719161),
		(( 1965, 12, 2), -1745),
		(( 1969, 52, 3), -1),
		(( 1970,  0, 4), 0),
		(( 1970,  0, 5), 1),
		(( 1999, 52, 5), 10956),
		(( 2000,  0, 6), 10957),
		(( 2000,  0, 7), 10958),
		(( 2020,  0, 3), 18262),
		(( 2022,  9, 1), 19051),
		(( 2022,  9, 2), 19052),
		(( 2023, 51, 7), 19715),
		(( 2024,  8, 3), 19781),
		(( 2024,  8, 4), 19782),
		(( 2024,  8, 5), 19783),
		(( 2024, 49, 4), 20069),
		(( 2024, 52, 2), 20088),
		(( 2038,  3, 1), 24854),
		(( 2038,  3, 2), 24855),
		(( 2038,  3, 3), 24856),
		(( 2243, 41, 2), 99993),
		(( 4707, 47, 4), 999999),
		(( 4707, 47, 5), 1000000),
		((29349,  3, 6), 9999999),
	];

	for (let i = 0z; i < len(cases); i += 1) {
		const ywd = cases[i].0;
		const expected = cases[i].1;
		const actual = calc_daydate__ywd(ywd.0, ywd.1, ywd.2)!;
		assert(actual == expected,
			"incorrect calc_daydate__ywd() result");
	};
};

@test fn calc_daydate__yd() void = {
	const cases = [
		( -768, 36,  -999999),
		(   -1, 365, -719529),
		(    0, 1,   -719528),
		(    0, 2,   -719527),
		(    0, 366, -719163),
		(    1, 1,   -719162),
		(    1, 2,   -719161),
		( 1965, 82,  -1745  ),
		( 1969, 365, -1     ),
		( 1970, 1,   0      ),
		( 1970, 2,   1      ),
		( 1999, 365, 10956  ),
		( 2000, 1,   10957  ),
		( 2000, 2,   10958  ),
		( 2038, 18,  24854  ),
		( 2038, 19,  24855  ),
		( 2038, 20,  24856  ),
		( 2243, 290, 100000 ),
		( 4707, 332, 999999 ),
		( 4707, 333, 1000000),
		(29349, 25,  9999999),
	];

	for (let i = 0z; i < len(cases); i += 1) {
		const y = cases[i].0;
		const yd = cases[i].1;
		const expected = cases[i].2;
		const actual = calc_daydate__yd(y, yd)!;
		assert(expected == actual,
			"error in date calculation from yd");
	};
	assert(calc_daydate__yd(2020, 0) is invalid,
		"calc_daydate__yd() did not reject invalid yearday");
	assert(calc_daydate__yd(2020, 400) is invalid,
		"calc_daydate__yd() did not reject invalid yearday");
};

@test fn calc_ymd() void = {
	const cases = [
		(-999999, ( -768,  2,  5)),
		(-719529, (   -1, 12, 31)),
		(-719528, (    0,  1,  1)),
		(-719527, (    0,  1,  2)),
		(-719163, (    0, 12, 31)),
		(-719162, (    1,  1,  1)),
		(-719161, (    1,  1,  2)),
		(  -1745, ( 1965,  3, 23)),
		(     -1, ( 1969, 12, 31)),
		(      0, ( 1970,  1,  1)),
		(      1, ( 1970,  1,  2)),
		(  10956, ( 1999, 12, 31)),
		(  10957, ( 2000,  1,  1)),
		(  10958, ( 2000,  1,  2)),
		(  24854, ( 2038,  1, 18)),
		(  24855, ( 2038,  1, 19)),
		(  24856, ( 2038,  1, 20)),
		( 100000, ( 2243, 10, 17)),
		( 999999, ( 4707, 11, 28)),
		(1000000, ( 4707, 11, 29)),
		(9999999, (29349,  1, 25)),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const paramt = cases[i].0;
		const expect = cases[i].1;
		const actual = calc_ymd(paramt);
		assert(expect.0 == actual.0, "year mismatch");
		assert(expect.1 == actual.1, "month mismatch");
		assert(expect.2 == actual.2, "day mismatch");
	};
};

@test fn calc_yearday() void = {
	const cases = [
		(( -768,  2,  5),  36),
		((   -1, 12, 31), 365),
		((    0,  1,  1),   1),
		((    0,  1,  2),   2),
		((    0, 12, 31), 366),
		((    1,  1,  1),   1),
		((    1,  1,  2),   2),
		(( 1965,  3, 23),  82),
		(( 1969, 12, 31), 365),
		(( 1970,  1,  1),   1),
		(( 1970,  1,  2),   2),
		(( 1999, 12, 31), 365),
		(( 2000,  1,  1),   1),
		(( 2000,  1,  2),   2),
		(( 2020,  2, 12),  43),
		(( 2038,  1, 18),  18),
		(( 2038,  1, 19),  19),
		(( 2038,  1, 20),  20),
		(( 2243, 10, 17), 290),
		(( 4707, 11, 28), 332),
		(( 4707, 11, 29), 333),
		((29349,  1, 25),  25),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const params = cases[i].0;
		const expect = cases[i].1;
		const actual = calc_yearday(params.0, params.1, params.2);
		assert(expect == actual, "yearday miscalculation");
	};
};

@test fn calc_week() void = {
	const cases = [
		((  1, 0),  1),
		((  1, 1),  0),
		((  1, 2),  0),
		((  1, 3),  0),
		((  1, 4),  0),
		((  1, 5),  0),
		((  1, 6),  0),
		(( 21, 1),  3),
		(( 61, 2),  9),
		((193, 4), 27),
		((229, 0), 33),
		((286, 3), 41),
		((341, 6), 48),
		((365, 5), 52),
		((366, 0), 53),
	];

	for (let i = 0z; i < len(cases); i += 1) {
		const params = cases[i].0;
		const expect = cases[i].1;
		const actual = calc_week(params.0, params.1);
		assert(expect == actual, "week miscalculation");
	};
};

@test fn calc_sundayweek() void = {
	const cases = [
		((  1, 0),  0),
		((  1, 1),  0),
		((  1, 2),  0),
		((  1, 3),  0),
		((  1, 4),  0),
		((  1, 5),  0),
		((  1, 6),  1),
		(( 21, 1),  3),
		(( 61, 2),  9),
		((193, 4), 27),
		((229, 0), 33),
		((286, 3), 41),
		((341, 6), 49),
		((365, 5), 52),
		((366, 0), 53),
	];

	for (let i = 0z; i < len(cases); i += 1) {
		const params = cases[i].0;
		const expect = cases[i].1;
		const actual = calc_sundayweek(params.0, params.1);
		assert(expect == actual, "week miscalculation");
	};
};

@test fn calc_weekday() void = {
	const cases = [
		(-999999, 3), // -0768-02-05
		(-719529, 4), // -0001-12-31
		(-719528, 5), //  0000-01-01
		(-719527, 6), //  0000-01-02
		(-719163, 6), //  0000-12-31
		(-719162, 0), //  0001-01-01
		(-719161, 1), //  0001-01-02
		(  -1745, 1), //  1965-03-23
		(     -1, 2), //  1969-12-31
		(      0, 3), //  1970-01-01
		(      1, 4), //  1970-01-02
		(  10956, 4), //  1999-12-31
		(  10957, 5), //  2000-01-01
		(  10958, 6), //  2000-01-02
		(  24854, 0), //  2038-01-18
		(  24855, 1), //  2038-01-19
		(  24856, 2), //  2038-01-20
		( 100000, 1), //  2243-10-17
		( 999999, 3), //  4707-11-28
		(1000000, 4), //  4707-11-29
		(9999999, 5), // 29349-01-25
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const paramt = cases[i].0;
		const expect = cases[i].1;
		const actual = calc_weekday(paramt);
		assert(expect == actual, "weekday miscalculation");
	};
};

@test fn calc_janfirstweekday() void = {
	const cases = [
	//	 year   weekday
		(1969,  2),
		(1970,  3),
		(1971,  4),
		(1972,  5),
		(1973,  0),
		(1974,  1),
		(1975,  2),
		(1976,  3),
		(1977,  5),
		(1978,  6),
		(1979,  0),
		(1980,  1),
		(1981,  3),
		(1982,  4),
		(1983,  5),
		(1984,  6),
		(1985,  1),
		(1986,  2),
		(1987,  3),
		(1988,  4),
		(1989,  6),
		(1990,  0),
		(1991,  1),
		(1992,  2),
		(1993,  4),
		(1994,  5),
		(1995,  6),
		(1996,  0),
		(1997,  2),
		(1998,  3),
		(1999,  4),
		(2000,  5),
		(2001,  0),
		(2002,  1),
		(2003,  2),
		(2004,  3),
		(2005,  5),
		(2006,  6),
		(2007,  0),
		(2008,  1),
		(2009,  3),
		(2010,  4),
		(2011,  5),
		(2012,  6),
		(2013,  1),
		(2014,  2),
		(2015,  3),
		(2016,  4),
		(2017,  6),
		(2018,  0),
		(2019,  1),
		(2020,  2),
		(2021,  4),
		(2022,  5),
		(2023,  6),
		(2024,  0),
		(2025,  2),
		(2026,  3),
		(2027,  4),
		(2028,  5),
		(2029,  0),
		(2030,  1),
		(2031,  2),
		(2032,  3),
		(2033,  5),
		(2034,  6),
		(2035,  0),
		(2036,  1),
		(2037,  3),
		(2038,  4),
		(2039,  5),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const paramt = cases[i].0;
		const expect = cases[i].1;
		const actual = calc_janfirstweekday(paramt);
		assert(expect == actual, "calc_janfirstweekday() miscalculation");
	};
};
diff --git a/time/date/daytime.ha b/time/date/daytime.ha
new file mode 100644
index 0000000..b5b2828
--- /dev/null
+++ b/time/date/daytime.ha
@@ -0,0 +1,32 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use errors;
use time;

// Calculates the wall clock (hour, minute, second, nanosecond),
// given a time-of-day (amount of daytime progressed in a day).
fn calc_hmsn(t: time::duration) (int, int, int, int) = {
	// TODO: Special case for leap seconds, 61st second?
	const hour = (t / time::HOUR): int;
	const min = ((t / time::MINUTE) % 60): int;
	const sec = ((t / time::SECOND) % 60): int;
	const nsec = (t % time::SECOND): int;
	return (hour, min, sec, nsec);
};

// Calculates the time-of-day (amount of daytime progressed in a day),
// given a wall clock (hour, minute, second, nanosecond).
fn calc_daytime__hmsn(
	hour: int,
	min: int,
	sec: int,
	nsec: int,
) (time::duration | invalid) = {
	const t = (
		(hour * time::HOUR) +
		(min * time::MINUTE) +
		(sec * time::SECOND) +
		(nsec * time::NANOSECOND)
	);
	return t;
};
diff --git a/time/date/error.ha b/time/date/error.ha
new file mode 100644
index 0000000..78ee7cd
--- /dev/null
+++ b/time/date/error.ha
@@ -0,0 +1,18 @@
// License: MPL-2.0
// (c) 2023 Byron Torres <b@torresjrjr.com>

// All possible errors returned from [[date]].
export type error = !(insufficient | invalid | parsefail);

// Converts an [[error]] into a human-friendly string.
export fn strerror(err: error) const str = {
	match (err) {
	case insufficient =>
		return "Insufficient date information";
	case invalid =>
		return "Invalid date information";
	case let rn: parsefail =>
		// TODO: use rune 'rn' here
		return "Date parsing error";
	};
};
diff --git a/time/date/format.ha b/time/date/format.ha
new file mode 100644
index 0000000..b5dee19
--- /dev/null
+++ b/time/date/format.ha
@@ -0,0 +1,296 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
// (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net>
use ascii;
use errors;
use fmt;
use io;
use memio;
use strings;
use time;
use time::chrono;

// [[format]] layout for the email date format.
export def EMAIL: str = "%a, %d %b %Y %H:%M:%S %z";

// [[format]] layout for the email date format, with zone offset and
// zone abbreviation.
export def EMAILZ: str = "%a, %d %b %Y %H:%M:%S %z %Z";

// [[format]] layout for the POSIX locale's default date & time representation.
export def POSIX: str = "%a %b %e %H:%M:%S %Y";

// [[format]] layout compatible with RFC 3339.
export def RFC3339: str = "%Y-%m-%dT%H:%M:%S%z";

// [[format]] layout for a simple timestamp.
export def STAMP: str = "%Y-%m-%d %H:%M:%S";

// [[format]] layout for a simple timestamp with nanoseconds.
export def STAMP_NANO: str = "%Y-%m-%d %H:%M:%S.%N";

// [[format]] layout for a simple timestamp with nanoseconds and zone
// offset.
export def STAMP_ZOFF: str = "%Y-%m-%d %H:%M:%S.%N %z";

// [[format]] layout for a simple timestamp with nanoseconds,
// zone offset, zone abbreviation, and locality.
export def STAMP_NOZL: str = "%Y-%m-%d %H:%M:%S.%N %z %Z %L";

def WEEKDAYS: [_]str = [
	"Monday",
	"Tuesday",
	"Wednesday",
	"Thursday",
	"Friday",
	"Saturday",
	"Sunday",
];

def WEEKDAYS_SHORT: [_]str = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

def MONTHS: [_]str = [
	"January",
	"February",
	"March",
	"April",
	"May",
	"June",
	"July",
	"August",
	"September",
	"October",
	"November",
	"December",
];

def MONTHS_SHORT: [_]str = [
	"Jan", "Feb", "Mar",
	"Apr", "May", "Jun",
	"Jul", "Aug", "Sep",
	"Oct", "Nov", "Dec",
];

// TODO: Make format() accept parameters of type (date | period), using the
// "intervals" standard representation provided by ISO 8601?
//
// See https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
//
// Ticket: https://todo.sr.ht/~sircmpwn/hare/650

// Formats a [[date]] and writes it into a caller supplied buffer.
// The returned string is borrowed from this buffer.
export fn bsformat(
	buf: []u8,
	layout: str,
	d: *date,
) (str | io::error) = {
	let sink = memio::fixed(buf);
	format(&sink, layout, d)?;
	return memio::string(&sink)!;
};

// Formats a [[date]] and writes it into a heap-allocated string.
// The caller must free the return value.
export fn asformat(layout: str, d: *date) (str | io::error) = {
	let sink = memio::dynamic();
	format(&sink, layout, d)?;
	return memio::string(&sink)!;
};

fn fmtout(out: io::handle, r: rune, d: *date) (size | io::error) = {
	switch (r) {
	case 'a' =>
		return fmt::fprint(out, WEEKDAYS_SHORT[_weekday(d)]);
	case 'A' =>
		return fmt::fprint(out, WEEKDAYS[_weekday(d)]);
	case 'b' =>
		return fmt::fprint(out, MONTHS_SHORT[_month(d) - 1]);
	case 'B' =>
		return fmt::fprint(out, MONTHS[_month(d) - 1]);
	case 'd' =>
		return fmt::fprintf(out, "{:02}", _day(d));
	case 'e' =>
		return fmt::fprintf(out, "{: 2}", _day(d));
	case 'F' =>
		return fmt::fprintf(out, "{:04}-{:02}-{:02}", _year(d), _month(d), _day(d));
	case 'H' =>
		return fmt::fprintf(out, "{:02}", _hour(d));
	case 'I' =>
		return fmt::fprintf(out, "{:02}", (_hour(d) + 11) % 12 + 1);
	case 'j' =>
		return fmt::fprintf(out, "{:03}", _yearday(d));
	case 'L' =>
		return fmt::fprint(out, d.loc.name);
	case 'm' =>
		return fmt::fprintf(out, "{:02}", _month(d));
	case 'M' =>
		return fmt::fprintf(out, "{:02}", _minute(d));
	case 'N' =>
		return fmt::fprintf(out, "{:09}", _nanosecond(d));
	case 'p' =>
		return fmt::fprint(out, if (_hour(d) < 12) "AM" else "PM");
	case 's' =>
		return fmt::fprintf(out, "{:02}", time::unix(*(d: *time::instant)));
	case 'S' =>
		return fmt::fprintf(out, "{:02}", _second(d));
	case 'T' =>
		return fmt::fprintf(out, "{:02}:{:02}:{:02}", _hour(d), _minute(d), _second(d));
	case 'u' =>
		return fmt::fprintf(out, "{}", _weekday(d) + 1);
	case 'U' =>
		return fmt::fprintf(out, "{:02}", _sundayweek(d));
	case 'w' =>
		return fmt::fprintf(out, "{}", (_weekday(d) + 1) % 7);
	case 'W' =>
		return fmt::fprintf(out, "{:02}", _week(d));
	case 'y' =>
		return fmt::fprintf(out, "{:02}", _year(d) % 100);
	case 'Y' =>
		return fmt::fprintf(out, "{:04}", _year(d));
	case 'z' =>
		const (sign, zo) = if (chrono::mzone(d).zoff >= 0) {
			yield ('+', calc_hmsn(chrono::mzone(d).zoff));
		} else {
			yield ('-', calc_hmsn(-chrono::mzone(d).zoff));
		};
		const (hr, mi) = (zo.0, zo.1);
		return fmt::fprintf(out, "{}{:02}{:02}", sign, hr, mi);
	case 'Z' =>
		return fmt::fprint(out, chrono::mzone(d).abbr);
	case '%' =>
		return fmt::fprint(out, "%");
	case =>
		abort("Invalid format string provided to time::date::format");
	};
};

// Formats a [[date]] according to a layout and writes to an [[io::handle]].
//
// The layout may contain any of the following format specifiers listed below.
// Implemented are a subset of the POSIX strftime(3) format specifiers, as well
// as some others. Use of unimplemented specifiers or an otherwise invalid
// layout will cause an abort.
//
// 	%% -- A literal '%' character.
// 	%a -- The abbreviated name of the day of the week.
// 	%A -- The full name of the day of the week.
// 	%b -- The abbreviated name of the month.
// 	%B -- The full name of the month.
// 	%d -- The day of the month (decimal, range 01 to 31).
// 	%e -- The day of the month (decimal, range  1 to 31), left-padded space.
// 	%F -- The full date, equivalent to %Y-%m-%d
// 	%H -- The hour of the day as from a 24-hour clock (range 00 to 23).
// 	%I -- The hour of the day as from a 12-hour clock (range 01 to 12).
// 	%j -- The ordinal day of the year (range 001 to 366).
// 	%L -- The locality's name (the timezone's identifier).
// 	%m -- The month (decimal, range 01 to 12).
// 	%M -- The minute (decimal, range 00 to 59).
// 	%N -- The nanosecond of the second (range 000000000 to 999999999).
// 	%p -- Either "AM" or "PM" according to the current time.
// 	      "AM" includes midnight, and "PM" includes noon.
// 	%s -- Number of seconds since 1970-01-01 00:00:00, the Unix epoch
// 	%S -- The second of the minute (range 00 to 60).
// 	%T -- The full time, equivalent to %H:%M:%S
// 	%u -- The day of the week (decimal, range 1 to 7). 1 represents Monday.
// 	%U -- The week number of the current year (range 00 to 53),
// 	      starting with the first Sunday as the first day of week 01.
// 	%w -- The day of the week (decimal, range 0 to 6). 0 represents Sunday.
// 	%W -- The week number of the current year (range 00 to 53),
// 	      starting with the first Monday as the first day of week 01.
// 	%y -- The year without the century digits (range 00 to 99).
// 	%Y -- The year.
// 	%z -- The observed zone offset.
// 	%Z -- The observed zone abbreviation.
//
export fn format(
	h: io::handle,
	layout: str,
	d: *date
) (size | io::error) = {
	const iter = strings::iter(layout);
	let escaped = false;
	let n = 0z;
	for (true) {
		let r: rune = match (strings::next(&iter)) {
		case void =>
			break;
		case let r: rune =>
			yield r;
		};

		if (escaped) {
			escaped = false;
			n += fmtout(h, r, d)?;
		} else {
			if (r == '%') {
				escaped = true;
			} else {
				memio::appendrune(h, r)?;
			};
		};
	};
	return n;
};

@test fn format() void = {
	const d = new(chrono::UTC, 0, 1994, 1, 1, 2, 17, 5, 24)!;

	const cases = [
		// special characters
		("%%", "%"),
		// hour
		("%H", "02"),
		("%I", "02"),
		// minute
		("%M", "17"),
		// second
		("%S", "05"),
		// nanosecond
		("%N", "000000024"),
		// am/pm
		("%p", "AM"),
		// day
		("%d", "01"),
		// day
		("%e", " 1"),
		// month
		("%m", "01"),
		// year
		("%Y", "1994"),
		("%y", "94"),
		// month name
		("%b", "Jan"),
		("%B", "January"),
		// weekday
		("%u", "6"),
		("%w", "6"),
		("%a", "Sat"),
		("%A", "Saturday"),
		// yearday
		("%j", "001"),
		// week
		("%W", "00"),
		// full date
		("%F", "1994-01-01"),
		// full time
		("%T", "02:17:05"),
		// Unix timestamp
		("%s", "757390625"),
	];

	for (let i = 0z; i < len(cases); i += 1) {
		const layout = cases[i].0;
		const expected = cases[i].1;
		const actual = asformat(layout, &d)!;
		defer free(actual);
		if (actual != expected) {
			fmt::printfln(
				"expected format({}, &d) to be {} but was {}",
				layout, expected, actual
			)!;
			abort();
		};
	};
};
diff --git a/time/date/locality.ha b/time/date/locality.ha
new file mode 100644
index 0000000..eb7ea52
--- /dev/null
+++ b/time/date/locality.ha
@@ -0,0 +1,13 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
use time;
use time::chrono;

// Creates an equivalent [[date]] with a different
// [[time::chrono::locality]].
//
// The [[time::chrono::discontinuity]] rules from [[time::chrono::in]] apply here.
export fn in(loc: chrono::locality, d: date) (date | chrono::discontinuity) = {
	return from_moment(chrono::in(loc, *(&d: *chrono::moment))?);
};
diff --git a/time/date/observe.ha b/time/date/observe.ha
new file mode 100644
index 0000000..4da2fb0
--- /dev/null
+++ b/time/date/observe.ha
@@ -0,0 +1,251 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
use errors;
use time;
use time::chrono;

// These functions are renamed to avoid namespace conflicts, like in the
// parameters of the [[new]] function.

// Observes a [[date]]'s era.
export fn era(d: *date) int = _era(d);

// Observes a [[date]]'s year.
export fn year(d: *date) int = _year(d);

// Observes a [[date]]'s month of the year.
export fn month(d: *date) int = _month(d);

// Observes a [[date]]'s day of the month.
export fn day(d: *date) int = _day(d);

// Observes a [[date]]'s day of the week; Monday=0 to Sunday=6.
export fn weekday(d: *date) int = _weekday(d);

// Observes a [[date]]'s ordinal day of the year.
export fn yearday(d: *date) int = _yearday(d);

// Observes a [[date]]'s ISO week-numbering year.
export fn isoweekyear(d: *date) int = _isoweekyear(d);

// Observes a [[date]]'s Gregorian week starting Monday.
export fn week(d: *date) int = _week(d);

// Observes a [[date]]'s Gregorian week starting Sunday.
export fn sundayweek(d: *date) int = _sundayweek(d);

// Observes a [[date]]'s ISO week.
export fn isoweek(d: *date) int = _isoweek(d);

// Observes a [[date]]'s hour of the day.
export fn hour(d: *date) int = _hour(d);

// Observes a [[date]]'s minute of the hour.
export fn minute(d: *date) int = _minute(d);

// Observes a [[date]]'s second of the minute.
export fn second(d: *date) int = _second(d);

// Observes a [[date]]'s nanosecond of the second.
export fn nanosecond(d: *date) int = _nanosecond(d);

fn _era(d: *date) int = {
	match (d.era) {
	case void =>
		d.era = calc_era(
			_year(d),
		);
		return d.era: int;
	case let a: int =>
		return a;
	};
};

fn _year(d: *date) int = {
	match (d.year) {
	case void =>
		const ymd = calc_ymd(
			chrono::daydate(d),
		);
		d.year = ymd.0;
		d.month = ymd.1;
		d.day = ymd.2;
		return d.year: int;
	case let y: int =>
		return y;
	};
};

fn _month(d: *date) int = {
	match (d.month) {
	case void =>
		const ymd = calc_ymd(
			chrono::daydate(d),
		);
		d.year = ymd.0;
		d.month = ymd.1;
		d.day = ymd.2;
		return d.month: int;
	case let y: int =>
		return y;
	};
};

fn _day(d: *date) int = {
	match (d.day) {
	case void =>
		const ymd = calc_ymd(
			chrono::daydate(d),
		);
		d.year = ymd.0;
		d.month = ymd.1;
		d.day = ymd.2;
		return d.day: int;
	case let y: int =>
		return y;
	};
};

fn _weekday(d: *date) int = {
	match (d.weekday) {
	case void =>
		d.weekday = calc_weekday(
			chrono::daydate(d),
		);
		return d.weekday: int;
	case let y: int =>
		return y;
	};
};

fn _yearday(d: *date) int = {
	match (d.yearday) {
	case void =>
		d.yearday = calc_yearday(
			_year(d),
			_month(d),
			_day(d),
		);
		return d.yearday: int;
	case let yd: int =>
		return yd;
	};
};

fn _isoweekyear(d: *date) int = {
	match (d.isoweekyear) {
	case void =>
		d.isoweekyear = calc_isoweekyear(
			_year(d),
			_month(d),
			_day(d),
			_weekday(d),
		);
		return d.isoweekyear: int;
	case let iwy: int =>
		return iwy;
	};
};

fn _week(d: *date) int = {
	match (d.week) {
	case void =>
		d.week = calc_week(
			_yearday(d),
			_weekday(d),
		);
		return d.week: int;
	case let w: int =>
		return w;
	};
};

fn _sundayweek(d: *date) int = {
	match (d.sundayweek) {
	case void =>
		d.sundayweek = calc_sundayweek(
			_yearday(d),
			_weekday(d),
		);
		return d.sundayweek: int;
	case let w: int =>
		return w;
	};
};

fn _isoweek(d: *date) int = {
	match (d.isoweek) {
	case void =>
		d.isoweek = calc_isoweek(
			_year(d),
			_week(d),
		);
		return d.isoweek: int;
	case let iw: int =>
		return iw;
	};
};

fn _hour(d: *date) int = {
	match (d.hour) {
	case void =>
		const hmsn = calc_hmsn(
			chrono::daytime(d),
		);
		d.hour = hmsn.0;
		d.minute = hmsn.1;
		d.second = hmsn.2;
		d.nanosecond = hmsn.3;
		return d.hour: int;
	case let h: int =>
		return h;
	};
};

fn _minute(d: *date) int = {
	match (d.minute) {
	case void =>
		const hmsn = calc_hmsn(
			chrono::daytime(d),
		);
		d.hour = hmsn.0;
		d.minute = hmsn.1;
		d.second = hmsn.2;
		d.nanosecond = hmsn.3;
		return d.minute: int;
	case let m: int =>
		return m;
	};
};

fn _second(d: *date) int = {
	match (d.second) {
	case void =>
		const hmsn = calc_hmsn(
			chrono::daytime(d),
		);
		d.hour = hmsn.0;
		d.minute = hmsn.1;
		d.second = hmsn.2;
		d.nanosecond = hmsn.3;
		return d.second: int;
	case let s: int =>
		return s;
	};
};

fn _nanosecond(d: *date) int = {
	match (d.nanosecond) {
	case void =>
		const hmsn = calc_hmsn(
			chrono::daytime(d),
		);
		d.hour = hmsn.0;
		d.minute = hmsn.1;
		d.second = hmsn.2;
		d.nanosecond = hmsn.3;
		return d.nanosecond: int;
	case let n: int =>
		return n;
	};
};
diff --git a/time/date/parithm.ha b/time/date/parithm.ha
new file mode 100644
index 0000000..7a7eef8
--- /dev/null
+++ b/time/date/parithm.ha
@@ -0,0 +1,395 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
// (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net>
use fmt;
use time;
use time::chrono;

// The nominal units of the Gregorian chronology. Used for chronological
// arithmetic.
export type unit = enum int {
	ERA,
	YEAR,
	MONTH,
	WEEK,
	DAY,
	HOUR,
	MINUTE,
	SECOND,
	NANOSECOND,
};

// Calculates the [[period]] between two [[date]]s, from A to B.
// The returned period, provided to [[reckon]] along with A, will produce B,
// regardless of the [[calculus]] used. All the period's non-zero fields will
// have the same sign.
export fn pdiff(a: date, b: date) period = {
	let p = period { ... };

	if (chrono::compare(&a, &b) == 0) {
		return p;
	};

	let reverse = if (chrono::compare(&a, &b) > 0) true else false;
	if (reverse) {
		let tmp = a;
		a = b;
		b = tmp;
	};

	p.years = _year(&b) - _year(&a);

	p.months = _month(&b) - _month(&a);
	if (p.months < 0) {
		p.years -= 1;
		p.months += 12;
	};

	p.days = _day(&b) - _day(&a);
	let year = _year(&b);
	let month = _month(&b);
	let daycnt = calc_month_daycnt(year, month);
	for (_day(&a) > daycnt || p.days < 0) {
		month -= 1;
		if (month == 0) {
			year -= 1;
			month = 12;
		};
		daycnt = calc_month_daycnt(year, month);

		p.months -= 1;
		if (p.months < 0) {
			p.years -= 1;
			p.months += 12;
		};
		p.days += daycnt;
	};

	p.hours = _hour(&b) - _hour(&a);
	if (p.hours < 0) {
		p.days -= 1;
		p.hours += 24;
	};

	p.minutes = _minute(&b) - _minute(&a);
	if (p.minutes < 0) {
		p.hours -= 1;
		p.minutes += 60;
	};

	p.seconds = _second(&b) - _second(&a);
	if (p.seconds < 0) {
		p.minutes -= 1;
		p.seconds += 60;
	};

	p.nanoseconds = _nanosecond(&b) - _nanosecond(&a);
	if (p.nanoseconds < 0) {
		p.seconds -= 1;
		p.nanoseconds += 1000000000; // 10E9
	};

	return if (reverse) neg(p) else p;
};

// Calculates the nominal [[unit]] difference between two [[date]]s.
export fn unitdiff(a: date, b: date, u: unit) i64 = {
	switch (u) {
	case unit::ERA =>
		return era(&b) - era(&a);
	case unit::YEAR =>
		return pdiff(a, b).years;
	case unit::MONTH =>
		const d = pdiff(a, b);
		return d.years * 12 + d.months;
	case unit::WEEK =>
		return unitdiff(a, b, unit::DAY) / 7;
	case unit::DAY =>
		return chrono::daydate(&b) - chrono::daydate(&a);
	case unit::HOUR =>
		return unitdiff(a, b, unit::DAY) * 24 + pdiff(a, b).hours;
	case unit::MINUTE =>
		return unitdiff(a, b, unit::HOUR) * 60 + pdiff(a, b).minutes;
	case unit::SECOND =>
		return unitdiff(a, b, unit::MINUTE) * 60 + pdiff(a, b).seconds;
	case unit::NANOSECOND =>
		return unitdiff(a, b, unit::SECOND) * 1000000000 + pdiff(a, b).nanoseconds;
	};
};

// Truncates the given [[date]] at the provided nominal [[unit]].
//
// For example, truncating to the nearest unit::MONTH will set the 'day',
// 'hour', 'minute', 'second', and 'nanosecond' fields to their minimum values.
export fn truncate(d: date, u: unit) date = {
	// TODO: There exist timezones where midnight is invalid on certain
	// days. The new()! calls will fail, but we probably don't want to '?'
	// propagate [[invalid]] to keep this function's use simple. The minimum
	// values (the zeroes and ones here) can't be hardcoded. They need
	// calculation. We should either handle this here; or probably in
	// realize(), and then use realize() here.
	return switch (u) {
	case unit::ERA =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			1, 1, 1,
			0, 0, 0, 0,
		)!;
	case unit::YEAR =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			_year(&d), 1, 1,
			0, 0, 0, 0,
		)!;
	case unit::MONTH =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			_year(&d), _month(&d), 1,
			0, 0, 0, 0,
		)!;
	case unit::WEEK =>
		const dd = chrono::daydate(&d) - _weekday(&d);
		const ymd = calc_ymd(dd);
		yield new(d.loc, chrono::mzone(&d).zoff,
			ymd.0, ymd.1, ymd.2,
			0, 0, 0, 0,
		)!;
	case unit::DAY =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			_year(&d), _month(&d), _day(&d),
			0, 0, 0, 0,
		)!;
	case unit::HOUR =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			_year(&d), _month(&d), _day(&d),
			_hour(&d), 0, 0, 0,
		)!;
	case unit::MINUTE =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			_year(&d), _month(&d), _day(&d),
			_hour(&d), _minute(&d), 0, 0,
		)!;
	case unit::SECOND =>
		yield new(d.loc, chrono::mzone(&d).zoff,
			_year(&d), _month(&d), _day(&d),
			_hour(&d), _minute(&d), _second(&d), 0,
		)!;
	case unit::NANOSECOND =>
		yield d;
	};
};

@test fn pdiff() void = {
	const cases = [
		(
			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2022, 2, 16, 0, 0, 0, 0)!,
			period {
				years = 1,
				months = 1,
				days = 1,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2022, 3, 27, 0, 0, 0, 0)!,
			period {
				years = 1,
				months = 2,
				days = 12,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2022, 3, 14, 0, 0, 0, 0)!,
			period {
				years = 1,
				months = 1,
				days = 27,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2021, 1, 16, 0, 0, 0, 0)!,
			period {
				days = 1,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2021, 1, 15, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2021, 1, 16, 1, 3, 2, 4)!,
			period {
				days = 1,
				hours = 1,
				minutes = 3,
				seconds = 2,
				nanoseconds = 4,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2021, 1, 15, 2, 3, 2, 2)!,
			new(chrono::UTC, 0, 2021, 1, 16, 1, 1, 2, 4)!,
			period {
				hours = 22,
				minutes = 58,
				nanoseconds = 2,
				...
			},
		),
		(
			new(chrono::UTC, 0,  500, 1, 1, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 3500, 1, 1, 0, 6, 0, 0)!,
			period {
				years = 3000,
				minutes = 6,
				...
			},
		),
		(
			new(chrono::UTC, 0, -500, 1, 1, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2500, 1, 1, 0, 6, 0, 0)!,
			period {
				years = 3000,
				minutes = 6,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2000, 1, 1, 0, 0, 0, 0)!,
			new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!,
			period {
				minutes = 6,
				nanoseconds = 999999999,
				...
			},
		),
		(
			new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 0, 999999999)!,
			new(chrono::UTC, 0, 2000, 1, 1, 0, 6, 1, 0)!,
			period {
				nanoseconds = 1,
				...
			},
		),
		(
			new(chrono::UTC, 0, -4000, 1, 1, 0, 6, 0, 999999999)!,
			new(chrono::UTC, 0, 4000,  1, 1, 0, 6, 1, 0)!,
			period {
				years = 8000,
				nanoseconds = 1,
				...
			},
		),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const da = cases[i].0;
		const db = cases[i].1;
		const expected = cases[i].2;
		const actual = pdiff(da, db);
		assert(peq(actual, expected), "pdiff miscalculation");
	};
};

@test fn unitdiff() void = {
	const cases = [
		(
			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         2)!,
			new(chrono::UTC, 0,  2022,  1,  5,  13, 53, 30,        20)!,
			(27, 328, 1427, 9993, 239834, 14390073, 863404409i64,
				(863404409i64 * time::SECOND) + 18),
		),
		(
			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         0)!,
			new(chrono::UTC, 0,  1994,  8, 28,  11, 20,  1,         2)!,
			(0, 0, 0, 1, 24, 1440, 86400i64,
				(86400i64 * time::SECOND) + 2),
		),
		(
			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         0)!,
			new(chrono::UTC, 0,  1994,  8, 27,  11, 20,  1,         0)!,
			(0, 0, 0, 0, 0, 0, 0i64, 0i64),
		),
		(
			new(chrono::UTC, 0,  -500,  1,  1,   0, 59,  1,         0)!,
			new(chrono::UTC, 0,  2000,  1,  1,  23,  1,  1,         0)!,
			(2500, 30000, 130443, 913106, 913106 * 24 + 22,
				(913106 * 24 + 22) * 60 + 2,
				((913106 * 24 + 22) * 60 + 2) * 60i64,
				(((913106 * 24 + 22) * 60 + 2) * 60i64 *
					time::SECOND)),
		),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const da = cases[i].0;
		const db = cases[i].1;
		const expected = cases[i].2;
		assert(unitdiff(da, db, unit::YEAR) == expected.0,
			"invalid diff_in_years() result");
		assert(unitdiff(da, db, unit::MONTH) == expected.1,
			"invalid diff_in_months() result");
		assert(unitdiff(da, db, unit::WEEK) == expected.2,
			"invalid diff_in_weeks() result");
		assert(unitdiff(da, db, unit::DAY) == expected.3,
			"invalid diff_in_days() result");
		assert(unitdiff(da, db, unit::HOUR) == expected.4,
			"invalid diff_in_hours() result");
		assert(unitdiff(da, db, unit::MINUTE) == expected.5,
			"invalid diff_in_minutes() result");
		assert(unitdiff(da, db, unit::SECOND) == expected.6,
			"invalid diff_in_seconds() result");
		assert(unitdiff(da, db, unit::NANOSECOND) == expected.7,
			"invalid diff_in_nanoseconds() result");
	};
};

@test fn truncate() void = {
	const d = new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 2)!;

	assert(chrono::eq(
			&truncate(d, unit::ERA),
			&new(chrono::UTC, 0, 1, 1, 1, 0, 0, 0, 0)!)!,
		"invalid truncate() result 01");

	assert(chrono::eq(
			&truncate(d, unit::YEAR),
			&new(chrono::UTC, 0, 1994, 1, 1, 0, 0, 0, 0)!)!,
		"invalid truncate() result 02");

	assert(chrono::eq(
			&truncate(d, unit::MONTH),
			&new(chrono::UTC, 0, 1994, 8, 1, 0, 0, 0, 0)!)!,
		"invalid truncate() result 03");

	assert(chrono::eq(
			&truncate(d, unit::WEEK),
			&new(chrono::UTC, 0, 1994, 8, 22, 0, 0, 0, 0)!)!,
		"invalid truncate() result 04");

	assert(chrono::eq(
			&truncate(d, unit::DAY),
			&new(chrono::UTC, 0, 1994, 8, 27, 0, 0, 0, 0)!)!,
		"invalid truncate() result 05");

	assert(chrono::eq(
			&truncate(d, unit::HOUR),
			&new(chrono::UTC, 0, 1994, 8, 27, 11, 0, 0, 0)!)!,
		"invalid truncate() result 06");

	assert(chrono::eq(
			&truncate(d, unit::MINUTE),
			&new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 0, 0)!)!,
		"invalid truncate() result 07");

	assert(chrono::eq(
			&truncate(d, unit::SECOND),
			&new(chrono::UTC, 0, 1994, 8, 27, 11, 20, 1, 0)!)!,
		"invalid truncate() result 08");

	assert(chrono::eq(
			&truncate(d, unit::NANOSECOND),
			&d)!,
		"invalid truncate() result 09");
};
diff --git a/time/date/parse.ha b/time/date/parse.ha
new file mode 100644
index 0000000..91df4a4
--- /dev/null
+++ b/time/date/parse.ha
@@ -0,0 +1,499 @@
// License: MPL-2.0
// (c) 2021-2022 Byron Torres <b@torresjrjr.com>
// (c) 2022 Drew DeVault <sir@cmpwn.com>
// (c) 2021-2022 Vlad-Stefan Harbuz <vlad@vladh.net>
use ascii;
use errors;
use io;
use strconv;
use strings;
use time;
use time::chrono;

type failure = !void;

// A parsing error occurred. If appropriate, the offending format specifier is
// stored. A null rune represents all other error cases.
export type parsefail = !rune;

// Parses a date/time string into a [[virtual]], according to a layout format
// string with specifiers as documented under [[format]]. Partial, sequential,
// aggregative parsing is possible.
//
// 	date::parse(&v, "%Y-%m-%d",    "2019-12-27");
// 	date::parse(&v, "%H:%M:%S.%N", "22:07:08.000000000");
// 	date::parse(&v, "%z %Z %L",    "+0100 CET Europe/Amsterdam");
//
// Parse will return parsefail, if an invalid format specifier is encountered
// or if given string 's' does not match the layout.
export fn parse(v: *virtual, layout: str, s: str) (void | parsefail) = {
	const liter = strings::iter(layout);
	const siter = strings::iter(s);
	let escaped = false;

	for (true) {
		const lr: rune = match (strings::next(&liter)) {
		case void =>
			break;
		case let lr: rune =>
			yield lr;
		};

		if (!escaped && lr == '%') {
			escaped = true;
			continue;
		};

		if (!escaped) {
			const sr = match (strings::next(&siter)) {
			case void =>
				return '\x00';
			case let sr: rune =>
				yield sr;
			};
			if (sr != lr) {
				return '\x00';
			};
			continue;
		};

		escaped = false;

		match (parse_specifier(v, &siter, lr)) {
		case void => void;
		case failure =>
			return lr;
		};
	};

	return void;
};

fn parse_specifier(
	v: *virtual,
	iter: *strings::iterator,
	lr: rune,
) (void | failure) = {
	switch (lr) {
	case 'a' =>
		v.weekday = scan_for(iter, WEEKDAYS_SHORT...)?;
	case 'A' =>
		v.weekday = scan_for(iter, WEEKDAYS...)?;
	case 'b' =>
		v.month = scan_for(iter, MONTHS_SHORT...)? + 1;
	case 'B' =>
		v.month = scan_for(iter, MONTHS...)? + 1;
	case 'd', 'e' =>
		v.day = scan_int(iter, 2)?;
	case 'F' =>
		v.year = scan_int(iter, 4)?;
		eat_rune(iter, '-')?;
		v.month = scan_int(iter, 2)?;
		eat_rune(iter, '-')?;
		v.day = scan_int(iter, 2)?;
	case 'H' =>
		v.hour = scan_int(iter, 2)?;
	case 'I' =>
		v.halfhour = scan_int(iter, 2)?;
	case 'j' =>
		v.yearday = scan_int(iter, 3)?;
	case 'L' =>
		v.locname = scan_str(iter)?;
	case 'm' =>
		v.month = scan_int(iter, 2)?;
	case 'M' =>
		v.minute = scan_int(iter, 2)?;
	case 'N' =>
		v.nanosecond = scan_decimal(iter, 9)?;
	case 'p' => // AM=false PM=true
		v.ampm = scan_for(iter, "AM", "PM", "am", "pm")? % 2 == 1;
	case 'S' =>
		v.second = scan_int(iter, 2)?;
	case 'T' =>
		v.hour = scan_int(iter, 2)?;
		eat_rune(iter, ':')?;
		v.minute = scan_int(iter, 2)?;
		eat_rune(iter, ':')?;
		v.second = scan_int(iter, 2)?;
	case 'u' =>
		v.weekday = scan_int(iter, 1)? - 1;
	case 'U' =>
		v.week = scan_int(iter, 2)?;
	case 'w' =>
		v.weekday = scan_int(iter, 1)? - 1;
	case 'W' =>
		v.week = scan_int(iter, 2)?;
	case 'Y' =>
		v.year = scan_int(iter, 4)?;
	case 'z' =>
		v.zoff = scan_zo(iter)?;
	case 'Z' =>
		v.zabbr = scan_str(iter)?;
	case '%' =>
		eat_rune(iter, '%')?;
	case =>
		return failure;
	};
};

fn eat_rune(iter: *strings::iterator, needle: rune) (uint | failure) = {
	const rn = match (strings::next(iter)) {
	case void =>
		return failure;
	case let rn: rune =>
		yield rn;
	};
	if (rn == needle) {
		return 1;
	} else {
		strings::prev(iter);
		return 0;
	};
};

// Scans the iterator for a given list of strings.
// Returns the list index of the matched string.
fn scan_for(iter: *strings::iterator, list: str...) (int | failure) = {
	const name = strings::iterstr(iter);
	if (len(name) == 0) {
		return failure;
	};
	for(let i = 0z; i < len(list); i += 1) {
		if (strings::hasprefix(name, list[i])) {
			// Consume name
			for (let j = 0z; j < len(list[i]); j += 1) {
				strings::next(iter);
			};
			return i: int;
		};
	};
	return failure;
};

// Scans the iterator for consecutive numeric digits.
// Left-padded whitespace and zeros are permitted.
// Returns the resulting int.
fn scan_int(iter: *strings::iterator, maxrunes: size) (int | failure) = {
	let start = *iter;
	let startfixed = false;
	for (let i = 0z; i < maxrunes; i += 1) {
		let rn: rune = match (strings::next(iter)) {
		case void =>
			break;
		case let rn: rune =>
			yield rn;
		};
		if (!ascii::isdigit(rn) && rn != ' ') {
			return failure;
		};
		if (!startfixed) {
			if (ascii::isdigit(rn)) {
				startfixed = true;
			} else {
				strings::next(&start);
			};
		};
	};
	match (strconv::stoi(strings::slice(&start, iter))) {
	case let num: int =>
		return num;
	case =>
		return failure;
	};
};

// Scans the iterator for consecutive numeric digits.
// Left-padded whitespace and zeros are NOT permitted.
// The resulting decimal is right-padded with zeros.
fn scan_decimal(iter: *strings::iterator, maxrunes: size) (int | failure) = {
	let start = *iter;
	for (let i = 0z; i < maxrunes; i += 1) {
		let rn: rune = match (strings::next(iter)) {
		case void =>
			break;
		case let rn: rune =>
			yield rn;
		};
		if (!ascii::isdigit(rn)) {
			strings::prev(iter);
			break;
		};
	};
	const s = strings::slice(&start, iter);
	match (strconv::stoi(s)) {
	case let num: int =>
		for (let i = 0z; i < maxrunes - len(s); i += 1) {
			num *= 10;
		};
		return num;
	case =>
		return failure;
	};
};