~sircmpwn/hare-dev

RFC hare: time::chrono,datetime: add instant field to moment v1 NEEDS REVISION

Byron Torres: 2
 time::chrono,datetime: add instant field to moment
 time::chrono,datetime: embed instant into moment

 14 files changed, 247 insertions(+), 304 deletions(-)
Error ./datetime/parse.ha:150:39: Cannot select field from non-struct, non-union object


> Can you rebase your v2 against master and send the diff as a v3?
The v2 is mergeable onto the current master.
Are you asking for a single big patch of it?
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/32972/mbox | git am -3
Learn more about email & git

[PATCH hare 1/2] 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.

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.

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. datetime.inst is
accessible and of course always valid. The result is great continuity
across modules.

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.

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

Signed-off-by: Byron Torres <b@torresjrjr.com>
---

# TLDR

Better safety and validation. Less aborts. Better interop with other
modules. Simplifies API. Makes internal chrono and datetime code easier
to work with.


 datetime/arithmetic.ha    |  83 +++--------------------
 datetime/chronology.ha    |  27 +++-----
 datetime/datetime.ha      | 111 +++++++++++++-----------------
 datetime/format.ha        |   8 +--
 datetime/timezone.ha      |  17 +----
 time/chrono/chronology.ha | 137 ++++++++++++++++++++++++++++----------
 time/chrono/timezone.ha   |  35 +++-------
 7 files changed, 178 insertions(+), 240 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/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 1ff706e1..8e2f514b 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 RFC hare 2/2] 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 "darian" martian chronology module
	const b: darian::datetime = darian::now();
	time::add(*(&b: *time::instant), delta);

A side effect is the namespace conflict of the [[datetime::datetime]]
fields '.sec' and '.nsec'. These names along with a few others are
renamed throughout the [[datetime]] module for stylistic consistency.

* s/min/minute/
* s/sec/second/
* s/nsec/nanosecond/

Signed-off-by: Byron Torres <b@torresjrjr.com>
---

# WARNING

This renames the datetime::{min,sec,nsec} functions, a fairly
distruptive API change.


 datetime/arithmetic.ha    | 30 ++++++++++++------------
 datetime/chronology.ha    | 48 +++++++++++++++++++--------------------
 datetime/datetime.ha      | 24 +++++++++++---------
 datetime/format.ha        |  6 ++---
 datetime/parse.ha         |  2 +-
 time/chrono/chronology.ha | 15 +++++++-----
 time/chrono/timezone.ha   |  8 +++----
 7 files changed, 69 insertions(+), 64 deletions(-)

diff --git a/datetime/arithmetic.ha b/datetime/arithmetic.ha
index d114bc07..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.
@@ -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..bfa8d7b2 100644
--- a/datetime/datetime.ha
+++ b/datetime/datetime.ha
@@ -23,14 +23,15 @@ 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 {
	loc         = chrono::LOCAL,
	inst        = time::instant { ... },
	sec         = 0,
	nsec        = 0,
	date        = void,
	time        = void,
	zone        = void,
@@ -47,9 +48,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 +141,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;
	};
@@ -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/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 309c3a95..c3fead9d 100644
--- a/datetime/parse.ha
+++ b/datetime/parse.ha
@@ -88,7 +88,7 @@ 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(
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 8e2f514b..888c68b8 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
Error ./datetime/parse.ha:150:39: Cannot select field from non-struct, non-union object