Choosing the correct representation for storing Dates and Times

We are building a parcel tracking app for PostNL, the Dutch Postal Services. Previously we only supported tracking packages within The Netherlands, but now we are expanding to international shipments. A problem came up: how do we show the delivery date and time when all we have from the API is a single string: 2019-08-28T03:23:45Z?

This DateTime appears to be in the middle of the night, but that’s because in the UTC time zone. We need to convert it...but to what? Do we show this date in the users’ current time zone? Or does it make more sense to show the time in the time zone where the package was actually delivered?

Three different representations

There are multiple ways of representing the same moment in time. Each representation can store one or more distinct pieces of information. The more information we have, the wider we can use the DateTime unit. In the example of tracking package delivery times, we want to know two different things: the local date and time, as well as the absolute UTC date and time.

Let’s look at three different representations of the same moment in time. Each representation has a unique use case for which it is ideal. In this example, we’ll look at the start of the 2014 Winter Olympics opening ceremony in Sochi, Russia:

  • 2014-02-07T16:14:00Z
    A single piece of information, useful for things like server logs, where locations aren’t relevant.
  • 2014-02-07T20:14:00+04:00
    Two pieces of information, useful for vacation photos or package delivery times.
  • 2014-02-07T20:14:00+04:00 Europe/Moscow
    Three pieces of information, needed when manipulating date times or for storing dates in the future.

All three representations refer to the same moment in time, but they are not equivalent in the amount of information they represent. A local date and time plus a UTC offset can be converted to a UTC date and time, but not the other way around. We will look at each representation in detail, to see for which use case it is ideal.

Note 1: For simplicity, in this article I’ll use terms like LocalDateTime and TimeZone; these are types used in Java 8. But this article isn’t specific to a single programming language. Look up what the equivalent types are in your language of choice.

Note 2: This article ignores relativity. I’m assuming all readers currently live on Earth and only have to deal with Earth-based events. If you’re programming a robot going to Mars, or if you’re reading this in the future from a lunar base, please use a different, more relevant article!

Instant: A single piece of information

2014-02-07T16:14:00Z

An Instant is a single moment in time, e.g. the opening of the Olympics. It is the same moment, for everyone around the globe. People living in Sydney may say the moment happened on Friday, while people from the US say it occurred on Thursday. But the whole world is watching the opening ceremony on TV at the same moment, regardless of how they call that date or time.

You can use different representations to refer to the same moment, for example 1391789640 is the Unix timestamp referring to 2014-02-07T16:14:00Z. A Unix timestamp is the number of non-leap seconds since January 1st, 1970 at 0:00 in UTC, aka the Unix Epoch. These representations are equivalent to each other. You can freely convert between the two.

Note that this representation contains only a single piece of information, the moment itself. You can’t say what day this was or even what time this was without adding a second piece of information. You can say this was a Friday in Sydney, and Thursday in New York, but only because you’re including the city name in that statement.

Applications of Instants include server event logs and other information that isn’t related to a specific location on the globe. Instants are useful for sorting and calculations. It is trivial to see that 2014-02-07T16:14:00Z happened before 2014-02-07T16:14:30Z and there’s 30 seconds between those two instants.

Example: Server log

2014-02-07T08:26:01.234Z Application starting
2014-02-07T08:26:01.321Z Loading config file
2014-02-07T08:26:02.193Z Ready for incoming calls
2014-02-07T08:29:52.832Z GET request at /homepage

The above example shows a fictional event log from a server, using Instants for representing when an event occurred. Note that this event log uses millisecond precision, while the previous examples only used second precision. Depending on your application, it's important to decide the amount of significant numbers you need. Are seconds enough? Milliseconds? Nanoseconds? This is also useful to consider when programming. A .NET server might send DateTimes with nanosecond precision, but if your JavaScript client can only store millisecond precision DateTimes, then it might be sending back a (slightly) different time!

OffsetDateTime: Two pieces of information

2014-02-07T20:14:00+04:00

This representation contains two separate pieces of information. A local date and time (2014-02-07T20:14:00) and an offset from UTC (+04:00). When combining both these pieces of information, we can convert back to an Instant. For a lot of (human) applications, we can just use the local date and time, though.

For example, we can say:

The Opening Ceremony in Sochi starts Thursday February 7th 2014 at 20:14

For everyone in Sochi, this is enough information. When I receive an invite for my niece’s 1st birthday party stating “Saturday August 10th, starting at 10:00”, I know when to show up. When my plane lands at 4:12 local time in Rome, I know it’s dark outside. My friends birthday is September 3th 1986, I know when to celebrate it.

Humans constantly deal with local dates and times. This is because humans mostly live in one place and most people they deal with live in the same timezone and share the local date and time. For human applications, converting from a LocalDateTime to an Instant is the exception, not the rule. It’s only needed when dealing with other UTC offsets. For example when making an international phone call, or when sorting photos from a vacation trip.

Note that we shouldn’t convert an OffsetDateTime to an Instant and store only that, because then we’re losing information. An OffsetDateTime is a LocalDateTime and a UTC offset, two pieces of information. An Instant is just a single piece of information. When combined, it’s impossible to restore an Instant to its constituent parts, without adding extra information.

