Chronology in Hare April 17, 2022 by Byron Torres

Date and time support has come to Hare, with the introduction of two new modules to the standard library, written by Hare contributors Byron Torres (Hi, there! Today’s blog guest) and Vlad-Stefan Harbuz.

With these, we are now able to handle human notions of time: dates, the Gregorian calendar, wall-times, timezones, the “Olson” Timezone database, timezone transitions (daylight saving time, etc.), and timescales (leap seconds, etc.). We can also parse, format, and programmatically construct datetimes of high precision, and perform elementary calendrical arithmetic.

Dates and times in software are notoriously complicated and error prone. It’s hard to get right, as many other library designers have expressed. There are challenges within every strata of code, concerning all the aforementioned affairs. Making a library which was robust and complete enough, yet had a user-friendly API, was a mighty challenge.

Nonetheless, we’ve tried our best to design our library to be easy-to-use, modular, performant, and above all else, correct. It will need a few tweaks before it reaches its high aspirations, but today’s library can handle the most common problems a user will come across.

Let’s go over Hare’s new additions to the standard library.

The chrono module

At the core exists the time::chrono submodule, a natural extension of the time module. It centres around the chrono::moment type, itself a nephew of the time::instant type. While instants are simple scalar objects without a plane of reference, moments carry information about their temporal context, like their locality (timezone). Moments are an abstracted form of what you may know as a datetime, and will largely be used by the stdlib and third-party modules which seek cohesion with the rest of ecosystem.

// A date & time, within a locality, intepreted via a chronology
type moment = struct {
	// The ordinal day (on Earth or otherwise)
	// since the Hare epoch (zeroth day) 1970-01-01
	date: epochal,

	// The time since the start of the day
	time: time::duration,

	// The timezone used for interpreting a moment's date and time
	loc: locality,

	// The current [[zone]] this moment observes
	zone: zone,
};

time::chrono includes the timezone type, best defined as “a region (physical or abstract) where all wall-clocks agree”. Provided are some default timezones, like LOCAL (Local time), UTC, TAI, MTC, etc. A TZif parser is used to read the system’s installed Olson/IANA Timezone database, utilised by the chrono::tz function.

use time::chrono;

// A new moment equivalent to 2022-04-17 15:30 UTC
// (19098 days since 1970-01-01, 55800 seconds since 00:00)
let m = chrono::new(19098, 55800, chrono::UTC);

// same moment in time, but in a different locality.
let m = chrono::in(&chrono::tz("Pacific/Honolulu"), m);

On a Unix-like machine, the Timezone database is a set of files located at /usr/share/zoneinfo. The chrono::LOCAL timezone is set during initialisation, according to the system’s configured /etc/localtime.

$ ls -F /usr/share/zoneinfo
Africa/      Cuba     GMT+0        Japan              Pacific/    Turkey
America/     EET      GMT-0        Kwajalein          Poland      tzdata.zi
Antarctica/  Egypt    GMT0         leapseconds        Portugal    UCT
Arctic/      Eire     Greenwich    leap-seconds.list  posix/      Universal
Asia/        EST      Hongkong     Libya              posixrules  US/
Atlantic/    EST5EDT  HST          MET                PRC         UTC
Australia/   Etc/     Iceland      Mexico/            PST8PDT     WET
Brazil/      Europe/  Indian/      MST                right/      W-SU
Canada/      Factory  Iran         MST7MDT            ROC         zone1970.tab
CET          GB       iso3166.tab  Navajo             ROK         zone.tab
Chile/       GB-Eire  Israel       NZ                 SECURITY    Zulu
CST6CDT      GMT      Jamaica      NZ-CHAT            Singapore
$ realpath /etc/localtime
/usr/share/zoneinfo/Europe/London

Where Hare differs from most other languages is the feature of timescales, the notion of a “dimension” on which instants exist, a dimension of reference and measure.

The chrono::timescale type embodies this notion. Its role allows for careful and deliberate handling of things like leap seconds at a stock exchange, or timekeeping in scientific contexts at an astronomic observatory.

// Represents a scale of time; a time standard
type timescale = struct {
	name: str,
	abbr: str,
	to_tai: *ts_converter,
	from_tai: *ts_converter,
};

// Converts one [[time::instant]] in one [[chrono::timescale]] to another
type ts_converter = fn(i: time::instant) (time::instant | time::error);

Hare uses TAI (International Atomic Time) as the central timescale, as chrono::tai. TAI has the useful properties of being continuous and regular.

