Juneum
Elixir, Phoenix, and Javascript

Working with Time Zones

January 23, 2020 (Updated March 19, 2021)
trisager

Elixir has two different types that can be used for timestamps: DateTime and NaiveDateTime. The main difference between them is that the first is aware of time zones, and the other isn't.

Of the two, NaiveDateTime is easier to work with, and so it is probably the one you reach for first if your application doesn't require you to deal with users in different time zones. Your users may all be in same time zone, or it may be implied which time zone you mean when you say "2 pm". But there is still one thing that may trip you up: Daylight savings time.

NaiveDateTime and DST

As an example, most of the European countries are on Central European Time during the winter (CET, UTC + 1 hour). In 2021 they will switch to Central European Summer Time (CEST, UTC + 2 hours) on March 28, and back to CET again on October 31. This is when you may run into trouble. For example:

iex> ts = ~N[2021-03-28 02:30:00]
~N[2021-03-28 02:30:00]

The match succeeds, but there is a problem: 2:30 am on March 28, 2021 does not actually exist in Central Europe. When the hour hand on the wall clock reaches 2:00:00 in the early morning of March 28, it immediately skips forward to 3:00:00 (or at least it ought to). So doing something like this in your code will not give the correct result:

iex> start_time = ~N[2021-03-27 17:00:00]
iex> end_time = ~N[2021-03-28 17:00:00]

iex> NaiveDateTime.diff(end_time, start_time)
86400

The code above calculates the elapsed time in seconds between 5 pm on March 27, 2021 and 5 pm on the following day. The result is 86,400 seconds (24 hours) and that looks right; it would be on most other dates. But in Central Europe it won't be correct on these particular dates - only 82,800 seconds (23 hours) will have elapsed.

DateTime

If you had been using DateTime instead, the above examples would not have caused any problems:

 iex> naive = ~N[2021-03-28 02:30:00]

 iex> DateTime.from_naive(naive, "Europe/Berlin")
 {:gap, #DateTime<2021-03-28 01:59:59.999999+01:00 CET Europe/Berlin>,
 #DateTime<2021-03-28 03:00:00+02:00 CEST Europe/Berlin>}

Instead of the {:ok, #DateTime<..>} tuple we would normally expect, we get a {:gap, #DateTime<..>, #DateTime<..>} reply. This tells us that we have hit a gap in time, and we get the last valid time (usec resolution) before the gap and the first valid time after. It is an error, but DateTime won't let it slip by unnoticed, and it won't give us the wrong result.

The elapsed time calculation would have done fine:

iex> start_naive = ~N[2021-03-27 17:00:00]
iex> {:ok, start_dt} = DateTime.from_naive(start_naive, "Europe/Berlin")
{:ok, #DateTime<2021-03-27 17:00:00+01:00 CET Europe/Berlin>}

iex> end_naive = ~N[2021-03-28 17:00:00]
iex> {:ok, end_dt} = DateTime.from_naive(end_naive, "Europe/Berlin")
{:ok, #DateTime<2021-03-28 17:00:00+02:00 CEST Europe/Berlin>}

iex> DateTime.diff(end_dt, start_dt)
82800

This time we get the correct result when we do the elapsed time calculation. Also note that the time zone information in the two DateTimes automatically get the right UTC offsets - +1 for the start time and +2 for the end time.

(Out of the box, Elixir's DateTime only works with UTC. You will need to configure a time zone database such as tzdata for the examples above to run).

Which one should you use?

So when can you safely use NaiveDatetime in your application?

If your application handles bookings, rentals, and such, or if it must handle dates and times in different time zones, the safest approach is to use DateTime everywhere. It is a bit more work, but you won't be caught out by issues like those described above.

In simple cases you can probably get away with using NaiveDateTimes. If you are just recording when something happened, showing opening hours for a store, or you are working with simple schedules of events that only occur during the daytime, you should be fine. Just remember that simple applications sometimes grow into more complicated ones!


DateTime documentation on hexdocs: hexdocs.pm/elixir/NaiveDateTime.html

DateTime documentation on hexdocs: hexdocs.pm/elixir/DateTime.html

Tzdata on GitHub: https://github.com/lau/tzdata

← Back to articles