We need to store the two pieces of information separately, because we almost always want to show our users the local date time. That’s the thing they’re interested in, so we can’t throw that away.

Example: Vacation photo

The photo above was taken during my vacation in Australia, at 13:33 local time [1]. It’s interesting for me to know this, and now I’m trying to remember what I did that morning...

However storing just the local time obviously isn’t good enough. If we did that, the photos in my library wouldn’t be sorted correctly when I travel across time zone boundaries. For sorting, we need the Instant.

To sum up: For events that relate to some human activity (at a specific location) we often need the LocalDateTime for display. As well as the UTC offset, for converting that LocalDateTime to an Instant, so we can compare and sort these events.

ZonedDateTime: Three pieces of information

2014-02-07T20:14:00+04:00 Europe/Moscow

This final representation contains three separate pieces of information. Again a local date and time (2014-02-07T20:14:00) and an offset from UTC (+04:00). But also a TimeZone identifier (Europe/Moscow) from the IANA database. This third piece of information is needed when just the offset is ambiguous. This happens when doing date calculations, or for referring to dates in the future (e.g. in the year 2022).

Example: Moving a meeting in a calendar app

In the above example, we see someone interacting with a calendar application. They drag and drop a planned meeting from Friday at 10:00 in the morning, to the next Monday. This is a common thing for people to do. When talking to a colleague, they will presumably understand the meeting just moved to after the weekend, but it still starts at the same time.

When programming this calendar app, we need to account for Daylight Saving Time (DST) and different time zones. Having just the UTC offset is not enough information. If the Friday meeting happens in Mexico City, the move to Monday crosses the DST change in Mexico that occurs that weekend. But if it happens in Chicago, there is no DST change, because the US changes DST the next weekend.

Example dates in Mexico City and Chicago:

2019-10-25T10:00:00-05:00 America/Mexico_City (Friday)
2019-10-28T10:00:00-06:00 America/Mexico_City (Monday)

2019-10-25T10:00:00-05:00 America/Chicago (Friday)
2019-10-28T10:00:00-05:00 America/Chicago (Monday)

Both Friday meetings start at the exact same OffsetDateTime (2019-10-25T10:00:00-05:00), but the meetings on Mondays differ by an hour. Note how Mexico shifts from -05:00 to -06:00, this indicates how Mexico changes from DST that weekend by changing their UTC offset. If we hadn’t included the time zone when storing this meeting, and only used the OffsetDateTime, we wouldn’t have had enough information to change the datetimes. We wouldn’t know if we should change the UTC offset or not.

Example: Planning an event in the future

When storing future dates, we need to account for the fact that countries (and thus time zones) often change UTC offsets [2]. Not only during scheduled Daylight Saving Time changes, but also permanently, for political or economic reasons. A big upcoming change is the abolishment of Daylight Saving Time in Europe. The exact details still have to be worked out, but there’s a good chance DST changes will no longer happen in Europe from 2022. What does that mean for storing dates?

Let's say I want to plan a festival in Amsterdam on Monday August 1, 2022 at 9:00 in the morning. When is that? At which UTC offset? According to the current rules that will be 2022-08-01T09:00:00+02:00. But will Amsterdam still be at +02:00 in August 2022? We don’t know yet. That depends on what the European Parliament and the Dutch Government will decide. The safest way to store the date of this event would be to include the time zone identifier: 2022-08-01T09:00:00+02:00 Europe/Amsterdam

When we’re decoding this date in the future, we might realise that the +02:00 and Europe/Amsterdam no longer match. Maybe Amsterdam now uses +01:00 instead. If so we can change the value in our database. Because we know the user intends for the event to happen in Amsterdam, we have enough information to ‘fix’ any issues that come up due to political changes. [3]

Conclusion

We’ve looked at three DateTime representations in detail. Having more information can be useful, but isn’t always necessary. For server logs, Instants are useful. For human-related events LocalDateTimes are nice to have. Don’t forget to include a UTC offset for converting these back to Instants. And finally, for dealing with date time computations, having a TimeZone is necessary to reliably store the users’ intention.

Be careful when choosing your date representation. Don’t throw away useful information you have (do you know the UTC offset? Keep it!), but also don’t invent information that doesn’t exist. Above all, be aware of what you have, and what you can do with that information.

[1]: This screenshot is from the iOS 13; on iOS 12 the Photos app converted the time to my current timezone, which is pretty useless.

[2]: Examples: In 2018 North Korea changed their TimeZone by half an hour to match South Korea, as part of the ongoing peace efforts. In 2016 Egypt cancelled DST (with 3 days notice), and in 2011 Samoa decided to skip a day and move across the international date line.

[3]: For the keen-eyed among you who might think storing just the TimeZone and LocalDateTime is enough information: you would think that works. But unfortunately there is one hour per year during DST where the same LocalDateTime happens twice in a single TimeZone. You really do need three pieces of information to disambiguate.

Check out our other tech posts on https://engineering.q42.nl.