There’s also UTC (Coordinated Universal Time), as chrono::utc. Note that we’re referring to the UTC timescale, not the incidentally named “UTC” timezone, which could be more unambiguously referred to as “UTC+00:00” or “Zulu time”. UTC is definitionally based upon TAI. The two are offsetted by a variable number of seconds (at the time of writing, 37). Thus, UTC is not continuous and contains leap seconds.

MTC (Coordinated Mars Time) also makes a notable appearance as chrono::mtc. Martian seconds run ~3% slower, as a “sol” (Martian solar day) takes 24 hours and 39 minutes in Earth time. Hare is not yet a space-faring language, but we can take small steps to get there1.

Hare allows for the conversion of instants between timescales, and leverages the time::error types if and when users decide to deal with edge cases. chrono::utc in particular reads from /usr/share/zoneinfo/leap-seconds.list, a plaintext list of leap seconds, maintained by and fetched from observatories.

let utc = time::now(time::clock::REALTIME);  // 1650123000
let tai = chrono::utc.to_tai(utc)!;          // 1650123037 (+37)
let utc = chrono::utc.from_tai(tai)!;        // 1650123000

The datetime module

The datetime module concerns itself with human-oriented forms of time. It centres around the datetime::datetime type, an extension of the chrono::moment type. A typical third-party library will create their own type with its own units. This datetime type is designed for the “Gregorian chronology”.

type datetime = struct {
	chrono::moment,
	era: (void | int),
	year: (void | int),
	month: (void | int),
	day: (void | int),
	yearday: (void | int),
	isoweekyear: (void | int),
	isoweek: (void | int),
	week: (void | int),
	weekday: (void | int),
	hour: (void | int),
	min: (void | int),
	sec: (void | int),
	nsec: (void | int),
};

Hare defines a “chronology” as a system used to name and order moments in time. In practice, a chronology is the combination of a calendar (for handling days) and a wall clock (for handling times throughout a day). The de facto international chronology is defined in the ISO 8601 standard, which uses the proleptic Gregorian calendar and the 24-hour timekeeping system. Alternatively, a theoretical third-party “mayan” module could implement the Mesoamerican Long Count calendar.

Datetimes hold the same information as moments (locality, date, time), but also hold “chronological” values corresponding to units like “years”, “hours”, etc. These values are cached for performance and utility, hence the (void | int) type. Datetimes created and handled by the datetime module are guaranteed to be internally valid and consistent, given you don’t modify the fields yourself.

Here’s what creating a datetime with datetime::new looks like. We specify the timezone and zone offset first, then its fields.

// short form
// 0000-01-01 00:00:00.000000000 +0000 UTC, UTC
let dt = datetime::new(chrono::UTC, 0000);

// 1995-09-01 00:00:00.000000000 +0000 UTC, UTC
let dt = datetime::new(chrono::UTC, 0000, 1995, 09);

// long form
// 2038-01-19 03:14:07.000000000 +0000 UTC, UTC
let dta = datetime::new(chrono::UTC, 0000, 2038, 01, 19, 03, 14, 07, 000000000);

const Tokyo = &chrono::tz("Asia/Tokyo")!;

// 2038-01-19 12:14:07.000000000 +0900 JST, Asia/Tokyo
let dtb = datetime::new(
	Tokyo, 9 * time::HOUR,
	2038, 01, 19, 12, 14, 07, 000000000,
	// Notice the ^^ hour is as observed in Tokyo
);

assert(datetime::eq(dta, dtb)) // datetimes are equivalent

// current system time
let now = datetime::now();

Internally, for the simple case of a UTC datetime, the values are submitted as-is for conversion to a moment. However, for non-normal timezones (ones with offsets other than 0), the library will do a little adjustment before converting.

Datetimes can exhibit different chronological values under different timezones. The underlying moment values stay the same, but the cached calendar and wall-time values adapt.

fn show(ls: []datetime::datetime) void = for (let i = 0z; i < len(ls); i += 1) {
	let dt = ls[i];
	fmt::println(
		// EMAILZ == "%a, %d %b %Y %H:%M:%S %z %Z"
		datetime::asformat(datetime::EMAILZ, &dt)!,
		"\t", dt.loc.name,
	)!;
};

