Plan 9's date and time formatting APIs sucked. It was a subset of the unix date and time APIs, which also suck. It was only aware of two timezones: local and UTC. And, unlike Unix, we had neither strftime, nor strftime. Instead, we had a bunch of buggy and quirky time parsers scattered through the source tree.
We've had a crappy home-rolled date parser in seconds(1), more than one in the upas mail server source tree to deal with mail formats in emails, another one in the http server, and a bunch more hidden under rugs and in dusty corners.
And on IRC, I've been trying to help users that are trying to compute times in one timezone and move them to another. Our APIs may as well not have existed.
So I fixed it.
I aimed to write a library that is adequate, without being complicated. Flexible date and time formatting were critical. It also needs to be simple to compute dates such as "tomorrow at this time", as well as "24 hours from now". Becasue of timezones transitions, the two are not the same.
The manpage is here
The API that I ended up settling on is this:
#include <u.h> #include <libc.h> typedef struct Tm Tm; typedef struct Tmfmt Tmfmt; typedef struct Tzone Tzone; struct Tm { int nsec; /* nanoseconds (range 0..1e9) */ int sec; /* seconds (range 0..59) */ int min; /* minutes (0..59) */ int hour; /* hours (0..23) */ int mday; /* day of the month (1..31) */ int mon; /* month of the year (0..11) */ int year; /* C.E year - 1900 */ int wday; /* day of week (0..6, Sunday = 0) */ int yday; /* day of year (0..365) */ char zone[]; /* time zone name */ int tzoff; /* time zone delta from GMT, seconds */ Tzone *tz; /* time zone */ }; Tzone *tzload(char *name); Tm *tmnow(Tm *tm, char *tz); Tm *tmtime(Tm *tm, vlong abs, Tzone *tz); Tm *tmtimens(Tm *tm, vlong abs, int ns, Tzone *tz); Tm *tmparse(Tm *dst, char *fmt, char *tm, Tzone *zone, char **ep); vlong tmnorm(Tm *tm); Tmfmt tmfmt(Tm *tm, char *fmt); void tmfmtinstall(void);
This API largely maintains interoperability with the legacy
time APIs. No new types are introduced. Instead, Tm
is augmented to support the new functionality.
The largest amount of effort went into the design of the time parsing API
One of the largest deficiencies with the old date and time APIs was their complete ignorance of timezones. They had the concept of local time, and of GMT. There was no way to handle any other timezones. Most software in the system hard coded tables of timezone names and their offsets. These were out of date.
The new APIs add functionality for loading timezones by name. These timezones can be passed to the various time manipulation functions in order to operate on them, allowing for correct handling of timezones.
Timezones are an opaque type, and the only way to get the offset out of them is by computing a time with them. This turns out to be all we really need them for. So far, it looks like getting timezone offsets in isolation is simply unnecessary.
The most useful new functions are the parsing and formatting APIs. These are designed for flexible time handling, sufficient to replace all hand-rolled date parsing code through the whole OS.
Formatting is done using the fmt family of APIs, such as
print
, dofmt
, and the usual machinery
around that. The format character used for time formatting is
τ
. Yes, that's the greek letter tau -- you can input
it by pressing alt-*-t in sequence. The tmfmt
function
is used to produce a Tmfmt struct that will format a date according
to a format string.
In the intial versions of the API, I looked to Go for guidance, with its magic date to determine formatting. However, as I looked more closely at the implementation, I found that I didn't like it very much.
In addition to the difficulty in remembering which magic numbers mapped to which date fields, I realized that it was more difficult than necessary to parse the format string when there were no spaces between date components.
Jan2020
is something that I would like to be able to generate.So, on a recomemendation, I looked at moment.js. While the API they provide is not a good fit for Plan 9, their format strings are top notch. I didn't take it directly, but the inspiration is clear.
The format string that is implemented in 9front is below:
For example, this program computes the date one week from now, ignoring any time zone translations.
Computations were the next large feature to design. In many date and time libraries, there is a large API surface area dedicated to this. In the Plan 9 time library, the decision was the best API was tiny.
For computing dates at an absolute offset in the future, it's simplest to just compute the number of seconds, and add it to the Unix time offset, constructing a date from this. Because unix time is an absolute time, with no adjustments, this just works.
Below is an example program that computes a day 1 week in the future, with no time zone adjustments:
#include <u.h> #include <libc.h> #define Fmt "?WW, ?DD ?MMM ?YYYY ?hh:?mm:?ss ?Z" void main(void) { Tm tm; vlong t; Tzone *tz; tmfmtinstall(); if(argc != 2) sysfatal("usage: %s date\n"); if((tz = tzload("local")) == nil) sysfatal("load timezone: %r"); if(tmparse(&tm, argv[1], fmt, tz) == nil) sysfatal("parse date: %r"); t = tmnorm(&tm); t += 7*24*3600; tmtime(&tm, t, tm->tz); print("%τ", tmfmt(&tm, Fmt)); }
But often, you want to be able to say "Set an alarm for 4 AM tomorrow". Adding an absolute offset doesn't work in this case, so we need a way to add adjusted offsets.
This is done with the tmnorm
API. Tmnorm
does two things: It computes the absolute unix time of
a Tm
structure, and it wraps out of range
values. So, for example, Jan 32nd becomes Feb 1st.
This means that you can simply add your offsets to the Tm structure, normalize it, and you have a timezone-adjusted offset into the future. Here's an example like the previous one, but with DST and timezone adjustments:
#include <u.h> #include <libc.h> #define Fmt "?WW, ?DD ?MMM ?YYYY ?hh:?mm:?ss ?Z" void main(void) { Tm tm; vlong t; tmfmtinstall(); if(argc != 2) sysfatal("usage: %s date\n"); if((tz = tzload("local")) == nil) sysfatal("load timezone: %r"); if(tmparse(&tm, argv[1], fmt, local) == nil) sysfatal("parse date: %r"); tm.mday += 7; tmnorm(&tm); print("%τ", tmfmt(&tm, Fmt)); }
There's not much to this API. In the end, the goal of a minimalist, easy to understand, easy to implement date and time parsing API seems to be achieved, and the amount of code dedicated to parsing dates has shrunk quite a bit.