DateTime comparison gotcha in Elixir

January 17, 2024 3 min read

I wrote a simple function in elixir that checks if a given date is before another date and if the difference in milliseconds between the two dates is less than a given threshold.

The problem

Here is my naive implementation:

defmodule DateValidation do
  def date_valid?(date_before, date_after, threshold_ms) do
    difference_in_ms = DateTime.diff(date_before, date_after, :millisecond) |> abs()
    date_before < date_after && difference_in_ms < threshold_ms
  end
end

Looks fine right? Well, it’s not. While the threshold check works as expected, the date comparison does not. Let’s take a look at what happens when we give it two dates that are a few milliseconds apart and in the correct order:

iex(1)> date_before = ~U[2024-01-17 15:05:08.997288Z]
iex(2)> date_after = ~U[2024-01-17 15:05:09.011286Z]
iex(3)> DateValidation.date_valid?(date_before, date_after, 10_000)
false

That’s not what we expected. The dates are in the correct order and the difference is less than 10,000 milliseconds.

iex(4)> date_before < date_after
false

The DateTime documentation states the following:

Remember, comparisons in Elixir using ==/2, >/2, </2 and friends are structural and based on the DateTime struct fields. For proper comparison between datetimes, use the compare/2 function.

That’s it, a date such as ~U[2024-01-17 15:05:08.997288Z] is represented as a DateTime struct and that’s what we are comparing. The DateTime struct has the following fields:

iex(5)> DateTime.utc_now() |> Map.from_struct()
%{
  calendar: Calendar.ISO,
  day: 17,
  hour: 16,
  microsecond: {100543, 6},
  minute: 20,
  month: 1,
  second: 10,
  std_offset: 0,
  time_zone: "Etc/UTC",
  utc_offset: 0,
  year: 2024,
  zone_abbr: "UTC"
}

According to the elixir documentation regarding structural comparison the fields are compared in the order seen above. In our example the only differing fields are microsecond and second. The microsecond field is compared before the second field and since the .997288 part in first date is greater than the .011286 part in the second date, the first date is considered greater than the second date even though the second date is actually greater than the first date.

The solution

As the documentation states, the comparison operators in Elixir are structural and based on the DateTime struct fields and we need to use the DateTime.compare/2 function instead of the comparison operators. DateTime.compare/2 returns :lt if the first date is less than the second date, :gt if the first date is greater than the second date and :eq if the dates are equal.

defmodule DateValidation do
  def date_valid?(date_before, date_after, threshold_ms) do
    difference_in_ms = DateTime.diff(date_before, date_after, :millisecond) |> abs()
    DateTime.compare(date_before, date_after) == :lt && difference_in_ms < threshold_ms
  end
end

Et voilà!

iex(1)> date_before = ~U[2024-01-17 15:05:08.997288Z]
iex(2)> date_after = ~U[2024-01-17 15:05:09.011286Z]
iex(3)> DateValidation.date_valid?(date_before, date_after, 10_000)
true

Conclusion

The ~U sigil is a great way to create DateTime structs in Elixir but it’s important to remember that the a date like ~U[2024-01-17 15:05:08.997288Z] is not a primitive term but a DateTime struct with fields that are compared in a specific order.