let dt = datetime::new(chrono::UTC, 0000, 1995, 09, 24, 01, 00, 00, 000000000)!;
show([
	datetime::in(&chrono::tz("Pacific/Honolulu")!, dt),
	datetime::in(&chrono::tz("America/New_York")!, dt),
	datetime::in(&chrono::tz("Europe/Amsterdam")!, dt),
	datetime::in(&chrono::tz("Asia/Kathmandu")!, dt),
	datetime::in(&chrono::tz("Asia/Tokyo")!, dt),
	datetime::in(&chrono::tz("UTC")!, dt),
]);

This is the same datetime, the same moment in time, under different timezones. Nepal’s quirky UTC offset is handled just fine.

Sat, 23 Sep 1995 15:00:00 -1000 HST      Pacific/Honolulu
Sat, 23 Sep 1995 21:00:00 -0400 EDT      America/New_York
Sun, 24 Sep 1995 02:00:00 +0100 CET      Europe/Amsterdam
Sun, 24 Sep 1995 06:45:00 +0545 +0545    Asia/Kathmandu
Sun, 24 Sep 1995 10:00:00 +0900 JST      Asia/Tokyo
Sun, 24 Sep 1995 01:00:00 +0000 UTC      UTC

Datetimes handle timezone transitions too.

const Amst = &chrono::tz("Europe/Amsterdam")!;

// In Europe/Amsterdam, on March 26th, 1995
// the clock jumps forward 1 hour at 02:00 CET.
fmt::println("CET -> CEST")!;
show([
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 03, 26, 00, 00)!),
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 03, 26, 00, 30)!),
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 03, 26, 01, 00)!), // transition
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 03, 26, 01, 30)!),
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 03, 26, 02, 00)!),
]);

// In Europe/Amsterdam, on September 24th, 1995
// the clock jumps back 1 hour at 03:00 CEST.
fmt::println("CEST -> CET")!;
show([
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 09, 24, 00, 00)!),
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 09, 24, 00, 30)!),
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 09, 24, 01, 00)!), // transition
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 09, 24, 01, 30)!),
	datetime::in(Amst, datetime::new(chrono::UTC, 0, 1995, 09, 24, 02, 00)!),
]);

Notice at the change in time, offset, and zone abbreviation.

CET -> CEST
Sun, 26 Mar 1995 01:00:00 +0100 CET      Europe/Amsterdam
Sun, 26 Mar 1995 01:30:00 +0100 CET      Europe/Amsterdam
Sun, 26 Mar 1995 03:00:00 +0200 CEST     Europe/Amsterdam
Sun, 26 Mar 1995 03:30:00 +0200 CEST     Europe/Amsterdam
Sun, 26 Mar 1995 04:00:00 +0200 CEST     Europe/Amsterdam
CEST -> CET
Sun, 24 Sep 1995 02:00:00 +0200 CEST     Europe/Amsterdam
Sun, 24 Sep 1995 02:30:00 +0200 CEST     Europe/Amsterdam
Sun, 24 Sep 1995 02:00:00 +0100 CET      Europe/Amsterdam
Sun, 24 Sep 1995 02:30:00 +0100 CET      Europe/Amsterdam
Sun, 24 Sep 1995 03:00:00 +0100 CET      Europe/Amsterdam

It’s important to distinguish “timezones” from “zones”. If timezones are a region, a zone is a sort of “state” which a timezone observes (CET, CEST, etc.), depending on the date & time. Each timezone has a set of zones it observes. It’s also why datetime::new() asks for a zone offset, to disambiguate between possible zones. I told you it was complicated.

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

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

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

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

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

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

