Elixir's DateTime values don't seem to compare correctly

Elixir 1.3.4 & 1.4.0

dt1 = %DateTime{calendar: Calendar.ISO, day: 12, hour: 0, month: 1, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0, year: 2017, zone_abbr: "UTC", microsecond: {878630, 6}, minute: 57, second: 59}
dt2 = %DateTime{calendar: Calendar.ISO, day: 12, hour: 0, month: 1, std_offset: 0, time_zone: "Etc/UTC", utc_offset: 0, year: 2017, zone_abbr: "UTC", microsecond: {481183, 6}, minute: 58, second: 2}
dt1 <= dt2
false

This was part of a spec that had been working fine until I upgraded Ecto to 2.1, and swapped out Ecto.DateTime for elixir’s DateTime. After upgrading to 1.4.0, I tried DateTime.compare, which seemed to work

DateTime.compare(dt1, dt2)
:lt

What am I missing here?

Basic Erlang ordering often produces confusing results when you apply it to complicated data structures such as a map with Tuples.

Look into the Erlang man page on Comparision operators and Term ordering

http://erlang.org/doc/reference_manual/expressions.html#id80107

The arguments can be of different data types. The following order is defined:

number < atom < reference < fun < port < pid < tuple < map < nil < list < bit string
Lists are compared element by element. Tuples are ordered by size, two tuples with the same size are compared element by element.

Maps are ordered by size, two maps with the same size are compared by keys in ascending term order and then by values in key order. In maps key order integers types are considered less than floats types.

When comparing an integer to a float, the term with the lesser precision is converted into the type of the other term, unless the operator is one of =:= or =/=. A float is more precise than an integer until all significant figures of the float are to the left of the decimal point. This happens when the float is larger/smaller than +/-9007199254740992.0. The conversion strategy is changed depending on the size of the float because otherwise comparison of large floats and integers would lose their transitivity.

Term comparison operators return the Boolean value of the expression, true or false.

So since DateTime is a map, it will sort the keys in term order, and microsecond is before minute. Once it finds a key that disgrees it stops searching.

It is possible to make structs that will sort properly with the default Erlang term ordering, but it is quite tricky and often leads to many strange edge cases and odd naming conventions.

2 Likes

Thanks for the info!

Do note, the ‘nil’ in Erlang is not the ‘nil’ in Elixir. The nil in Elixir is just an atom, nothing special about it whatsoever (I really wish Elixir did not confuse these two things). The ‘nil’ in Erlang is the empty list in Elixir [].

nil is the empty list in Erlang [] as well, we just call it nil by tradition. We were lisp hackers from way back.

2 Likes