yu-tarrrrの日記

完全に個人の趣味でその時々書きたいことを書く

Elixirで日付比較を不等号で行ってはいけない、絶対にだ!

はじめに

  • 釣りタイトルみたいな話ですが、実際に私がやってしまっていたことを包み隠さず話そうと思います
  • tipsみたいな記事なので大したことは書きません

実現したかったこと

  • 構造体の配列を日付の降順で並べ替えたいということを思っていた
  • ElixirにはEnum.sort/2があるから、その中で日付を不等号で比較したら終了でしょみたいな安易な発想をしました
iex(1)> value_hoge = %{date: ~D[2020-08-21], text: "hello"}
%{date: ~D[2020-08-21], text: "hello"}

iex(2)> value_fuga = %{date: ~D[2020-08-24], text: "hello"}
%{date: ~D[2020-08-24], text: "hello"}

iex(3)> [value_hoge, value_fuga] |> Enum.sort(&(&1.date > &2.date ))
[%{date: ~D[2020-08-24], text: "hello"}, %{date: ~D[2020-08-21], text: "hello"}]
  • ここまでは問題なく動いたが、試しに違う月のものを入れてみると
iex(4)> value_piyo = %{date: ~D[2020-06-30], text: "hello"}
%{date: ~D[2020-06-30], text: "hello"}
iex(5)> [value_hoge, value_fuga, value_piyo] |> Enum.sort(&(&1.date > &2.date ))
[
  %{date: ~D[2020-06-30], text: "hello"},
  %{date: ~D[2020-08-24], text: "hello"},
  %{date: ~D[2020-08-21], text: "hello"}
]
  • なぜか、6月30日が一番上に来てしまう!?
  • ではもっと昔のものを入れたらどうなるか???
iex(7)> [value_hoge, value_fuga, value_piyo, value_foo] |> Enum.sort(&(&1.date > &2.date ))
[
  %{date: ~D[1900-07-31], text: "hello"},
  %{date: ~D[2020-06-30], text: "hello"},
  %{date: ~D[2020-08-24], text: "hello"},
  %{date: ~D[2020-08-21], text: "hello"}
]

(o_ _)oバタッ

ちょっと調査してみる

iex(8)> inspect value_hoge
"%{date: ~D[2020-08-21], text: \"hello\"}"
iex(9)> inspect value_hoge, structs: false
"%{date: %{__struct__: Date, calendar: Calendar.ISO, day: 21, month: 8, year: 2020}, text: \"hello\"}"
iex(10)> inspect value_fuga, structs: false
"%{date: %{__struct__: Date, calendar: Calendar.ISO, day: 24, month: 8, year: 2020}, text: \"hello\"}"
  • Date型のデータを見た時に、daymonthyearの順番でキーが登録されていることがわかる
  • ということは、今までの挙動+構造体の中身的にmapを不等号で比較した時に、先に出てくるキーに対応するvalue同士で比較をしてしまっているのでは?という仮説がここで生まれる

ということで、不等号に関する公式参照してみる Operators — Elixir v1.10.4

The collection types are compared using the following rules: Tuples are compared by size, then element by element. Maps are compared by size, then by keys in ascending term order, then by values in key order. In the specific case of maps' key ordering, integers are always considered to be less than floats. Lists are compared element by element. Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit.

書いてあった。どうやら、マップの比較では「マップのサイズ→キーの降順→キーの順番に対応する値」の順番で評価されていくようなので、今回はdateが最初に出てくるためこのようなことになってしまった模様

対策

  • そもそも、Date型の比較を不等号でやろうとしたのが悪いというのはあるとして、どうするのが良かったのか?という話で、結論としてはDate.compare/2を使えば良かったと思います
iex(1)> Date.compare(~D[2020-08-21], ~D[2020-08-22])
:lt
iex(2)> Date.compare(~D[2020-07-31], ~D[2020-08-22])
:lt
iex(3)> Date.compare(~D[1900-07-31], ~D[2020-08-22])
:lt
  • Date.compareは引数を2つ受け取って、1つ目の引数が2つ目の引数と比べてどうなのかという評価をしてくれます
  • 戻り値はatomで、:lt(less than),:gt(greater than) ,eq (equal)のいずれかなので、あとはパターンマッチさせればいいと思います
iex(10)> [value_hoge, value_fuga, value_piyo, value_foo] |> Enum.sort(&(Date.compare(&1.date, &2.date)) == :gt)
[
  %{date: ~D[2020-08-24], text: "hello"},
  %{date: ~D[2020-08-21], text: "hello"},
  %{date: ~D[2020-06-30], text: "hello"},
  %{date: ~D[1900-07-31], text: "hello"}
]
  • これで期待通りにソートされた。めでたしめでたし

まとめ

  • Elixirで日付を比較したい時は、横着して不等号で比較するなどしてはいけない絶対にだ。