// A timezone state, with an offset for calculating localized civil time
type zone = struct {
	// The offset from the normal timezone (2 * time::HOUR)
	zoffset: time::duration,

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

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

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

Formatting and parsing

In the real world, datetimes often exist as representations, and we parse and format these representations. We have a family of functions and predefined layouts centred around the datetime::format function.

let dt = datetime::new(chrono::UTC, 0, 2038, 01, 19, 03, 14, 07)!;
fmt::println(datetime::asformat("%Y-%m-%d %H:%M:%S.%N %z %Z", &dt)!)!;
fmt::println(datetime::asformat(datetime::STAMP_NANO, &dt)!)!;
fmt::println(datetime::asformat(datetime::EMAILZ, &dt)!)!;
fmt::println(datetime::asformat(datetime::POSIX, &dt)!)!;

Output:

2038-01-19 03:14:07.000000000 +0000 UTC
2038-01-19 03:14:07.000000000
Tue, 19 Jan 2038 03:14:07 +0000 UTC
Tue Jan 19 03:14:07 UTC 2038

Parsing is a bit trickier. Hare holds the guarantee that datetimes are internally valid and consistent, so we validate at the moment of parsing. One can use the datetime::from_str function for an immediate parse if there’s sufficient information given, or an immediate error.

// 2038-01-19 00:00:00.000000000 UTC
let dt = datetime::from_str("%Y-%m-%d", "2038-01-19")!;

However, Hare provides a more delicate, progressive way of parsing with the datetime::builder type. This is internally just a datetime, but waives the guarantee of validity, allowing its fields to hold possibly incorrect values. The user gradually builds a datetime with incremental calls to datetime::parse and other modifications, then attempts to manifest a real, valid datetime with datetime::finish.

let mock = datetime::newbuilder();

datetime::parse(&mock, "Year:  %Y", "Year:  2038")!;
datetime::parse(&mock, "Month: %m", "Month: 01")!;
mock.day = 19;

let dt = match (datetime::finish(&mock, datetime::strategy::YMD)) {
case dt: datetime::datetime =>
	yield dt;
case datetime::insufficient =>
	fmt::println("Not enough information")!;
case datetime::invalid =>
	fmt::println("Datetime is invalid")!;
};

Datetime arithmetic

Using what we’ve covered so far, we’re able to perform simple arithmetic using instants.

let dta = datetime::new(chrono::UTC, 0, 2000, 01, 01)!;
let dtb = datetime::new(chrono::UTC, 0, 2000, 01, 02)!;

let a = datetime::to_instant(dta);
let b = datetime::to_instant(dtb);
show([
	datetime::from_instant(a, chrono::UTC),
	datetime::from_instant(b, chrono::UTC),
]);

let delta = time::diff(a, b); // 86400 seconds (~1 day)
fmt::println("delta:", delta / time::SECOND)!;

let c = time::add(b,
	- 1 * time::HOUR
	+ 20 * time::MINUTE
	- 500 * time::MILLISECOND,
);
show([
	datetime::from_instant(c, chrono::UTC),
]);

fmt::printfln("a: {}.{}", a.sec, a.nsec)!;
fmt::printfln("b: {}.{}", b.sec, b.nsec)!;
fmt::printfln("c: {}.{}", c.sec, c.nsec)!;

Output:

Sat, 01 Jan 2000 00:00:00 +0000 UTC      UTC
Sun, 02 Jan 2000 00:00:00 +0000 UTC      UTC
delta: 86400
Sat, 01 Jan 2000 23:19:59 +0000 UTC      UTC
a: 946684800.0
b: 946771200.0
c: 946768799.500000000

However, to add and subtract datetimes, the datetime module provides some basic functionality. We introduce the datetime::period type to reason about calendrical differences between datetimes, as opposed to “absolute” differences between instants.

// Represents a span of time in the proleptic Gregorian calendar, using relative
// units of time. Used for calendar arithmetic.
type period = struct {
	eras: int,
	years: int,
	// Can be 28, 29, 30, or 31 days long
	months: int,
	// Weeks start on Monday
	weeks: int,
	days: int,
	hours: int,
	minutes: int,
	seconds: int,
	nanoseconds: i64,
};

We can find the difference between datetimes.

let a = datetime::new(chrono::UTC, 0, 1982, 06, 25, 20, 19)!;
let b = datetime::new(chrono::UTC, 0, 2017, 10, 03, 20, 49)!;

// years:35 months:3 days:8 hours:0 minutes:30 seconds:0 nanoseconds:0
let p = datetime::diff(a, b);

Periods are not durations; years and months contain variable numbers of days, and the length of a day varies by timezone transitions and leap seconds. Datetime arithmetic is inherently highly irregular. With so much complexity, it is the weakest point of the library (the same is true across languages), but Hare tries to provide something sensible. Third-party libraries, like a scheduling library, are expected to provide their own comprehensive set of operators to suit their needs.

Further datetime-based arithmetic is under development. We have “add” and “hop” functions which allow users to manipulate dates in the context of the calendar, such as finding the date one year from today, or finding the datetime at the start of last month. These functions still require some refinement, so we’ll save the introduction for later.


We wish you well in your datetime endeavours. If you’d like to discuss or contribute to chronology in Hare, reach us on IRC, or write to us at:

Happy time-travelling.


  1. Authur David Olson: “Although the tz database does not support time on other planets, it is documented here in the hopes that support will be added eventually.” — https://data.iana.org/time-zones/theory.html#planets ↩︎