I have mixed feelings about the Go time package's time formatting strings

January 9, 2022

Over on Twitter, Thomas H. Ptacek said something about how Go's time.Parse specifies the format of times to parse. I retweeted it because I have feelings in the same direction, but really I have some mixed feelings about Go's approach here. Let's start with the documentation, which is best found in the Constants section of the time package.

To summarize, you both parse and format times by telling Go how a magic time would look either on input or on output. This magic time is Monday, January 2, 15:04:05, 2006, in time zone seven hours west of GMT, which is chosen so all sets of digits are different (including the hours if they're not in 24 hour format). A typical time format string from one of my programs is '2006-01-02 15:04:05 -0700'.

Perhaps the most common alternate time format approach is to mimic C's strftime() and strptime(). If you're using C, you're getting whatever format options your platform C library provides. If you're in a non-C language, you might be getting the POSIX standard or a platform version, potentially with non-POSIX things, such as Linux's glibc version. See, for example, the Python documentation.

(So the first problem with using the C strftime() format is that there isn't 'a' such format, there are a bunch of different ones. If you're implementing your own standard library (as Go is), you would have to decide which extensions and features you do and don't support.)

As someone who doesn't use it all of the time, the C strftime() approach has two drawbacks for me, which both come from the fact that it uses more or less opaque %-markers. First, it's opaque when encountered in code. If I hit a strftime() or strptime() format string, I generally have little idea what the output or input will look like, because I don't remember the %-operators off the top of my head; to work out what it does, I'm going to have to try it out somehow (often I reach for date or Python). Second, it's uncertain to write; after looking up the formatting operators I think I want in the manpage, hopefully I've picked the right ones and written my string correctly but I'm absolutely going to have to check my assumptions somehow.

The Go approach feels excessively clever, but it's also somewhat more obvious than strptime()'s opaque operators. Once I've looked up the magic time in the time documentation (which I have to do regularly), I can be pretty certain that '2006-01-02 15:04' is going to do what I expect. And if I encounter that in Go source code, either for formatting or parsing time, I can predict pretty well what it's going to do without even remembering the magic time.

To get more clarity and explicitness, you'd probably want to have explicit names for each time field, perhaps something like '%{year}-%{month}-%{day} %{24hr}-%{min}'. But this requires coming up with good names for all of the fields and then figuring out things like how to specify how many digits the day uses (ie, is it '2' or '02'). All of these are solvable but they're more complex and more verbose, which is not what the Go developers are generally enthused about.

(Perhaps there's a radically different and better way lurking in other languages. In a way I'd hope so; you'd hope that by now someone in some language has come up with a really good solution.)

The cleverness of Go's time formatting strings makes me vaguely twitchy, but on the whole I think I prefer it to the C strftime() approach that dominates other languages. But I do wish there was a way to make time.Parse() less exposed to accidentally reversing the two arguments.

(One obvious API would be to have a time analog to regexp's ability to pre-compile regular expressions. Then you could precompile your time format and call 'precomp.Parse(inputstr)' to parse the actual time. As a bonus, this might improve efficiency when repeatedly parsing input with the same format ('layout'), which is something that various programs do regularly.)

Comments on this page:

From at 2022-01-11 03:56:03:

Or it could have used ISO8601 names...

In principle I like this idea a lot, since I already tend to add a comment showing a formatted date in code that expects or outputs a specific format. In a sense, Go just makes this comment executable – which, when feasible, is the ideal way to keep code and comment from diverging.

The problem I have with the idea is that a formatted date is not sufficient to identify certain semantics.

The example that comes to mind right away – because I’ve had to deal with it several times over the years – is the strftime %U/%V/%W set of formats for the week number. They yield the same result for many datetimes, but they aren’t the same. I suppose if you chose the magic date very carefully you could use it to distinguish these from each other, but I believe you cannot cover all of possible ambiguities between all sets of similar formats with a single a magic date.

Even if you could, however: would you want to? Looking at the example date wouldn’t tell you which semantic you’re looking at. You’d have to compute backward in your head to figure out which of week number semantic must have been used. Using an example date makes the code less self-documenting in this respect, rather than more.

As far as I can tell the way Go circumvents this issue is by simply not supporting layout components that could lead to such semantic ambiguities. This shouldn’t be too bad when it comes to output; you get to write tedious imperative code rather than using a declarative DSL (call a method on the datetime for each component you want, and print or concatenate them), but you’re starting from a datetime value and composing a string. I expect it to be terrible in the parse direction, though, where you are starting from a string and trying to get a datetime: you get to write all the tokenising and parsing yourself. The difference between how easy it is to deal with inputs that fit into Go’s supported layouts vs those that don’t has to be precipitous. Worse still, the set of methods looks like it doesn’t even support computing most such components, in which case you get to write the datetime math yourself as well – in the middle of imperative code. Not my idea of a good time.

This is a uniquely-stupid idea from the same people who think constantly parsing ad hoc character-stream formats is anything resembling a good idea.

A proper language, such as Common Lisp, uses a structured approach to handling such things. The function GET-DECODED-TIME returns multiple values corresponding to this information, which can be collected into a list, as one way of handling it, by MULTIPLE-VALUE-LIST. Follows is one way to use this, ignoring timezone and daylight savings time for brevity; this code was written from memory and lightly-tested, but I've written it several times over the years, for cases such as this:

(format t "~{~&~[Mon~;Tues~;Wednes~;Thurs~;Fri~;Satur~;Sun~]day, ~
             ~4,'0D-~2,'0D-~2,'0D ~@{~2,'0D~^:~}~}"
  (cddr (nreverse (multiple-value-list (get-decoded-time)))))

Friday, 2022-01-14 16:29:12

The FORMAT is a nice little language for such things, and notice I can format it nicely for contexts such as this. A great many flaws arise from fools passing around strings, instead of parsing them once and then using a real data structure to hold the information.


It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.

By Sam Birch at 2022-01-14 19:28:14:

I've been seeing the ICU date formatting mini-language getting used more lately. I'm finding I like it better than either strptime's or Go's.

I suppose I also should've mentioned this Perlis quote:

The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information.

Written on 09 January 2022.
« Good web scraping is not just about avoiding load
The complexity of seeing if your Prometheus Alertmanager is truly healthy »

Page tools: View Source, View Normal, Add Comment.
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Sun Jan 9 22:57:37 2022
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.