~sircmpwn/hare-dev

hare: time::chrono/datetime reform v2 SUPERSEDED

This patchset is a rebased v2 of this patchset:

	https://lists.sr.ht/~sircmpwn/hare-dev/patches/32972

with some additional datetime patches, mainly for formatting and
parsing code.

The following commits makes some notable API changes:


> [PATCH hare v2 01/12] time::chrono,datetime: add instant field to

This patch removes the exported to_instant and to_moment functions.
These may be added back later if deemed useful or necessary.


> [PATCH hare v2 02/12] time::chrono,datetime: rename min, sec, nsec

This patch renames the datetime module's min(), sec(), nsec() functions.


> [PATCH hare v2 03/12] time::chrono,datetime: embed instant into
> [PATCH hare v2 04/12] datetime: format: call field functions directly
> [PATCH hare v2 05/12] datetime: use tuple unpacking for %z
> [PATCH hare v2 06/12] datetime: organize parsing util functions
> [PATCH hare v2 07/12] datetime: format: simplify %I (12 hour)
> [PATCH hare v2 08/12] datetime: format: simplify %p (AM/PM)
> [PATCH hare v2 09/12] datetime: format: simplify %y (year of century)
> [PATCH hare v2 10/12] datetime: format: fix %j (ordinal-day/yearday)
> [PATCH hare v2 11/12] datetime: parse: tidy code
> [PATCH hare v2 12/12] datetime: revert %I and %p code
#786543 alpine.yml success
#786544 freebsd.yml success
hare/patches: SUCCESS in 1m31s

[time::chrono/datetime reform][0] v2 from [Byron Torres][1]

[0]: https://lists.sr.ht/~sircmpwn/hare-dev/patches/33225
[1]: mailto:b@torresjrjr.com

✓ #786544 SUCCESS hare/patches/freebsd.yml https://builds.sr.ht/~sircmpwn/job/786544
✓ #786543 SUCCESS hare/patches/alpine.yml  https://builds.sr.ht/~sircmpwn/job/786543
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/hare-dev/patches/33225/mbox | git am -3
Learn more about email & git

[PATCH hare v2 01/12] time::chrono,datetime: add instant field to moment Export this patch

The [[time::chrono::moment]] struct type is modified to:

* Introduce a new `.inst: time::instant` field.

* Change the .date field to a "voidable" field,
  from type `chrono::date` to `(chrono::date | void)`.

* Change the .time field to a "voidable" field,
  from type `time::duration` to `(time::duration | void)`.

* Change the .zone field to a "voidable" field,
  from type `chrono::zone` to `(chrono::zone | void)`.

These changes implicate a few important improvements throughout the
[[time::chrono]] and [[datetime]] modules.

By definition, the moment type is now always absolutely precise with
respect to a timescale. Previously, this was mostly the case in theory,
but juggling moment.{date,time} values was not safe and caused needless
aborts. This property nicely extends to the datetime::datetime type.

All the nice properties of time::instant, like comparisons, arithmetics,
and its ubiquitousness throughout the stdlib, are carried over to the
moment type and are more accessible. The to_instant() functions are no
longer needed.

On a similar note, this commit takes advantage of the embedded nature of
moments and datetimes, removing the datetime::to_moment() function, and
encouraging the use of pointers throughout, and thus interoperability
for third-party modules.

	assert(*(&dt: *chrono::moment) is chrono::moment);

The moment.{date,time} fields no longer represent normalized values (for
example, the UTC date & time of a moment in PST). They now represent
observed values, translated by the moment's observed zone offset, like
the datetime::datetime fields. This turns out to be much more useful.

Thus, the elusive transform() functions are no longer needed, which
returned (dangerous) invalid moments which had to be handled with care.
This helps a lot with datetime's field functions, which required a
carefully written web of transform() function calls. The datetime::new()
function is simplified a bit too.

Thus, the [[datetime]] module (and other modules which implement a
chronology with their own temporal type) can now normalize moments with
greater ease, which is an important and frequent calculation. This
allows datetime arithmetic to be simplified a lot.

The moment.{date,time,zone} fields are now cached for efficiency.

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/arithmetic.ha    |  83 +++--------------------
 datetime/chronology.ha    |  27 +++-----
 datetime/datetime.ha      | 111 +++++++++++++-----------------
 datetime/format.ha        |   8 +--
 datetime/parse.ha         |   9 +--
 datetime/timezone.ha      |  17 +----
 time/chrono/chronology.ha | 137 ++++++++++++++++++++++++++++----------
 time/chrono/timezone.ha   |  35 +++-------
 8 files changed, 183 insertions(+), 244 deletions(-)

