This is a document intended to persuade you, in the course of your computer doings, to use a particular representation of time called stiptime. Why a particular representation of time, and why so staunch about it? The main reason is that time is not handled well by libraries in any programming language, and misuse leads to subtle bugs. Like memory allocators, representations of time are not something application developers give much thought to. Unlike memory allocators, you cannot expect your time libraries to “just work.” This leads to a wide class of bugs that are unintuitive, difficult to anticipate, and difficult to test for. stiptime has been designed to avoid many of these pitfalls. For a large number of time-related tasks, stiptime just works.
If reading this document causes your head to spin, it has achieved its goal. Use stiptime and go on with your day.
stiptime is a contemporary terrestrial time format meant to reduce the number of time-related bugs in computer programs. It makes certain compromises to be easier to implement on most operating systems and programming languages circa 2015-07-10T22:54:32.0Z.
stiptime's format is UTC represented as follows:
YYYY-MM-DDThh:mm:ssZ
where
YYYY: four digit year MM: zero padded month DD: zero padded day hh: zero padded 24hr hour mm: zero padded minute ss: zero padded second, with a required fractional part
You may notice that this is exactly the ISO 8601 date format with Z used to represent UTC. That's because it is! Z is the nautical timezone for UTC. Since ‘Zulu’ is the NATO phonetic representation of Z, UTC (and by extension stiptime) can also be referred to as Zulu time. Z is used instead of +00:00 as it unambiguously signals UTC and not “put in any arbitrary timezone here.” It is also short and compact.
Lesser stiptime is to be used only when necessary. Lesser stiptime is Unix time or POSIX time, “fractional seconds since the epoch, defined as 1970-01-01T00:00:00Z. Seconds are corrected such that days are exactly 86400 seconds long.” The reason stiptime is preferred over lesser stiptime is due to that last correction. Since UTC occasionally contains days longer than 86400 seconds, lesser stiptime cannot encode positive leap seconds unambiguously. The consequences of this will be described in a later section. stiptime is preferred over lesser stiptime, but lesser stiptime is definitely preferred over bizarro time.
Bizarro time is any time format that is used for absolute time that isn't stiptime. Some of these may seem obvious and good, but a later section will show their pitfalls.
Using bizarro time will eventually lead to “fun” and subtle bugs in your programs. The inevitability of dealing with all of the contingencies of bizarro time (and the libraries that handle it) leads to a profound frustration and ennui — this is when you are stuck in time jail. The entrances to time jail are many, but can be broadly classified into implementation-induced time jail and timezone-induced time jail.
Writing proper time-handling libraries is hard, which means that most time libraries have quirks, unexpected behavior or outright bugs. Some of these even occur at the operating system level. This section mostly describes the datetime module included in the python standard library, but other quirks are documented as well.
python 2.7 datetime lets you print a date that itself cannot parse
import datetime from dateutil import tz offset = tz.tzoffset(None, -76060) dt = datetime.datetime(2015, 1, 1, 0, 0, 0, tzinfo=offset) a = dt.strftime(‘%Y-%m-%dT%H:%M:%S %z’) a ‘2015-01-01T00:00:00 -0700’ datetime.datetime.strptime(a, ‘%Y-%m-%dT%H:%M:%S %z’) ValueError: ‘z’ is a bad directive in format ‘%Y-%m-%dT%H:%M:%S %z’
it lets you print a date that it can parse most of the time, except that one time when you have zero microseconds and then it can't
See https://bugs.python.org/issue19475 for the problem, and see zulu.py for the ‘solution.’
import datetime a = datetime.datetime(2015, 1, 1, 0, 0, 0, 0).isoformat() b = datetime.datetime(2015, 1, 1, 0, 0, 0, 123).isoformat() a ‘2015-01-01T00:00:00’ b ‘2015-01-01T00:00:00.000123’ datetime.datetime.strptime(a, ‘%Y-%m-%dT%H:%M:%S’) datetime.datetime(2015, 1, 1, 0, 0) datetime.datetime.strptime(b, ‘%Y-%m-%dT%H:%M:%S’) ValueError: unconverted data remains: .000123
datetime.datetime.strptime(b, ‘%Y-%m-%dT%H:%M:%S.%f’) datetime.datetime(2015, 1, 1, 0, 0, 0, 123) datetime.datetime.strptime(a, ‘%Y-%m-%dT%H:%M:%S.%f’) ValueError: time data ‘2015-01-01T00:00:00’ does not match format ‘%Y-%m-%dT%H:%M:%S.%f’
it can't understand leap seconds
import datetime datetime.datetime(2015, 6, 30, 23, 59, 60) ValueError: second must be in 0..59
it can't tell you what timezone datetime.now() is
import datetime datetime.datetime.now().tzinfo is None true datetime.datetime.now().utcoffset() is None true datetime.datetime.utcnow().tzinfo is None true datetime.datetime.utcnow().utcoffset() is None true datetime.datetime.now().isoformat() ‘2015-07-15T15:36:23.591431’ datetime.datetime.utcnow().isoformat() ‘2015-07-15T22:36:28.431225’
it can't mix timezone aware datetimes with naive datetimes (why have naive datetimes to begin with?)
import datetime from dateutil import tz a = datetime.datetime(2015, 1, 1, 0, 0, 0, tzinfo=tz.tzoffset(None, -76060)) b = datetime.datetime(2015, 1, 1, 0, 0, 0) a == b TypeError: can't compare offset-naive and offset-aware datetimes
dateutil is not part of the standard library
You'll have to venv or wheel it wherever you go. A (non-leap second aware) stiptime parser is a single line of python and requires nothing more than the standard library. A stiptime formatter is 4 lines, and also requires nothing more than the standard library.
dateutil can't understand leap seconds
import dateutil.parser dateutil.parser.parse(‘2015-06-30T23:59:60’) ValueError: second must be in 0..59
parser.parse works great, except when it silently doesn't
(note, running this example will yield different results depending on your local timezone. lol.)
import dateutil.parser a = dateutil.parser.parse(‘2015-01-01T00:00:00 PST’) b = dateutil.parser.parse(‘2015-01-01T00:00:00 PDT’) c = dateutil.parser.parse(‘2015-01-01T00:00:00 EST’) a.isoformat() ‘2015-01-01T00:00:00-08:00’ # great! b.isoformat() ‘2015-01-01T00:00:00-08:00’ # uh... c.isoformat() ‘2015-01-01T00:00:00’ # uhhhhhhh c == a TypeError: can't compare offset-naive and offset-aware datetimes
time.time()'s definition is incorrect
According to the python docs, time.time() “[returns] the time in seconds since the epoch as a floating point number.” Except it doesn't, as (at least on unix) days are corrected to be 86400 seconds long. Thus the true definition of time.time() should be “the time in seconds since the epoch minus any UTC leap seconds added since the epoch.”
These are a collection of gotchas that occur even if your timezone-handling libraries are perfect. They arise purely out of not using UTC for internal computations.
dates which represent the same moment in time can have different weekdays or other attributes
import dateutil.parser a = dateutil.parser.parse(‘2015-06-17T23:00:00 PDT’) b = dateutil.parser.parse(‘2015-06-18T06:00:00 UTC’) a == b True a.weekday() 2 b.weekday() 3
it‘s easy to write code thinking it’s in one timezone when it's really in another
you can have timezone un-aware code in a timezone that shifts (PST -> PDT).
Now you have graphs wrapping back on themselves, systems restarting repeatedly for an hour, or silent data corruption. This is the classic timezone bug.
ambiguous encoding
If you're not careful, you can encode dates which refer to two instances in time. The date ‘2015-11-01T01:30:00 Pacific’ or ‘2015-11-01T01:30:00 America/Los_Angeles’ refers to two distinct times: ‘2015-11-01T01:30:00-0800’ and ‘2015-11-01T01:30:00-0700’. Imagine an alarm clock or cron job which triggers on that.
illegal dates that aren't obviously illegal
The opposite of ambiguous encoding: did you know that ‘2015-03-08T02:30:00 Pacific’ doesn't exist? It jumped from 2015-03-08T01:59:59 immediately to 2015-03-08T03:00:00.
what does a configuration file look like where any timezone is allowed?
You've now required everyone to convert every timezone into every timezone, instead of every timezone into one (UTC):
[‘2015-06-18T05:00:00+00:00’ ‘2015-06-17T23:00:00-07:00’, ‘2015-06-18T06:00:00-10:00’, ]
Finally, there is a small jail associated with errors occurring due to leap seconds themselves. Unfortunately, stiptime is not immune to these.
ambiguous unix time
2015-06-30T23:59:59.0Z -> 1435708799.0 2015-06-30T23:59:60.0Z -> 1435708799.0
illegal unix time
This hasn't happened yet, but if a negative leap second ever occurs, there will be a floating point time which ‘never happened.’
calculating durations using start/stop times
Of course, calculating durations across leap seconds can cause slight errors, mistriggers or even retriggers. Since they happen infrequently (and TAI is not commonly available), stiptime has chosen to be susceptible.
It is my hope that after reading this document, you will consider stiptime and, occasionally, lesser stiptime to be the proper way to represent time in stored formats. I firmly believe time should be displayed to humans in their local format — but only in ephemeral displays. A local absolute time should never touch a disk. Join me in using stiptime and get back some sanity in your life.