Time, the eternal struggle

How to not mess up timekeeping as a software engineer

We’ve all been there. Our app is acting weird: timestamps in our database or UI are somehow incorrect. But wait, didn’t we switch to summer time recently due to Daylight Saving Time? So it might have to do with time zones. What did we do wrong?

I call it the eternal struggle of the software engineer. Recently, I gave a talk about this subject and in this article I would like to give you some tips and tricks to improve your timekeeping.

1. Have a proper understanding of time concepts

If you’re like me, and things keep blowing up in your face, you might not understand the concepts behind timekeeping properly. Let me try to explain all the different concepts and how they differ from each other.

Let’s start with dates. Today, dates aren’t that complicated anymore. However, historically they could be very confusing due to the use of different calendars. It might be very interesting to understand how historical calendars relate to each other, but luckily most of us today are using the ISO-8601 calendar (which is based on the familiar Gregorian calendar).

But did you know that there are still some non-Gregorian calendars in use today, like the Jewish or Muslim calendar? In some cases, you might need to work with them.

So what is a calendar?

A calendar usually refers to a set of rules on how dates work. They define how many days exist in a month, how many months exist in a year, how long a year exactly is and, optionally, when leap days occur. In most cases, you will need to use the “default” calendar, which is the ISO-8601 calendar. Calendars like ISO-8601 may also specify how week numbering works and what weekday is the first one.

Now, let’s look at time. The time part of a “date time” is where most of the confusion arises.

Time used to be simple: we just had our local clock tower telling us the exact time. Of course time used to be a little bit different from town to town, but it didn’t really matter. This all changed when the railroads were invented. To properly schedule trains, Britain came up with Greenwich Mean Time (GMT) in the 1800s to synchronize all public clocks. Later on, in 1972, it was replaced by Coordinated Universal Time (UTC).

The concept behind time zones should have been very simple: divide the world in 24 zones and in every zone, everyone uses the same local time, which has a certain “offset” or difference compared to UTC. However, in the real world we have country borders, politics and trade. And we have daylight saving times. This all means that time zones are governed by local rules that can change any time, and cannot easily be captured in a simple algorithm.

So, to wrap up, there is:

Universal Coordinated Time (UTC)
A global time standard that follows the solar time at 0° longitude that all local times adhere to. It is also referred to as “Zulu time”, which explains the “Z” in the ISO-8601 representation of a UTC timestamp, e.g. 2022-09-23T15:00:00Z.

An offset is used to express how many hours a local timestamp deviates from UTC. For example, 2022-09-23T15:00:00+02:00 has an offset of 2 hours compared to UTC. Without an offset, it is unclear to what time you are referring (local time or UTC) and thus, you will lose very precious information on the accuracy.

Daylight Saving Time (DST)
Originally invented to save energy on lighting, daylight saving time is the practice to advance the clock (e.g. by 1 hours) during warmer months so that darkness falls at a later clock time. Technically, this means that the offset changes: for example, having an offset of +02:00 in summer and an offset of +01:00 in winter. For local clock times, this means that a day can have 23 or 25 hours. Remember that!

Time zone
A time zone is a set of local rules governed by local authorities that regulates the offset used in a certain geographical/political zone. It tells which offset should be used and on which dates the daylight saving time applies. And it can change at any time.

To illustrate, this is what happens when Daylight Saving Time kicks in and your local clock moves one hour back in time:

2022-10-30T01:59:00+02:00[Europe/Amsterdam] -> 23:59:00 UTC
2022-10-30T02:00:00+02:00[Europe/Amsterdam] -> 00:00:00 UTC
2022-10-30T02:59:00+02:00[Europe/Amsterdam] -> 00:59:00 UTC
2022-10-30T02:00:00+01:00[Europe/Amsterdam] -> 01:00:00 UTC
2022-10-30T03:00:00+01:00[Europe/Amsterdam] -> 02:00:00 UTC

2. Use a proper library and use it properly

Most Java developers will remember the pain of the old java.util.Date. Let’s be honest, it was horrible to work with. Luckily, with JSR-310 the very popular Joda Time library was adopted in Java 8 which made the lives of many developers a lot better.

Frontend developers that work with JavaScript are less lucky. Similar to Java, it has a very crappy Date implementation (which was actually based on the old java.util.Date). To name a few issues:

  • No (real) support for time zones – other than the user’s local time and UTC
  • Therefore, DST behavior is unpredictable
  • The Date object is mutable
  • It has zero-based month numbers (yes, month 11 is December…)
  • Parser behavior is so unreliable that it is unusable