diff --git a/datetime/arithmetic.ha b/datetime/arithmetic.ha
index f2942041..d114bc07 100644
--- a/datetime/arithmetic.ha
+++ b/datetime/arithmetic.ha
@@ -53,7 +53,7 @@ export type unit = enum int {
// Equivalence means they represent the same moment in time, regardless of their
// locality or observed chronological values.
export fn eq(a: datetime, b: datetime) bool = {
	return a.date == b.date && a.time == b.time;
	return time::compare(a.inst, b.inst) == 0;
};

// Returns true if [[datetime]] "a" succeeds [[datetime]] "b".
@@ -61,8 +61,7 @@ export fn eq(a: datetime, b: datetime) bool = {
// Temporal order is evaluated in a universal frame of reference, regardless of
// their locality or observed chronological values.
export fn after(a: datetime, b: datetime) bool = {
	return !eq(a, b) &&
		(a.date > b.date || a.date == b.date && a.time > b.time);
	return time::compare(a.inst, b.inst) == +1;
};

// Returns true if [[datetime]] "a" precedes [[datetime]] "b".
@@ -70,7 +69,7 @@ export fn after(a: datetime, b: datetime) bool = {
// Temporal order is evaluated in a universal frame of reference, regardless of
// their locality or observed chronological values.
export fn before(a: datetime, b: datetime) bool = {
	return !eq(a, b) && !after(a, b);
	return time::compare(a.inst, b.inst) == -1;
};

// Calculates the [[period]] between two [[datetime]]s.
@@ -148,7 +147,7 @@ export fn unitdiff(a: datetime, b: datetime, u: unit) i64 = {
	case unit::WEEK =>
		yield unitdiff(a, b, unit::DAY) / 7;
	case unit::DAY =>
		yield math::absi(a.date - b.date): int;
		yield math::absi(chrono::getdate(&a) - chrono::getdate(&b)): int;
	case unit::HOUR =>
		const full_diff = diff(a, b);
		yield (unitdiff(a, b, unit::DAY) * 24) + full_diff.hours;
@@ -202,7 +201,7 @@ export fn truncate(dt: datetime, u: unit) datetime = {
			00, 00, 00, 0,
		)!;
	case unit::WEEK =>
		const date = dt.date - (weekday(&dt) - 1);
		const date = chrono::getdate(&dt) - (weekday(&dt) - 1);
		const ymd = calc_ymd(date);
		yield new(dt.loc, 0,
			ymd.0, ymd.1, ymd.2,
@@ -322,7 +321,7 @@ export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
	for (let i = 0z; i < len(pp); i += 1) {
		const p = pp[i];

		let latest_date = dt.date;
		let latest_date = chrono::getdate(&dt);

		if (p.years != 0) {
			d_year += p.years;
@@ -346,8 +345,7 @@ export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
		if (p.weeks != 0) {
			p.days += p.weeks * 7;
		};
		latest_date = calc_date_from_ymd(
			d_year, d_month, d_day)!;
		latest_date = calc_date_from_ymd(d_year, d_month, d_day)!;
		if (p.days != 0) {
			const new_ymd = calc_ymd(latest_date + p.days);
			d_year = new_ymd.0;
@@ -376,7 +374,7 @@ export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
				p.nanoseconds %= ns_in_day;
			};

			let new_time = dt.time + p.nanoseconds;
			let new_time = chrono::gettime(&dt) + p.nanoseconds;

			if (new_time >= ns_in_day) {
				overflowed_days += 1;
@@ -387,8 +385,7 @@ export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
			};

			if (overflowed_days != 0) {
				const new_date = latest_date +
					overflowed_days;
				const new_date = latest_date + overflowed_days;
				const new_ymd = calc_ymd(new_date);
				d_year = new_ymd.0;
				d_month = new_ymd.1;
@@ -432,68 +429,6 @@ export fn sub(dt: datetime, flag: calculus, pp: period...) datetime = {
	return add(dt, flag, pp...);
};

@test fn eq() void = {
	const dt = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
	const cases = [
		((-768, 01, 01, 03, 14, 07, 0), false),
		((1, 1, 01, 14, 00, 00, 1234), false),
		((2022, 02, 04, 03, 14, 07, 0), true),
		((2022, 02, 04, 03, 14, 07, 1), false),
		((2038, 01, 19, 03, 14, 07, 0), false),
		((5555, 05, 05, 05, 55, 55, 5555), false),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const c = cases[i].0;
		const expected = cases[i].1;
		const case_dt = new(chrono::UTC, 0,
			c.0, c.1, c.2, c.3, c.4, c.5, c.6)!;
		assert(eq(dt, case_dt) == expected,
			"equality comparison failed");
	};
};

@test fn after() void = {
	const dt = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
	const cases = [
		((-768, 01, 01, 03, 14, 07, 0), false),
		((1, 1, 01, 14, 00, 00, 1234), false),
		((2020, 02, 04, 03, 14, 07, 1), false),
		((2022, 02, 04, 03, 14, 07, 0), false),
		((2022, 02, 04, 04, 01, 01, 0), true),
		((2038, 01, 19, 03, 14, 07, 0), true),
		((5555, 05, 05, 05, 55, 55, 5555), true),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const c = cases[i].0;
		const expected = cases[i].1;
		const case_dt = new(chrono::UTC, 0,
			c.0, c.1, c.2, c.3, c.4, c.5, c.6)!;
		assert(after(case_dt, dt) == expected,
			"incorrect date ordering in after()");
	};
};

@test fn before() void = {
	const dt = new(chrono::UTC, 0, 2022, 02, 04, 03, 14, 07, 0)!;
	const cases = [
		((-768, 01, 01, 03, 14, 07, 0), true),
		((1, 1, 01, 14, 00, 00, 1234), true),
		((2020, 02, 04, 03, 14, 07, 1), true),
		((2022, 02, 04, 03, 14, 07, 0), false),
		((2022, 02, 04, 04, 01, 01, 0), false),
		((2038, 01, 19, 03, 14, 07, 0), false),
		((5555, 05, 05, 05, 55, 55, 5555), false),
	];
	for (let i = 0z; i < len(cases); i += 1) {
		const c = cases[i].0;
		const expected = cases[i].1;
		const case_dt = new(chrono::UTC, 0,
			c.0, c.1, c.2, c.3, c.4, c.5, c.6)!;
		assert(before(case_dt, dt) == expected,
			"incorrect date ordering in before()");
	};
};

@test fn diff() void = {
	const cases = [
		(
diff --git a/datetime/chronology.ha b/datetime/chronology.ha
index 2c9e558d..e2347350 100644
--- a/datetime/chronology.ha
+++ b/datetime/chronology.ha
@@ -57,8 +57,7 @@ export fn nsec(dt: *datetime) int = _nsec(dt);


fn _epochal(dt: *datetime) chrono::date = {
	const ldt = transform(*dt, dt.zone.zoffset);
	return ldt.date - EPOCHAL_GREGORIAN;
	return chrono::getdate(dt) - EPOCHAL_GREGORIAN;
};

fn _era(dt: *datetime) int = {
@@ -75,10 +74,9 @@ fn _era(dt: *datetime) int = {
};

fn _year(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.year) {
	case void =>
		const ymd = calc_ymd(ldt.date: chrono::date);
		const ymd = calc_ymd(chrono::getdate(dt));
		dt.year = ymd.0;
		dt.month = ymd.1;
		dt.day = ymd.2;
@@ -89,10 +87,9 @@ fn _year(dt: *datetime) int = {
};

fn _month(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.month) {
	case void =>
		const ymd = calc_ymd(ldt.date: chrono::date);
		const ymd = calc_ymd(chrono::getdate(dt));
		dt.year = ymd.0;
		dt.month = ymd.1;
		dt.day = ymd.2;
@@ -103,10 +100,9 @@ fn _month(dt: *datetime) int = {
};

fn _day(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.day) {
	case void =>
		const ymd = calc_ymd(ldt.date: chrono::date);
		const ymd = calc_ymd(chrono::getdate(dt));
		dt.year = ymd.0;
		dt.month = ymd.1;
		dt.day = ymd.2;
@@ -117,10 +113,9 @@ fn _day(dt: *datetime) int = {
};

fn _weekday(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.weekday) {
	case void =>
		dt.weekday = calc_weekday(ldt.date: chrono::date);
		dt.weekday = calc_weekday(chrono::getdate(dt));
		return dt.weekday: int;
	case let y: int =>
		return y;
@@ -235,10 +230,9 @@ fn _isoweek(dt: *datetime) int = {
};

fn _hour(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.hour) {
	case void =>
		const hmsn = calc_hmsn(ldt.time: time::duration);
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
@@ -250,10 +244,9 @@ fn _hour(dt: *datetime) int = {
};

fn _min(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.min) {
	case void =>
		const hmsn = calc_hmsn(ldt.time: time::duration);
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
@@ -265,10 +258,9 @@ fn _min(dt: *datetime) int = {
};

fn _sec(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.sec) {
	case void =>
		const hmsn = calc_hmsn(ldt.time: time::duration);
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
@@ -280,10 +272,9 @@ fn _sec(dt: *datetime) int = {
};

fn _nsec(dt: *datetime) int = {
	const ldt = transform(*dt, dt.zone.zoffset);
	match (dt.nsec) {
	case void =>
		const hmsn = calc_hmsn(ldt.time: time::duration);
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
diff --git a/datetime/datetime.ha b/datetime/datetime.ha
index eb4231c4..8c7331da 100644
--- a/datetime/datetime.ha
+++ b/datetime/datetime.ha
@@ -29,10 +29,11 @@ export type datetime = struct {
};

fn init() datetime = datetime {
	date        = 0,
	time        = 0,
	loc         = chrono::LOCAL,
	zone        = chrono::zone { ... },
	inst        = time::instant { ... },
	date        = void,
	time        = void,
	zone        = void,

	era         = void,
	year        = void,
@@ -63,12 +64,12 @@ fn init() datetime = datetime {
// 	datetime::new(&time::chrono::tz("Europe/Amsterdam"), 1 * time::HOUR,
// 		2038, 01, 19, 02);
//
// 'offs' is the zone offset from the normal timezone (in most cases, UTC). For
// 'zo' is the zone offset from the normal timezone (in most cases, UTC). For
// example, the "Asia/Tokyo" timezone has a single zoffset of +9 hours, but the
// "Australia/Sydney" timezone has zoffsets +10 hours and +11 hours, as they
// observe Daylight Saving Time.
//
// If specified (non-void), 'offs' must match one of the timezone's observed
// If specified (non-void), 'zo' must match one of the timezone's observed
// zoffsets, or will fail. See [[time::chrono::fixedzone]] for custom timezones.
//
// You may omit the zoffset. If the givem timezone has a single zone, [[new]]
@@ -85,7 +86,7 @@ fn init() datetime = datetime {
//   as the clock jumped back 1 hour from 03:00 CEST to 02:00 CET.
export fn new(
	loc: chrono::locality,
	offs: (time::duration | void),
	zo: (time::duration | void),
	fields: int...
) (datetime | invalid) = {
	// TODO:
@@ -93,67 +94,59 @@ export fn new(
	// - Implement as described.
	// - fix calls with `years <= -4715`.
	//   https://todo.sr.ht/~sircmpwn/hare/565
	let defaults: [_]int = [
	let forefields: [_]int = [
		0, 1, 1,    // year month day
		0, 0, 0, 0, // hour min sec nsec
	];

	if (len(fields) > len(defaults)) {
	if (len(fields) > len(forefields)) {
		// cannot specify more than 7 fields
		return invalid;
	};

	for (let i = 0z; i < len(fields); i += 1) {
		defaults[i] = fields[i];
		forefields[i] = fields[i];
	};

	const year  = defaults[0];
	const month = defaults[1];
	const day   = defaults[2];
	const hour  = defaults[3];
	const min   = defaults[4];
	const sec   = defaults[5];
	const nsec  = defaults[6];
	const year  = forefields[0];
	const month = forefields[1];
	const day   = forefields[2];
	const hour  = forefields[3];
	const min   = forefields[4];
	const sec   = forefields[5];
	const nsec  = forefields[6];

	let m = chrono::moment {
		date = calc_date_from_ymd(year, month, day)?,
		time = calc_time_from_hmsn(hour, min, sec, nsec)?,
		loc = loc,
		zone = chrono::zone { ... },
	};
	const mdate = calc_date_from_ymd(year, month, day)?;
	const mtime = calc_time_from_hmsn(hour, min, sec, nsec)?;

	// TODO: Set the correct values according to the given zo and
	// locality/timezone.
	//
	// figuring out what zone this moment observes
	if (offs is time::duration) {
		// Transform inversely to the moment that would transform back
		// to the current moment, then perform a zone lookup.
		m = chrono::transform(m, -(offs as time::duration));
		chrono::lookupzone(&m);
	} else {
		// Just perform a zone lookup, then try that zone and the
		// zones that are observed before and after. This requires
		// knowlegde of the transition index.
		//const z0 = chrono::lookupzone(*m);
		//m = chrono::transform(m, -z0.zoffset);
		abort("TODO"); // TODO
	// create the moment
	const m = match (zo) {
	case let zo: time::duration =>
		yield chrono::from_datetime(loc, zo, mdate, mtime);
	case void =>
		// TODO: Deduce the zone offset
		//
		// perform a zone lookup, then try that zone and the zones that
		// are observed before and after. This requires knowlegde of the
		// transition index.
		abort("TODO: datetime::new(zo=void)");
	};

	const dt = from_moment(m);

	// check if input values are actually observed
	if (
		year == _year(&dt)
		&& month == _month(&dt)
		&& day == _day(&dt)
		&& hour == _hour(&dt)
		&& min == _min(&dt)
		&& sec == _sec(&dt)
		&& nsec == _nsec(&dt)
		year     != _year(&dt)
		|| month != _month(&dt)
		|| day   != _day(&dt)
		|| hour  != _hour(&dt)
		|| min   != _min(&dt)
		|| sec   != _sec(&dt)
		|| nsec  != _nsec(&dt)
	) {
		void;
	} else {
		return invalid;
	};

	return dt;
};

@@ -166,29 +159,25 @@ export fn now() datetime = {
	//
	// https://todo.sr.ht/~sircmpwn/hare/645
	const i = time::now(time::clock::REALTIME);
	const m = chrono::from_instant(i, chrono::LOCAL);
	const m = chrono::new(chrono::LOCAL, i);
	return from_moment(m);
};

// Creates a [[datetime]] from a [[time::chrono::moment]].
export fn from_moment(m: chrono::moment) datetime = {
	const dt = init();
	dt.loc = m.loc;
	dt.inst = m.inst;
	dt.date = m.date;
	dt.time = m.time;
	dt.loc = m.loc;
	dt.zone = m.zone;
	return dt;
};

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

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

// Creates a [[datetime]] from a string, parsed according to a layout,
@@ -200,16 +189,6 @@ export fn from_str(layout: str, s: str) (datetime | insufficient | invalid) = {
	return finish(&b)?;
};

// Creates a [[time::chrono::moment]] from a [[datetime]].
export fn to_moment(dt: datetime) chrono::moment = {
	return chrono::moment {
		date = dt.date,
		time = dt.time,
		loc = dt.loc,
		zone = dt.zone,
	};
};

// A [[builder]] has insufficient information and cannot create a valid datetime.
export type insufficient = !void;

diff --git a/datetime/format.ha b/datetime/format.ha
index c226823b..d9e994eb 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -149,15 +149,15 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'z' =>
		// TODO: test me
		let pm = '+';
		const z = if (dt.zone.zoffset >= 0) {
			yield calc_hmsn(dt.zone.zoffset);
		const z = if (chrono::getzone(dt).zoffset >= 0) {
			yield calc_hmsn(chrono::getzone(dt).zoffset);
		} else {
			pm = '-';
			yield calc_hmsn(-dt.zone.zoffset);
			yield calc_hmsn(-chrono::getzone(dt).zoffset);
		};
		return fmt::fprintf(out, "{}{:02}{:02}", pm, z.0, z.1);
	case 'Z' =>
		return fmt::fprint(out, dt.zone.abbr);
		return fmt::fprint(out, chrono::getzone(dt).abbr);
	case '%' =>
		return fmt::fprint(out, "%");
	case =>
diff --git a/datetime/parse.ha b/datetime/parse.ha
index 060e25ad..dab4f9b2 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -5,6 +5,7 @@
use errors;
use strings;
use time;
use time::chrono;

// Parses a date/time string into a [[builder]], according to a layout format
// string with specifiers as documented under [[format]]. Partial, sequential,
@@ -147,20 +148,20 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
		case 'z' =>
			const rest = strings::iterstr(&s_iter);
			if(strings::hasprefix(rest, 'Z') || strings::hasprefix(rest, 'z')) {
				build.zone.zoffset = 0;
				(build.zone: chrono::zone).zoffset = 0;
			} else {
				const prefix = strings::next(&s_iter);
				build.zone.zoffset = get_max_n_digits(&s_iter, 2)? * time::HOUR;
				(build.zone: chrono::zone).zoffset = get_max_n_digits(&s_iter, 2)? * time::HOUR;

				const rest = strings::iterstr(&s_iter);
				if(strings::hasprefix(rest, ":")) {
					strings::next(&s_iter);
				};

				build.zone.zoffset += get_max_n_digits(&s_iter, 2)? * time::MINUTE;
				(build.zone: chrono::zone).zoffset += get_max_n_digits(&s_iter, 2)? * time::MINUTE;

				if(prefix == '-') {
					build.zone.zoffset *= -1;
					(build.zone: chrono::zone).zoffset *= -1;
				};
			};
		case '%' =>
diff --git a/datetime/timezone.ha b/datetime/timezone.ha
index 34b8fd3f..a1503650 100644
--- a/datetime/timezone.ha
+++ b/datetime/timezone.ha
@@ -7,20 +7,5 @@ use time::chrono;
// Creates an equivalent [[datetime]] with a different
// [[time::chrono::locality]].
export fn in(loc: chrono::locality, dt: datetime) datetime = {
	const old = to_moment(dt);
	const new = chrono::in(loc, old);
	const new_dt = from_moment(new);
	return new_dt;
};

// Finds, sets and returns a [[datetime]]'s currently observed zone.
export fn lookupzone(dt: *datetime) chrono::zone = {
	const m = to_moment(*dt);
	const z = chrono::lookupzone(&m);
	dt.zone = z;
	return z;
};

export fn transform(dt: datetime, zo: time::duration) datetime = {
	return from_moment(chrono::transform(to_moment(dt), zo));
	return from_moment(chrono::in(loc, *(&dt: *chrono::moment)));
};
diff --git a/time/chrono/chronology.ha b/time/chrono/chronology.ha
index eac10e6c..5222152a 100644
--- a/time/chrono/chronology.ha
+++ b/time/chrono/chronology.ha
@@ -5,20 +5,35 @@ use time;
// Invalid [[moment]].
export type invalid = !void;

// A moment in time, within a [[locality]], interpreted via a chronology.
// A moment in time within a [[locality]]. Create one with [[new]].
//
// Moments extend the [[time::instant]] type and couples it with a [[timescale]]
// via the .loc field.
//
// Moments observe a [[date]], time-of-day, and [[zone]], which are evaluated
// and accessed by the [[getdate]], [[gettime]], and [[getzone]] functions.
//
// The [[time::chrono]] modules implements a small chronology of dates & times.
// Higher level modules like [[datetime]] expand upon this with more complex
// chronological values (years, hours, etc.). The [[datetime::datetime]] type
// embeds this type, and other modules implementing other chronologies may
// interoperate by passing pointers.
export type moment = struct {
	// The ordinal day (on Earth or otherwise)
	// since the Hare epoch (zeroth day) 1970-01-01
	date: date,
	// The [[locality]] with which to interpret this moment
	loc: locality,

	// The time since the start of the day
	time: time::duration,
	// The [[time::instant]] of this moment
	inst: time::instant,

	// The timezone used for interpreting a moment's date and time
	loc: locality,
	// The observed ordinal day (on Earth or otherwise)
	// since an abitrary epoch, like the Hare epoch 1970-01-01
	date: (date | void),

	// The observed time since the start of the day
	time: (time::duration | void),

	// The current [[zone]] this moment observes
	zone: zone,
	// The observed [[zone]]
	zone: (zone | void),
};

// An ordinal day since an epoch. The Hare epoch (zeroth day) 1970 Jan 1st is
@@ -26,40 +41,92 @@ export type moment = struct {
export type date = i64;

// Creates a new [[moment]].
export fn new(
export fn new(loc: locality, inst: time::instant) moment = {
	return moment {
		loc = loc,
		inst = inst,
		date = void,
		time = void,
		zone = void,
	};
};

// Evalutes, caches, and returns a [[moment]]'s observed [[zone]].
export fn getzone(m: *moment) zone = {
	match (m.zone) {
	case let z: zone =>
		return z;
	case void =>
		return lookupzone(m);
	};
};

// Evaluates, caches, and returns a [[moment]]'s observed epochal date.
export fn getdate(m: *moment) date = {
	match (m.date) {
	case let d: date =>
		return d;
	case void =>
		return eval_datetime(m).0;
	};
};

// Evaluates, caches, and returns a [[moment]]'s observed time-of-day as a
// [[time::duration]] since the start of a day.
export fn gettime(m: *moment) time::duration = {
	match (m.time) {
	case let t: time::duration =>
		return t;
	case void =>
		return eval_datetime(m).1;
	};
};

// Evaluates, caches, and returns a [[moment]]'s observed date & time.
fn eval_datetime(m: *moment) (date, time::duration) = {
	const i = time::add(m.inst, getzone(m).zoffset);
	const day = m.loc.daylength;
	const daysec = day / time::SECOND;
	const d = if (i.sec < 0) i.sec / daysec - 1 else i.sec / daysec;
	const t = ((i.sec % daysec + daysec) * time::SECOND + i.nsec) % day;
	m.time = t;
	m.date = d;
	return (d, t);
};

// Creates a [[moment]] from a given [[locality]], zone offset, [[date]] and
// time-of-day.
export fn from_datetime(
	loc: locality,
	zo: time::duration,
	d: date,
	t: time::duration,
) (moment | invalid) = {
	if (t > loc.daylength) {
		return invalid;
	};
	const m = moment {
) moment = {
	return moment {
		loc = loc,
		inst = calc_instant(loc.daylength, zo, d, t),
		date = d,
		time = t,
		loc = loc,
		zone = zone { ... },
		zone = void
	};
	lookupzone(&m);
	return m;
};

// Creates a new [[moment]] from a [[time::instant]] in a [[locality]].
export fn from_instant(i: time::instant, loc: locality) moment = {
	const daysec = (loc.daylength / time::SECOND);
	const d = i.sec / daysec;
	const t = (i.sec % daysec) * time::SECOND + i.nsec * time::NANOSECOND;
	assert(t < loc.daylength, "Internal error: time excedes daylength");
	return new(loc, d, t)!;
};

// Creates a new [[time::instant]] from a [[moment]].
export fn to_instant(m: moment) time::instant = {
	const daysec = (m.loc.daylength / time::SECOND);
	const i = time::instant {
		sec = (m.date: i64 * daysec) + (m.time / time::SECOND),
		nsec = m.time % time::SECOND,
fn calc_instant(
	day: time::duration, // length of a day
	zo: time::duration,  // zone offset
	d: date,             // date since epoch
	t: time::duration,   // time since start of day
) time::instant = {
	// TODO: make sure this works across transitions
	const daysec = (day / time::SECOND): i64;
	const dayrem = day % time::SECOND;
	let i = time::instant {
		sec = d * daysec,
		nsec = 0,
	};
	i = time::add(i, d: i64 * dayrem);
	i = time::add(i, t);
	i = time::add(i, -zo);
	return i;
};

diff --git a/time/chrono/timezone.ha b/time/chrono/timezone.ha
index 52615b35..8e1a9eec 100644
--- a/time/chrono/timezone.ha
+++ b/time/chrono/timezone.ha
@@ -78,7 +78,7 @@ type tzname = struct {
// timescales.
export fn in(loc: locality, m: moment) moment = {
	if (m.loc.timescale != loc.timescale) {
		const i = to_instant(m);
		const i = m.inst;
		const i = match (m.loc.timescale.to_tai(i)) {
		case let i: time::instant =>
			yield i;
@@ -91,26 +91,9 @@ export fn in(loc: locality, m: moment) moment = {
		case time::error =>
			abort("time::chrono::in(): direct timescale conversion failed");
		};
		const m = from_instant(i, loc);
		return m;
		return new(loc, i);
	};

	assert(m.time < loc.daylength, "Internal error: time excedes daylength");
	return new(loc, m.date, m.time)!; // resets .zone
};

export fn transform(m: moment, zo: time::duration) moment = {
	const daylen = m.loc.daylength;

	const t = m.time + zo;
	const mtime = (if (t >= 0) t else t + daylen) % daylen;

	const d = (t / daylen): int;
	const mdate = m.date + (if (t >= 0) d else d - 1);

	m.time = mtime;
	m.date = mdate;
	return m;
	return new(loc, m.inst);
};

// Finds, sets and returns a [[moment]]'s currently observed zone.
@@ -118,19 +101,17 @@ export fn lookupzone(m: *moment) zone = {
	// TODO: https://todo.sr.ht/~sircmpwn/hare/643
	if (len(m.loc.zones) == 0) {
		// TODO: what to do? not ideal to assume UTC
		abort("lookup(): timezones should have at least one zone");
		abort("lookupzone(): timezones should have at least one zone");
	};

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

	const inst = to_instant(*m);

	if (
		len(m.loc.transitions) == 0
		|| time::compare(inst, m.loc.transitions[0].when) == -1
		|| time::compare(m.inst, m.loc.transitions[0].when) == -1
	) {
		// TODO: special case
		abort("lookupzone(): time is before known transitions");
@@ -141,7 +122,7 @@ export fn lookupzone(m: *moment) zone = {
	for (hi - lo > 1) {
		const mid = lo + (hi - lo) / 2;
		const middle = m.loc.transitions[mid].when;
		switch (time::compare(inst, middle)) {
		switch (time::compare(m.inst, middle)) {
		case -1 =>
			hi = mid;
		case 0 =>
@@ -163,7 +144,7 @@ export fn lookupzone(m: *moment) zone = {
		void;
	};

	return m.zone;
	return m.zone as zone;
};

// Creates a [[timezone]] with a single [[zone]]. Useful for fixed offsets.
-- 
2.36.1

[PATCH hare v2 02/12] time::chrono,datetime: rename min, sec, nsec Export this patch

This is in preparation for the next commit.

* min -> minute
* sec -> second
* nsec -> nanosecond

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/arithmetic.ha | 24 ++++++++++-----------
 datetime/chronology.ha | 48 +++++++++++++++++++++---------------------
 datetime/datetime.ha   | 18 ++++++++--------
 datetime/format.ha     |  6 +++---
 datetime/parse.ha      |  6 +++---
 5 files changed, 51 insertions(+), 51 deletions(-)

diff --git a/datetime/arithmetic.ha b/datetime/arithmetic.ha
index d114bc07..937f42d1 100644
--- a/datetime/arithmetic.ha
+++ b/datetime/arithmetic.ha
@@ -112,19 +112,19 @@ export fn diff(a: datetime, b: datetime) period = {
		res.hours = 24 + res.hours;
	};

	res.minutes = min(&a) - min(&b);
	res.minutes = minute(&a) - minute(&b);
	if (res.minutes < 0) {
		res.hours -= 1;
		res.minutes = 60 + res.minutes;
	};

	res.seconds = sec(&a) - sec(&b);
	res.seconds = second(&a) - second(&b);
	if (res.seconds < 0) {
		res.minutes -= 1;
		res.seconds = 60 + res.seconds;
	};

	res.nanoseconds = nsec(&a) - nsec(&b);
	res.nanoseconds = nanosecond(&a) - nanosecond(&b);
	if (res.nanoseconds < 0) {
		res.seconds -= 1;
		res.nanoseconds = time::SECOND + res.nanoseconds;
@@ -220,12 +220,12 @@ export fn truncate(dt: datetime, u: unit) datetime = {
	case unit::MINUTE =>
		yield new(dt.loc, 0,
			year(&dt), month(&dt), day(&dt),
			hour(&dt), min(&dt), 00, 0,
			hour(&dt), minute(&dt), 00, 0,
		)!;
	case unit::SECOND =>
		yield new(dt.loc, 0,
			year(&dt), month(&dt), day(&dt),
			hour(&dt), min(&dt), sec(&dt), 0,
			hour(&dt), minute(&dt), second(&dt), 0,
		)!;
	case unit::NANOSECOND =>
		yield dt;
@@ -315,9 +315,9 @@ export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
	let d_month = month(&dt);
	let d_day = day(&dt);
	let d_hour = hour(&dt);
	let d_min = min(&dt);
	let d_sec = sec(&dt);
	let d_nsec = ((nsec(&dt)): i64);
	let d_minute = minute(&dt);
	let d_second = second(&dt);
	let d_nanosecond = ((nanosecond(&dt)): i64);
	for (let i = 0z; i < len(pp); i += 1) {
		const p = pp[i];

@@ -393,14 +393,14 @@ export fn add(dt: datetime, flag: calculus, pp: period...) datetime = {
			};
			const new_hmsn = calc_hmsn(new_time);
			d_hour = new_hmsn.0;
			d_min = new_hmsn.1;
			d_sec = new_hmsn.2;
			d_nsec = new_hmsn.3;
			d_minute = new_hmsn.1;
			d_second = new_hmsn.2;
			d_nanosecond = new_hmsn.3;
		};
	};
	// TODO: Add zoffset back in here once API is settled
	return new(dt.loc, 0,
		d_year, d_month, d_day, d_hour, d_min, d_sec, d_nsec: int,
		d_year, d_month, d_day, d_hour, d_minute, d_second, d_nanosecond: int,
	)!;
};

diff --git a/datetime/chronology.ha b/datetime/chronology.ha
index e2347350..42c9febb 100644
--- a/datetime/chronology.ha
+++ b/datetime/chronology.ha
@@ -47,13 +47,13 @@ export fn isoweek(dt: *datetime) int = _isoweek(dt);
export fn hour(dt: *datetime) int = _hour(dt);

// Returns a [[datetime]]'s minute of the hour.
export fn min(dt: *datetime) int = _min(dt);
export fn minute(dt: *datetime) int = _minute(dt);

// Returns a [[datetime]]'s second of the minute.
export fn sec(dt: *datetime) int = _sec(dt);
export fn second(dt: *datetime) int = _second(dt);

// Returns a [[datetime]]'s nanosecond of the second.
export fn nsec(dt: *datetime) int = _nsec(dt);
export fn nanosecond(dt: *datetime) int = _nanosecond(dt);


fn _epochal(dt: *datetime) chrono::date = {
@@ -234,52 +234,52 @@ fn _hour(dt: *datetime) int = {
	case void =>
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
		dt.nsec = hmsn.3;
		dt.minute = hmsn.1;
		dt.second = hmsn.2;
		dt.nanosecond = hmsn.3;
		return dt.hour: int;
	case let h: int =>
		return h;
	};
};

fn _min(dt: *datetime) int = {
	match (dt.min) {
fn _minute(dt: *datetime) int = {
	match (dt.minute) {
	case void =>
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
		dt.nsec = hmsn.3;
		return dt.min: int;
		dt.minute = hmsn.1;
		dt.second = hmsn.2;
		dt.nanosecond = hmsn.3;
		return dt.minute: int;
	case let m: int =>
		return m;
	};
};

fn _sec(dt: *datetime) int = {
	match (dt.sec) {
fn _second(dt: *datetime) int = {
	match (dt.second) {
	case void =>
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
		dt.nsec = hmsn.3;
		return dt.sec: int;
		dt.minute = hmsn.1;
		dt.second = hmsn.2;
		dt.nanosecond = hmsn.3;
		return dt.second: int;
	case let s: int =>
		return s;
	};
};

fn _nsec(dt: *datetime) int = {
	match (dt.nsec) {
fn _nanosecond(dt: *datetime) int = {
	match (dt.nanosecond) {
	case void =>
		const hmsn = calc_hmsn(chrono::gettime(dt));
		dt.hour = hmsn.0;
		dt.min = hmsn.1;
		dt.sec = hmsn.2;
		dt.nsec = hmsn.3;
		return dt.nsec: int;
		dt.minute = hmsn.1;
		dt.second = hmsn.2;
		dt.nanosecond = hmsn.3;
		return dt.nanosecond: int;
	case let n: int =>
		return n;
	};
diff --git a/datetime/datetime.ha b/datetime/datetime.ha
index 8c7331da..cff9b5e5 100644
--- a/datetime/datetime.ha
+++ b/datetime/datetime.ha
@@ -23,9 +23,9 @@ export type datetime = struct {
	weekday:     (void | int),

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

fn init() datetime = datetime {
@@ -47,9 +47,9 @@ fn init() datetime = datetime {
	weekday     = void,

	hour        = void,
	min         = void,
	sec         = void,
	nsec        = void,
	minute      = void,
	second      = void,
	nanosecond  = void,
};

// Creates a new datetime. When loc=void, defaults to chrono::local.
@@ -140,9 +140,9 @@ export fn new(
		|| month != _month(&dt)
		|| day   != _day(&dt)
		|| hour  != _hour(&dt)
		|| min   != _min(&dt)
		|| sec   != _sec(&dt)
		|| nsec  != _nsec(&dt)
		|| min   != _minute(&dt)
		|| sec   != _second(&dt)
		|| nsec  != _nanosecond(&dt)
	) {
		return invalid;
	};
diff --git a/datetime/format.ha b/datetime/format.ha
index d9e994eb..56d52da1 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -120,9 +120,9 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'm' =>
		return fmt::fprintf(out, "{:02}", month(dt));
	case 'M' =>
		return fmt::fprintf(out, "{:02}", min(dt));
		return fmt::fprintf(out, "{:02}", minute(dt));
	case 'N' =>
		return fmt::fprintf(out, "{:09}", strconv::itos(nsec(dt)));
		return fmt::fprintf(out, "{:09}", strconv::itos(nanosecond(dt)));
	case 'p' =>
		const s = if (hour(dt) < 12) {
			yield "AM";
@@ -131,7 +131,7 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
		};
		return fmt::fprint(out, s);
	case 'S' =>
		return fmt::fprintf(out, "{:02}", sec(dt));
		return fmt::fprintf(out, "{:02}", second(dt));
	case 'u' =>
		return fmt::fprint(out, strconv::itos(weekday(dt)));
	case 'U' =>
diff --git a/datetime/parse.ha b/datetime/parse.ha
index dab4f9b2..0a0d45e5 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -90,10 +90,10 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
			build.month = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 1, 12);
		case 'M' =>
			build.min = clamp_int(
			build.minute = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 0, 59);
		case 'N' =>
			build.nsec = clamp_int(
			build.nanosecond = clamp_int(
				get_max_n_digits(&s_iter, 9)?, 0, 999999999);
		case 'p' =>
			if (build.hour is void) {
@@ -123,7 +123,7 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
			strings::next(&s_iter);
			strings::next(&s_iter);
		case 'S' =>
			build.sec = clamp_int(
			build.second = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 0, 61);
		case 'u', 'w' =>
			build.weekday = match (get_max_n_digits(&s_iter, 1)) {
-- 
2.36.1

[PATCH hare v2 03/12] time::chrono,datetime: embed instant into moment Export this patch

The [[time::chrono::moment]] struct type now embeds the
[[time::instant]] type, instead of having an '.inst' field.

This commit expands upon the idea of continuity between all time-related
modules (stdlib & third-party) and all temporal types. The idea is to
have code which can fluently handle pointers to any temporal type based
upon (embedding) time::instant (often via embedding chrono::moment),
similar to the stdlib's I/O types. This allows us not only to encompass
all chronologies and timescales with a few temporal types, but to
operate upon these types across modules with the same utilities.

    const a: datetime::datetime = datetime::now();
    time::add(*(&a: *time::instant), delta);

    // theoretical "martian" chronology module
    const b: martian::datetime = martian::now();
    time::add(*(&b: *time::instant), delta);

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/arithmetic.ha    |  6 +++---
 datetime/datetime.ha      |  6 ++++--
 time/chrono/chronology.ha | 15 +++++++++------
 time/chrono/timezone.ha   |  8 ++++----
 4 files changed, 20 insertions(+), 15 deletions(-)

diff --git a/datetime/arithmetic.ha b/datetime/arithmetic.ha
index 937f42d1..9af141c0 100644
--- a/datetime/arithmetic.ha
+++ b/datetime/arithmetic.ha
@@ -53,7 +53,7 @@ export type unit = enum int {
// Equivalence means they represent the same moment in time, regardless of their
// locality or observed chronological values.
export fn eq(a: datetime, b: datetime) bool = {
	return time::compare(a.inst, b.inst) == 0;
	return time::compare(*(&a: *time::instant), *(&b: *time::instant)) == 0;
};

// Returns true if [[datetime]] "a" succeeds [[datetime]] "b".
@@ -61,7 +61,7 @@ export fn eq(a: datetime, b: datetime) bool = {
// Temporal order is evaluated in a universal frame of reference, regardless of
// their locality or observed chronological values.
export fn after(a: datetime, b: datetime) bool = {
	return time::compare(a.inst, b.inst) == +1;
	return time::compare(*(&a: *time::instant), *(&b: *time::instant)) == +1;
};

// Returns true if [[datetime]] "a" precedes [[datetime]] "b".
@@ -69,7 +69,7 @@ export fn after(a: datetime, b: datetime) bool = {
// Temporal order is evaluated in a universal frame of reference, regardless of
// their locality or observed chronological values.
export fn before(a: datetime, b: datetime) bool = {
	return time::compare(a.inst, b.inst) == -1;
	return time::compare(*(&a: *time::instant), *(&b: *time::instant)) == -1;
};

// Calculates the [[period]] between two [[datetime]]s.
diff --git a/datetime/datetime.ha b/datetime/datetime.ha
index cff9b5e5..bfa8d7b2 100644
--- a/datetime/datetime.ha
+++ b/datetime/datetime.ha
@@ -30,7 +30,8 @@ export type datetime = struct {

fn init() datetime = datetime {
	loc         = chrono::LOCAL,
	inst        = time::instant { ... },
	sec         = 0,
	nsec        = 0,
	date        = void,
	time        = void,
	zone        = void,
@@ -167,7 +168,8 @@ export fn now() datetime = {
export fn from_moment(m: chrono::moment) datetime = {
	const dt = init();
	dt.loc = m.loc;
	dt.inst = m.inst;
	dt.sec = m.sec;
	dt.nsec = m.nsec;
	dt.date = m.date;
	dt.time = m.time;
	dt.zone = m.zone;
diff --git a/time/chrono/chronology.ha b/time/chrono/chronology.ha
index 5222152a..393db52d 100644
--- a/time/chrono/chronology.ha
+++ b/time/chrono/chronology.ha
@@ -19,12 +19,12 @@ export type invalid = !void;
// embeds this type, and other modules implementing other chronologies may
// interoperate by passing pointers.
export type moment = struct {
	// The embedded [[time::instant]] of this moment
	time::instant,

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

	// The [[time::instant]] of this moment
	inst: time::instant,

	// The observed ordinal day (on Earth or otherwise)
	// since an abitrary epoch, like the Hare epoch 1970-01-01
	date: (date | void),
@@ -44,7 +44,8 @@ export type date = i64;
export fn new(loc: locality, inst: time::instant) moment = {
	return moment {
		loc = loc,
		inst = inst,
		sec = inst.sec,
		nsec = inst.nsec,
		date = void,
		time = void,
		zone = void,
@@ -84,7 +85,7 @@ export fn gettime(m: *moment) time::duration = {

// Evaluates, caches, and returns a [[moment]]'s observed date & time.
fn eval_datetime(m: *moment) (date, time::duration) = {
	const i = time::add(m.inst, getzone(m).zoffset);
	const i = time::add(*(m: *time::instant), getzone(m).zoffset);
	const day = m.loc.daylength;
	const daysec = day / time::SECOND;
	const d = if (i.sec < 0) i.sec / daysec - 1 else i.sec / daysec;
@@ -102,9 +103,11 @@ export fn from_datetime(
	d: date,
	t: time::duration,
) moment = {
	const inst = calc_instant(loc.daylength, zo, d, t);
	return moment {
		loc = loc,
		inst = calc_instant(loc.daylength, zo, d, t),
		sec = inst.sec,
		nsec = inst.nsec,
		date = d,
		time = t,
		zone = void
diff --git a/time/chrono/timezone.ha b/time/chrono/timezone.ha
index 8e1a9eec..6a872f0c 100644
--- a/time/chrono/timezone.ha
+++ b/time/chrono/timezone.ha
@@ -78,7 +78,7 @@ type tzname = struct {
// timescales.
export fn in(loc: locality, m: moment) moment = {
	if (m.loc.timescale != loc.timescale) {
		const i = m.inst;
		const i = *(&m: *time::instant);
		const i = match (m.loc.timescale.to_tai(i)) {
		case let i: time::instant =>
			yield i;
@@ -93,7 +93,7 @@ export fn in(loc: locality, m: moment) moment = {
		};
		return new(loc, i);
	};
	return new(loc, m.inst);
	return new(loc, *(&m: *time::instant));
};

// Finds, sets and returns a [[moment]]'s currently observed zone.
@@ -111,7 +111,7 @@ export fn lookupzone(m: *moment) zone = {

	if (
		len(m.loc.transitions) == 0
		|| time::compare(m.inst, m.loc.transitions[0].when) == -1
		|| time::compare(*(m: *time::instant), m.loc.transitions[0].when) == -1
	) {
		// TODO: special case
		abort("lookupzone(): time is before known transitions");
@@ -122,7 +122,7 @@ export fn lookupzone(m: *moment) zone = {
	for (hi - lo > 1) {
		const mid = lo + (hi - lo) / 2;
		const middle = m.loc.transitions[mid].when;
		switch (time::compare(m.inst, middle)) {
		switch (time::compare(*(m: *time::instant), middle)) {
		case -1 =>
			hi = mid;
		case 0 =>
-- 
2.36.1

[PATCH hare v2 04/12] datetime: format: call field functions directly Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 34 +++++++++++++++++-----------------
 1 file changed, 17 insertions(+), 17 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index 56d52da1..92cf55f4 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -100,52 +100,52 @@ export fn asformat(layout: str, dt: *datetime) (str | invalid | io::error) = {
fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	switch (r) {
	case 'a' =>
		return fmt::fprint(out, WEEKDAYS_SHORT[weekday(dt) - 1]);
		return fmt::fprint(out, WEEKDAYS_SHORT[_weekday(dt) - 1]);
	case 'A' =>
		return fmt::fprint(out, WEEKDAYS[weekday(dt) - 1]);
		return fmt::fprint(out, WEEKDAYS[_weekday(dt) - 1]);
	case 'b' =>
		return fmt::fprint(out, MONTHS_SHORT[month(dt) - 1]);
		return fmt::fprint(out, MONTHS_SHORT[_month(dt) - 1]);
	case 'B' =>
		return fmt::fprint(out, MONTHS[month(dt) - 1]);
		return fmt::fprint(out, MONTHS[_month(dt) - 1]);
	case 'd' =>
		return fmt::fprintf(out, "{:02}", day(dt));
		return fmt::fprintf(out, "{:02}", _day(dt));
	case 'H' =>
		return fmt::fprintf(out, "{:02}", hour(dt));
		return fmt::fprintf(out, "{:02}", _hour(dt));
	case 'I' =>
		return fmt::fprintf(out, "{:02}", hour12(dt));
	case 'j' =>
		return fmt::fprint(out, strconv::itos(yearday(dt)));
		return fmt::fprint(out, strconv::itos(_yearday(dt)));
	case 'L' =>
		return fmt::fprint(out, dt.loc.name);
	case 'm' =>
		return fmt::fprintf(out, "{:02}", month(dt));
		return fmt::fprintf(out, "{:02}", _month(dt));
	case 'M' =>
		return fmt::fprintf(out, "{:02}", minute(dt));
		return fmt::fprintf(out, "{:02}", _minute(dt));
	case 'N' =>
		return fmt::fprintf(out, "{:09}", strconv::itos(nanosecond(dt)));
		return fmt::fprintf(out, "{:09}", strconv::itos(_nanosecond(dt)));
	case 'p' =>
		const s = if (hour(dt) < 12) {
		const s = if (_hour(dt) < 12) {
			yield "AM";
		} else {
			yield "PM";
		};
		return fmt::fprint(out, s);
	case 'S' =>
		return fmt::fprintf(out, "{:02}", second(dt));
		return fmt::fprintf(out, "{:02}", _second(dt));
	case 'u' =>
		return fmt::fprint(out, strconv::itos(weekday(dt)));
		return fmt::fprint(out, strconv::itos(_weekday(dt)));
	case 'U' =>
		return fmt::fprintf(out, "{:02}", _sundayweek(dt));
	case 'w' =>
		return fmt::fprint(out, strconv::itos(weekday(dt) % 7));
		return fmt::fprint(out, strconv::itos(_weekday(dt) % 7));
	case 'W' =>
		return fmt::fprintf(out, "{:02}", week(dt));
		return fmt::fprintf(out, "{:02}", _week(dt));
	case 'y' =>
		let year_str = strconv::itos(year(dt));
		let year_str = strconv::itos(_year(dt));
		year_str = strings::sub(year_str, len(year_str) - 2, strings::end);
		return fmt::fprint(out, year_str);
	case 'Y' =>
		return fmt::fprint(out, strconv::itos(year(dt)));
		return fmt::fprint(out, strconv::itos(_year(dt)));
	case 'z' =>
		// TODO: test me
		let pm = '+';
-- 
2.36.1

[PATCH hare v2 05/12] datetime: use tuple unpacking for %z Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index 92cf55f4..b0f2eb46 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -147,15 +147,13 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'Y' =>
		return fmt::fprint(out, strconv::itos(_year(dt)));
	case 'z' =>
		// TODO: test me
		let pm = '+';
		const z = if (chrono::getzone(dt).zoffset >= 0) {
			yield calc_hmsn(chrono::getzone(dt).zoffset);
		const (sign, zo) = if (chrono::getzone(dt).zoffset >= 0) {
			yield ('+', calc_hmsn(chrono::getzone(dt).zoffset));
		} else {
			pm = '-';
			yield calc_hmsn(-chrono::getzone(dt).zoffset);
			yield ('-', calc_hmsn(-chrono::getzone(dt).zoffset));
		};
		return fmt::fprintf(out, "{}{:02}{:02}", pm, z.0, z.1);
		const (hr, mi) = (zo.0, zo.1);
		return fmt::fprintf(out, "{}{:02}{:02}", sign, hr, mi);
	case 'Z' =>
		return fmt::fprint(out, chrono::getzone(dt).abbr);
	case '%' =>
-- 
2.36.1

[PATCH hare v2 06/12] datetime: organize parsing util functions Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 71 -------------------------------------------
 datetime/parse.ha  | 75 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 75 insertions(+), 71 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index b0f2eb46..786c8a57 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -227,77 +227,6 @@ export fn format(
	return n;
};

fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int | invalid) = {
	const name = strings::iterstr(iter);
	if (len(name) == 0) {
		return invalid;
	};
	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) + 1;
		};
	};
	return invalid;
};

fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | invalid) = {
	let buf: [64]u8 = [0...];
	let bufstr = strio::fixed(buf);
	for (let i = 0z; i < n; i += 1) {
		let r: rune = match (strings::next(iter)) {
			case void =>
				break;
			case let r: rune =>
				yield r;
		};
		if (!ascii::isdigit(r)) {
			strings::prev(iter);
			break;
		};
		match (strio::appendrune(&bufstr, r)) {
		case io::error =>
			return invalid;
		case =>
			void;
		};
	};
	return match (strconv::stoi(strio::string(&bufstr))) {
	case let res: int =>
		yield res;
	case =>
		yield invalid;
	};
};

fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = {
	let s_r = match (strings::next(iter)) {
	case void =>
		return invalid;
	case let r: rune =>
		yield r;
	};
	if (s_r == needle) {
		return 1;
	} else {
		strings::prev(iter);
		return 0;
	};
};

fn clamp_int(i: int, min: int, max: int) int = {
	return if (i < min) {
		yield min;
	} else if (i > max) {
		yield max;
	} else {
		yield i;
	};
};

fn hour12(dt: *datetime) int = {
	let mod_hour = hour(dt) % 12;
	if (mod_hour == 0) {
diff --git a/datetime/parse.ha b/datetime/parse.ha
index 0a0d45e5..e7883868 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -2,8 +2,12 @@
// (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 strio;
use time;
use time::chrono;

@@ -174,3 +178,74 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
	};
	return void;
};

fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = {
	let s_r = match (strings::next(iter)) {
	case void =>
		return invalid;
	case let r: rune =>
		yield r;
	};
	if (s_r == needle) {
		return 1;
	} else {
		strings::prev(iter);
		return 0;
	};
};

fn clamp_int(i: int, min: int, max: int) int = {
	return if (i < min) {
		yield min;
	} else if (i > max) {
		yield max;
	} else {
		yield i;
	};
};

fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int | invalid) = {
	const name = strings::iterstr(iter);
	if (len(name) == 0) {
		return invalid;
	};
	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) + 1;
		};
	};
	return invalid;
};

fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | invalid) = {
	let buf: [64]u8 = [0...];
	let bufstr = strio::fixed(buf);
	for (let i = 0z; i < n; i += 1) {
		let r: rune = match (strings::next(iter)) {
			case void =>
				break;
			case let r: rune =>
				yield r;
		};
		if (!ascii::isdigit(r)) {
			strings::prev(iter);
			break;
		};
		match (strio::appendrune(&bufstr, r)) {
		case io::error =>
			return invalid;
		case =>
			void;
		};
	};
	return match (strconv::stoi(strio::string(&bufstr))) {
	case let res: int =>
		yield res;
	case =>
		yield invalid;
	};
};
-- 
2.36.1

[PATCH hare v2 07/12] datetime: format: simplify %I (12 hour) Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index 786c8a57..f0a60ef4 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -112,7 +112,7 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'H' =>
		return fmt::fprintf(out, "{:02}", _hour(dt));
	case 'I' =>
		return fmt::fprintf(out, "{:02}", hour12(dt));
		return fmt::fprintf(out, "{:02}", (_hour(dt) + 11) % 12 + 1);
	case 'j' =>
		return fmt::fprint(out, strconv::itos(_yearday(dt)));
	case 'L' =>
@@ -227,14 +227,6 @@ export fn format(
	return n;
};

fn hour12(dt: *datetime) int = {
	let mod_hour = hour(dt) % 12;
	if (mod_hour == 0) {
		mod_hour = 12;
	};
	return mod_hour;
};

@test fn format() void = {
	const dt = new(chrono::UTC, 0, 1994, 01, 01, 02, 17, 05, 24)!;

-- 
2.36.1

[PATCH hare v2 08/12] datetime: format: simplify %p (AM/PM) Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index f0a60ef4..abe8861d 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -124,12 +124,7 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'N' =>
		return fmt::fprintf(out, "{:09}", strconv::itos(_nanosecond(dt)));
	case 'p' =>
		const s = if (_hour(dt) < 12) {
			yield "AM";
		} else {
			yield "PM";
		};
		return fmt::fprint(out, s);
		return fmt::fprint(out, if (_hour(dt) < 12) "AM" else "PM");
	case 'S' =>
		return fmt::fprintf(out, "{:02}", _second(dt));
	case 'u' =>
-- 
2.36.1

[PATCH hare v2 09/12] datetime: format: simplify %y (year of century) Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index abe8861d..bce0d8c0 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -136,9 +136,7 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'W' =>
		return fmt::fprintf(out, "{:02}", _week(dt));
	case 'y' =>
		let year_str = strconv::itos(_year(dt));
		year_str = strings::sub(year_str, len(year_str) - 2, strings::end);
		return fmt::fprint(out, year_str);
		return fmt::fprintf(out, "{:02}", _year(dt) % 100);
	case 'Y' =>
		return fmt::fprint(out, strconv::itos(_year(dt)));
	case 'z' =>
-- 
2.36.1

[PATCH hare v2 10/12] datetime: format: fix %j (ordinal-day/yearday) Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/format.ha | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/datetime/format.ha b/datetime/format.ha
index bce0d8c0..2282d459 100644
--- a/datetime/format.ha
+++ b/datetime/format.ha
@@ -114,7 +114,7 @@ fn fmtout(out: io::handle, r: rune, dt: *datetime) (size | io::error) = {
	case 'I' =>
		return fmt::fprintf(out, "{:02}", (_hour(dt) + 11) % 12 + 1);
	case 'j' =>
		return fmt::fprint(out, strconv::itos(_yearday(dt)));
		return fmt::fprintf(out, "{:03}", strconv::itos(_yearday(dt)));
	case 'L' =>
		return fmt::fprint(out, dt.loc.name);
	case 'm' =>
@@ -253,7 +253,7 @@ export fn format(
		("%a", "Sat"),
		("%A", "Saturday"),
		// yearday
		("%j", "1"),
		("%j", "001"),
		// week
		("%W", "00"),
	];
-- 
2.36.1

[PATCH hare v2 11/12] datetime: parse: tidy code Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/parse.ha | 174 +++++++++++++++++++++++-----------------------
 1 file changed, 86 insertions(+), 88 deletions(-)

diff --git a/datetime/parse.ha b/datetime/parse.ha
index e7883868..a6326e6f 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -19,93 +19,81 @@ use time::chrono;
// 	datetime::parse(&builder, "%H:%M:%S", "03:14:07");
//
export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
	const format_iter = strings::iter(layout);
	const s_iter = strings::iter(s);
	const layout_iter = strings::iter(layout);
	const iter = strings::iter(s);
	let escaped = false;
	for (true) {
		let format_r: rune = match (strings::next(&format_iter)) {
		const layout_rune: rune = match (strings::next(&layout_iter)) {
		case void =>
			break;
		case let r: rune =>
			yield r;
		case let rn: rune =>
			yield rn;
		};

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

		if (!escaped) {
			let s_r = match (strings::next(&s_iter)) {
			const rn = match (strings::next(&iter)) {
			case void =>
				return invalid;
			case let r: rune =>
				yield r;
			case let rn: rune =>
				yield rn;
			};
			if (s_r != format_r) {
			if (rn != layout_rune) {
				return invalid;
			};
			continue;
		};

		escaped = false;
		switch (format_r) {

		switch (layout_rune) {
		// Basic specifiers
		case 'a' =>
			build.weekday = get_default_locale_string_index(
				&s_iter, WEEKDAYS_SHORT[..])?;
			build.weekday = scan_for(&iter, WEEKDAYS_SHORT[..])?;
		case 'A' =>
			build.weekday = get_default_locale_string_index(
				&s_iter, WEEKDAYS[..])?;
			build.weekday = scan_for(&iter, WEEKDAYS[..])?;
		case 'b' =>
			build.month = get_default_locale_string_index(
				&s_iter, MONTHS_SHORT[..])?;
			build.month = scan_for(&iter, MONTHS_SHORT[..])?;
		case 'B' =>
			build.month = get_default_locale_string_index(
				&s_iter, MONTHS[..])?;
			build.month = scan_for(&iter, MONTHS[..])?;
		case 'd' =>
			let max_n_digits = 2u;
			build.day = clamp_int(
				get_max_n_digits(&s_iter, max_n_digits)?, 1, 31);
			build.day = trunc_int(scan_int(&iter, 2)?, 1, 31);
		case 'H' =>
			let max_n_digits = 2u;
			build.hour = clamp_int(
				get_max_n_digits(&s_iter, max_n_digits)?, 0, 23);
			build.hour = trunc_int(scan_int(&iter, 2)?, 0, 23);
		case 'I' =>
			let max_n_digits = 2u;
			const hour = get_max_n_digits(&s_iter, max_n_digits);
			const hour = scan_int(&iter, 2);
			build.hour = match (hour) {
			case let hour: int =>
				yield if (hour > 12) {
					yield clamp_int(hour - 12, 1, 12);
					yield trunc_int(hour - 12, 1, 12);
				} else {
					yield clamp_int(hour, 1, 12);
					yield trunc_int(hour, 1, 12);
				};
			case =>
				return invalid;
			};
		case 'j' =>
			build.yearday = clamp_int(
				get_max_n_digits(&s_iter, 3)?, 1, 366);
			build.yearday = trunc_int(scan_int(&iter, 3)?, 1, 366);
		case 'L' =>
			// TODO: Parse %L (locality/timezone name/ID).
			continue;
		case 'm' =>
			build.month = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 1, 12);
			build.month = trunc_int(scan_int(&iter, 2)?, 1, 12);
		case 'M' =>
			build.minute = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 0, 59);
			build.minute = trunc_int(scan_int(&iter, 2)?, 0, 59);
		case 'N' =>
			build.nanosecond = clamp_int(
				get_max_n_digits(&s_iter, 9)?, 0, 999999999);
			build.nanosecond = trunc_int(scan_int(&iter, 3)?, 0, 999999999);
		case 'p' =>
			if (build.hour is void) {
				// We can't change the hour's am/pm because we
				// have no hour.
				return invalid;
			};
			const rest = strings::iterstr(&s_iter);
			const rest = strings::iterstr(&iter);
			if (strings::hasprefix(rest, "AM")) {
				if (build.hour as int > 12) {
					// 13 AM?
@@ -118,59 +106,38 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
					// 13 PM?
					return invalid;
				} else if (build.hour as int < 12) {
					build.hour =
						(build.hour as int) + 12;
					build.hour = (build.hour as int) + 12;
				};
			} else {
				return invalid;
			};
			strings::next(&s_iter);
			strings::next(&s_iter);
			strings::next(&iter);
			strings::next(&iter);
		case 'S' =>
			build.second = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 0, 61);
			build.second = trunc_int(scan_int(&iter, 2)?, 0, 60);
		case 'u', 'w' =>
			build.weekday = match (get_max_n_digits(&s_iter, 1)) {
			build.weekday = match (scan_int(&iter, 1)) {
			case let i: int =>
				yield if (format_r == 'w') {
				yield if (layout_rune == 'w') {
					yield if (i == 0) {
						yield 7;
					} else {
						yield clamp_int(i, 1, 7);
						yield trunc_int(i, 1, 7);
					};
				} else {
					yield clamp_int(i, 1, 7);
					yield trunc_int(i, 1, 7);
				};
			case =>
				return invalid;
			};
		case 'U', 'W' =>
			build.week = clamp_int(
				get_max_n_digits(&s_iter, 2)?, 0, 53);
			build.week = trunc_int(scan_int(&iter, 2)?, 0, 53);
		case 'Y' =>
			build.year = get_max_n_digits(&s_iter, 4)?;
			build.year = scan_int(&iter, 4)?;
		case 'z' =>
			const rest = strings::iterstr(&s_iter);
			if(strings::hasprefix(rest, 'Z') || strings::hasprefix(rest, 'z')) {
				(build.zone: chrono::zone).zoffset = 0;
			} else {
				const prefix = strings::next(&s_iter);
				(build.zone: chrono::zone).zoffset = get_max_n_digits(&s_iter, 2)? * time::HOUR;

				const rest = strings::iterstr(&s_iter);
				if(strings::hasprefix(rest, ":")) {
					strings::next(&s_iter);
				};

				(build.zone: chrono::zone).zoffset += get_max_n_digits(&s_iter, 2)? * time::MINUTE;

				if(prefix == '-') {
					(build.zone: chrono::zone).zoffset *= -1;
				};
			};
			(build.zone: chrono::zone).zoffset = scan_zo(&iter)?;
		case '%' =>
			eat_one_rune(&s_iter, '%')?;

			eat_rune(&iter, '%')?;
		case =>
			// Ignore invalid specifier
			continue;
@@ -179,14 +146,14 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
	return void;
};

fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = {
	let s_r = match (strings::next(iter)) {
fn eat_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = {
	const rn = match (strings::next(iter)) {
	case void =>
		return invalid;
	case let r: rune =>
		yield r;
	case let rn: rune =>
		yield rn;
	};
	if (s_r == needle) {
	if (rn == needle) {
		return 1;
	} else {
		strings::prev(iter);
@@ -194,7 +161,8 @@ fn eat_one_rune(iter: *strings::iterator, needle: rune) (uint | invalid) = {
	};
};

fn clamp_int(i: int, min: int, max: int) int = {
// Truncates an int between the range [min,max].
fn trunc_int(i: int, min: int, max: int) int = {
	return if (i < min) {
		yield min;
	} else if (i > max) {
@@ -204,7 +172,9 @@ fn clamp_int(i: int, min: int, max: int) int = {
	};
};

fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int | invalid) = {
// 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 | invalid) = {
	const name = strings::iterstr(iter);
	if (len(name) == 0) {
		return invalid;
@@ -221,21 +191,23 @@ fn get_default_locale_string_index(iter: *strings::iterator, list: []str) (int |
	return invalid;
};

fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | invalid) = {
// Scans the iterator upto n consecutive numeric digits.
// Returns the resulting int.
fn scan_int(iter: *strings::iterator, n: uint) (int | invalid) = {
	let buf: [64]u8 = [0...];
	let bufstr = strio::fixed(buf);
	for (let i = 0z; i < n; i += 1) {
		let r: rune = match (strings::next(iter)) {
			case void =>
				break;
			case let r: rune =>
				yield r;
		let rn: rune = match (strings::next(iter)) {
		case void =>
			break;
		case let rn: rune =>
			yield rn;
		};
		if (!ascii::isdigit(r)) {
		if (!ascii::isdigit(rn)) {
			strings::prev(iter);
			break;
		};
		match (strio::appendrune(&bufstr, r)) {
		match (strio::appendrune(&bufstr, rn)) {
		case io::error =>
			return invalid;
		case =>
@@ -243,9 +215,35 @@ fn get_max_n_digits(iter: *strings::iterator, n: uint) (int | invalid) = {
		};
	};
	return match (strconv::stoi(strio::string(&bufstr))) {
	case let res: int =>
		yield res;
	case let n: int =>
		yield n;
	case =>
		yield invalid;
	};
};

// Scans and parses zone offsets of the form:
//
// 	Z
// 	z
// 	+nn:nn
// 	-nn:nn
//
fn scan_zo(iter: *strings::iterator) (time::duration | invalid) = {
	const rest = strings::iterstr(iter);
	if (strings::hasprefix(rest, 'Z') || strings::hasprefix(rest, 'z')) {
		return 0;
	} else {
		const prefix = strings::next(iter);
		let zo = scan_int(iter, 2)? * time::HOUR;
		const rest = strings::iterstr(iter);
		if (strings::hasprefix(rest, ":")) {
			strings::next(iter);
		};
		zo += scan_int(iter, 2)? * time::MINUTE;
		if (prefix == '-') {
			zo *= -1;
		};
		return zo;
	};
};
-- 
2.36.1

[PATCH hare v2 12/12] datetime: revert %I and %p code Export this patch

Signed-off-by: Byron Torres <b@torresjrjr.com>
---
 datetime/parse.ha | 46 +++++++++-------------------------------------
 1 file changed, 9 insertions(+), 37 deletions(-)

diff --git a/datetime/parse.ha b/datetime/parse.ha
index a6326e6f..4c111b88 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -65,22 +65,17 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
		case 'H' =>
			build.hour = trunc_int(scan_int(&iter, 2)?, 0, 23);
		case 'I' =>
			const hour = scan_int(&iter, 2);
			build.hour = match (hour) {
			case let hour: int =>
				yield if (hour > 12) {
					yield trunc_int(hour - 12, 1, 12);
				} else {
					yield trunc_int(hour, 1, 12);
				};
			case =>
				return invalid;
			};
			// TODO: Calculate hour using %I and %p at end of loop.
			// %I is ambiguous and insufficient by itself, and must
			// be used with %p to calculate the 24-hour value.
			// The order of the specifiers is not guaranteed. We
			// need to store %I and %p elsewhere.
			abort("TODO: parse %I");
		case 'j' =>
			build.yearday = trunc_int(scan_int(&iter, 3)?, 1, 366);
		case 'L' =>
			// TODO: Parse %L (locality/timezone name/ID).
			continue;
			abort("TODO: parse %L");
		case 'm' =>
			build.month = trunc_int(scan_int(&iter, 2)?, 1, 12);
		case 'M' =>
@@ -88,31 +83,8 @@ export fn parse(build: *builder, layout: str, s: str) (void | invalid) = {
		case 'N' =>
			build.nanosecond = trunc_int(scan_int(&iter, 3)?, 0, 999999999);
		case 'p' =>
			if (build.hour is void) {
				// We can't change the hour's am/pm because we
				// have no hour.
				return invalid;
			};
			const rest = strings::iterstr(&iter);
			if (strings::hasprefix(rest, "AM")) {
				if (build.hour as int > 12) {
					// 13 AM?
					return invalid;
				} else if (build.hour as int == 12) {
					build.hour = 0;
				};
			} else if (strings::hasprefix(rest, "PM")) {
				if (build.hour as int > 12) {
					// 13 PM?
					return invalid;
				} else if (build.hour as int < 12) {
					build.hour = (build.hour as int) + 12;
				};
			} else {
				return invalid;
			};
			strings::next(&iter);
			strings::next(&iter);
			// TODO: See %I above.
			abort("TODO: parse %p");
		case 'S' =>
			build.second = trunc_int(scan_int(&iter, 2)?, 0, 60);
		case 'u', 'w' =>
-- 
2.36.1
hare/patches: SUCCESS in 1m31s

[time::chrono/datetime reform][0] v2 from [Byron Torres][1]

[0]: https://lists.sr.ht/~sircmpwn/hare-dev/patches/33225
[1]: mailto:b@torresjrjr.com

✓ #786544 SUCCESS hare/patches/freebsd.yml https://builds.sr.ht/~sircmpwn/job/786544
✓ #786543 SUCCESS hare/patches/alpine.yml  https://builds.sr.ht/~sircmpwn/job/786543