Dates and times
Dates and times can be tricky to interact with while programming.
One approach is to represent all times in UNIX epoch milleseconds and let a thin layer of frontend code handle the conversion to local time for rendering. This works well for system-level events like created_at
or logged_in_at
, and it might be enough for some simple business domains.
But epoch milliseconds are insufficient for more complicated domains that involve scheduling future times, time ranges, and users coordinating across time zones.
W3C Terminology
I’ve come to follow the terminology defined by the W3C here:
https://www.w3.org/International/articles/definitions-time
Examples
Let’s start with colloquial examples to illustrate a few concepts:
# date with a time zone
October 21, 2021 in America/Chicago
# date with a named UTC offset
October 21, 2021 Central Daylight Time
# date time with a numeric UTC offset
8:30AM on October 21, 2021 UTC-06:00
Notice the different types: date vs date time and offset vs time zone.
Key distinctions
UTC offset vs time zone
A UTC offset is an hour and minute offset from UTC, represented either as a numeric offset like “UTC+06:00” or a named offset like “Central Daylight Time (CDT)”.
A time zone is a geographic area with a label like “America/Chicago”. A time zone can be represented as a GeoJSON value – it’s a geospatial region on the planet.
There is a many-to-many relationship between UTC offsets and time zones. A time zone’s UTC offset usually changes twice per year for daylight savings, and also may be updated due to political decisions. The authoritative mapping between time zones and offsets is the IANA database.
Incremental time vs wall time
Incremental time is based on a progression of fixed integer units that increase monotonically from a specific point in time (called the “epoch”). UTC and offset-based times are different flavors of incremental time, and all can be converted to the UNIX epoch representation:
# UTC (Z is shorthand for UTC)
2021-10-23T10:30:00Z
# numeric offset
2021-10-23T10:30:00+06:00
# named offset
2021-10-23T10:30:00CDT
# seconds since 00:00:00 UTC 1 January 1970
1634257344
Wall time corresponds to what a person would recognize the time to be if they looked at a clock and/or calendar mounted on a wall in a particular place. In its most basic form it’s represented like this:
2021-10-01T10:30:00 # no offset
Wall time can be used without a time zone to represent something like “this year my birthday is on Tuesday June 8, 2021”, which is true regardless of time zone or UTC offset. But in my experience, most business use cases for wall times involve anchoring to a specific place.
Date vs date time
A “date” is not the same as a “date time”.
A “date” is a time range from midnight to midnight, optionally paired with a time zone. Usually this is a 24-hour range, but there are some exceptions for years with leap seconds and on daylight savings transitions.
A “date time” is a particular point in time, which may or may not be anchored to a time zone.
The poorly name JavaScript Date
object represents date times, not dates.
Serializable types
I find the following types provide all the functionality I need. These can be defined as opaque types in TypeScript, scalars in GraphQL, etc.
UtcTimestamp
- A point in incremental time with millisecond resolution.
- Example:
2022-05-23T23:12:05.123Z
(string) - Usage: database and system times (createdAt, revisedAt, etc).
- Always includes the Z at the end indicating UTC offset of 0.
- Persist with PostgreSQL’s timestamp(3) type.
FloatingDate
- A floating date, unanchored to a specific time zone or offset.
- Example:
2022-04-23
(string) - UTC offset is always undefined. Could be paired with time zone; see below.
- Usage: domain-specific dates like pick up dates, invoice issue dates.
- Persist as date- or string-type column in PostgreSQL.
FloatingDateTime
- A floating date and time with millisecond resolution, unanchored to a specific time zone or offset.
- Example:
2022-05-23T06:00:00.123
(string) - UTC offset is always undefined (i.e. no
Z
). Could be paired with time zone; see below. - Usage: domain-specific date times. For example, delivery appointment window for a warehouse could be represented with two of these (9am to 11am on May 5th 2021).
- Persist as timestamp- or string-typed column in PostgreSQL.
WallDate
- A wall time date.
- Example:
2022-04-23|America/Chicago
- UTC offset can be computed via IANA table lookup.
- Serialize and persist as a single string for ease-of-use.
WallDateTime
- A point in wall time with millisecond resolution.
- Example:
2022-05-23T06:00:00.000|America/Chicago
- UTC offset can be computed via IANA table lookup.
- Serialize and persist as a single string for ease-of-use.
IANA database
The IANA database is standard mapping of time zones to IANA offsets. You can download the dataset here:
https://www.iana.org/time-zones
Here’s an example of what the rules look like:
# Zone NAME STDOFF RULES FORMAT [UNTIL]
Zone America/Chicago -5:50:36 - LMT 1883 Nov 18 12:09:24
-6:00 US C%sT 1920
-6:00 Chicago C%sT 1936 Mar 1 2:00
-5:00 - EST 1936 Nov 15 2:00
-6:00 Chicago C%sT 1942
-6:00 US C%sT 1946
-6:00 Chicago C%sT 1967
-6:00 US C%sT
For Chicago, the offset has been stable since the mid-twentieth century (with annual daylights savings).
There are periodic changes to the IANA database, and it’s fun to see the world come together and collaborate in this arcane part of the Internet. Examples:
- Fiji skipping daylights savings (link)
- Palestine declaring its time zone specification (link)
- Amidst the Russian military build up on the Ukrainian border, a proposal to use the correct spelling of Kyiv (transliteration from Ukrainian) instead the incorrect Kiev (transliteration from Russian) (link)