That’s why a lot of JavaScript developers switch to using third party libraries like Moment.js or Luxon.

But there is good news: the folks at Ecma International are working on a new ECMAScript standard: the Temporal API. Similar to Java Time, it has all the good stuff:

  • All objects are immutable
  • Full support for time zones and DST arithmetic
  • Support for non-Gregorian calendars
  • January is month 1

There are several specialized types for Temporal values:

At the time of writing this blog, the proposal is in Stage 3 and will therefore be shipped in your browser anytime soon. I recommend you to read all about it.

3. Use the correct date time representation

With a proper date time library (either a default library or a third party library), you might have several different date time types to choose from. Let’s take the Java Time naming as an example:

  • LocalDateTime: a combination of a date and a time, without any information about offsets or time zones. Example: 2021-03:18T15:00:00.
  • Instant, sometimes also called epoch timestamp. It takes a well-defined moment in time (for example, 1970-01-01T00:00:00Z – note the UTC offset!) and takes all the seconds that have elapsed ever since. This guarantees to be an exact representation but doesn’t contain in itself any information about months, days, hours and offsets.
  • Offset: expresses a time difference compared to UTC, e.g. +01:00
  • OffsetDateTime: a combination of an Instant + Offset + LocalDateTime. For example: 2021-03:18T15:00:00+02:00.
  • Zone: a set of rules to tell when offsets change (e.g. DST), for example: Europe/Amsterdam.
  • ZonedDateTime: a combination of an OffsetDateTime and a Zone.

But what type should you use in your situation? When do we use local dates, offset date times or zones date times? As with most things in software engineering: it depends. Please consider the following aspects of your use case:

  • Accuracy: the main problem with a wall-clock time (often called LocalDateTime or PlainDateTime) is that it lacks precision. It tells you the time, but doesn’t tell you which timezone/offset is being used. Therefore, it might represent a different moment in time for different people, depending on the context. If you care about accuracy and you want to express the exact moment of time, you always have to use an instant (e.g. Unix epoch) or an offset date time (which often contains an instant).
  • Arithmetic: be careful with arithmetic, and decide what behavior you are expecting! If you want to add a few hours to a timestamp, you could decide to work with local date times or offset date times. In both cases, it is possible to perform this operation and have a valid result.
    But remember DST! A local date is not aware of any daylight saving time and will not take it into account. Neither will offset date times, but the difference is that it will not lose precision but the offset might rather become invalid for the time zone (that’s not always necessarily an issue).
    Also remember that when you are adding or subtracting days, do you want to work with calendar days or with exactly 24 hours? Because when DST kicks in, you might have days of 23 hours or 25 hours. So a lot of arithmetic might require you to work with zoned date times instead.
  • Regional settings and user expectations: if there are no humans involved, and only accuracy is important, an offset date time is just fine. But to be able to show a timestamp in the correct local time for a certain user, you will need time zone information.
    Which timezone you have to use, is very much depending on the case. In most cases, you will need the time zone that the user is currently in; but in some cases you want to adjust it for the location that an event will take place in. Now suddenly, the location is important! In some applications, it is critical to take this information into account already in the design phase.

Let’s look at a few examples

  • When referring to a newspaper edition, a local date is sufficient. We are not expressing an exact moment in time.
  • When referring to office times in general (e.g. offices opening up at 8:30 am), a local date time might be good enough. But when we want to express it for a specific office location, we might want to express it with the corresponding offset (depending on the location).
  • When I have an online appointment, I might want to see the appointment times in my local time (that is, the time zone I am currently in).
  • However, when I have a physical appointment in a different region (e.g. at 10:00 am in New York), I might want to see the local time of the location of the appointment
  • When I want to perform arithmetic, like taking the current time, adding 30 days to it, and then adjusting it to midnight, I really need a timezone-aware timestamp (zoned date time) because “midnight” very much depends on the time zone and DST rules.
  • When transferring information of a timestamp across a REST endpoint: always provide all the information to preserve precision, e.g. an instant or an offset date time. In some cases, you might even want to include a time zone (e.g. Europe/Amsterdam) if it can differ. Remember, not for precision, but for region-aware formatting or arithmetic.

4. Stick to the standards!

Finally, here are two do’s when it comes down to standards:

  • Always use ISO-8601 representations in APIs. Try to keep (region-aware) formatting only in user interfaces.
  • When using timestamps, always adhere to standards like the Unix epoch and express it relative to UTC.

Long story short: investing some time to understand the underlying principles of keeping time, saves you heaps of long headaches in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *