Timexのよくある罠

# はじめに

この記事は[Elixir Advent Calendar 2015](http://qiita.com/advent-calendar/2015/elixir-lang)の23日目の記事です。

# Timexとは

Elixirで時間や日付を扱う場合、よく使われるのが[Timex](https://github.com/bitwalker/timex)です。それなりのサービスやアプリを作る際には、大抵時間や日付の処理が必要になってきますので、利用頻度の高いライブラリだと思います。

Timexを使ってみて[ヒヤリ・ハット](https://ja.wikipedia.org/wiki/%E3%83%92%E3%83%A4%E3%83%AA%E3%83%BB%E3%83%8F%E3%83%83%E3%83%88)な挙動がいくつかあったので、その紹介をします。

# ケース1: タイムゾーンの取得

Timexでタイムゾーンの情報を取得するには```Date.local```を使います。

```ex
iex(1)> use Timex
nil
iex(2)> Date.local
%Timex.DateTime{calendar: :gregorian, day: 23, hour: 20, minute: 45, month: 12,
 ms: 755, second: 49,
 timezone: %Timex.TimezoneInfo{abbreviation: "JST",
  from: {:saturday, {{1951, 9, 8}, {1, 0, 0}}}, full_name: "Asia/Tokyo",
  offset_std: 0, offset_utc: 540, until: :max}, year: 2015}
iex(3)>
```

この```Date.local```ですが、どうやってローカルタイムゾーンの情報を取得しているかというと、OSの種類毎(OSX,Win,Unix)にタイムゾーン情報が定義された設定ファイル/コマンドを参照してタイムゾーンを取得します。

例えば、OSXの場合は、```systemsetup -gettimezone```の実行結果をパース、Unixの場合は、```/etc/timezone```,```/etc/sysconfig/clock```,```/etc/conf.d/clock```,```/etc/localtime```,```/usr/local/etc/localtime```を順番に参照してタイムゾーン情報を取得しています。

OSがUnixで、タイムゾーン情報が```/etc/sysconfig/clock```,```/etc/conf.d/clock```に定義されている場合、設定情報は以下の

```
ZONE="Asia/Tokyo"
```
```
TIMEZONE="Asia/Tokyo"
```

の様に定義されている事をTimexは前提としています。OSのディストリビューションによってはこれが、

```
ZONE=Asia/Tokyo
```

の様な記述のケースがあり、その場合は

```ex
iex(1)> use Timex
nil
iex(2)> Date.local
** (MatchError) no match of right hand side value: ["ZONE=Asia/Tokyo\n"]
    lib/timezone/timezone_local.ex:202: Timex.Timezone.Local.read_timezone_data/3
    lib/timezone/timezone_local.ex:100: Timex.Timezone.Local.localtz/2
    lib/timezone/timezone_local.ex:52: Timex.Timezone.Local.lookup/1
    lib/timezone/timezone.ex:48: Timex.Timezone.local/1
    lib/date/date.ex:170: Timex.Date.local/0
iex(2)>
```

といったエラーになるので、その時は上記のタイムゾーン設定を確認してみてください。

# ケース2: 日付/日時の比較

日付を比較する場合、RubyのDateの場合だと以下の様に記述できます

```rb
irb(main):001:0> require 'date'
=> true
irb(main):002:0> day_12_01 = Date.new(2015,12,01)
=> #<Date: 2015-12-01 ((2457358j,0s,0n),+0s,2299161j)>
irb(main):003:0> day_11_30 = Date.new(2015,11,30)
=> #<Date: 2015-11-30 ((2457357j,0s,0n),+0s,2299161j)>
irb(main):004:0> day_11_30 < day_12_01
=> true
irb(main):005:0>
```

しかし、ElixirのTimexで同様の記述をすると、結果は逆になってしまいます。

```ex
iex(1)> use Timex
nil
iex(2)> day_12_01 = Date.from({{2015,12,1},{0,0,0}})
%Timex.DateTime{calendar: :gregorian, day: 1, hour: 0, minute: 0, month: 12,
 ms: 0, second: 0,
 timezone: %Timex.TimezoneInfo{abbreviation: "UTC", from: :min,
  full_name: "UTC", offset_std: 0, offset_utc: 0, until: :max}, year: 2015}
iex(3)> day_11_30 = Date.from({{2015,11,30},{0,0,0}})
%Timex.DateTime{calendar: :gregorian, day: 30, hour: 0, minute: 0, month: 11,
 ms: 0, second: 0,
 timezone: %Timex.TimezoneInfo{abbreviation: "UTC", from: :min,
  full_name: "UTC", offset_std: 0, offset_utc: 0, until: :max}, year: 2015}
iex(4)> day_11_30 < day_12_01
false
iex(5)>
```

何故こうなるかというと、```day_12_01```,```day_11_30```の実態はMapの為、不等号での比較を行った場合は、カレンダー上での比較にならないからです。
Mapの不等号での比較について、もうすこしわかりやすく簡略化した例が以下になります。

```ex
iex(1)> %{year: 2015, month: 11, day: 30} < %{year: 2015, month: 12, day: 1}
false # 2015/11/30 < 2015/12/1 trueだけどfalseになってしまう!
iex(2)>
```

何故こうなるかというと、month(11月と12月)の比較の前にday(30日と1日)の比較が先に走ってしまい、```2015/11/30```よりも```2015/12/1```のほうが小さいと判定されてしまうからです。

Mapの比較の定義については[こちら](http://www.erlang.org/eeps/eep-0043.html)を参照していただくとして、この比較が恐ろしいのは、一見比較の処理は実行されているように「見える」所です。

例えば以下の様に月や年を跨いだ比較の場合は、挙動が月や年を跨ぐ時に変わってしまいます。

```ex:月を跨ぐケース
iex(1)> %{year: 2015, month: 11, day: 28} < %{year: 2015, month: 11, day: 29}
true # 2015/11/28 < 2015/11/29 #=> true
iex(2)> %{year: 2015, month: 11, day: 29} < %{year: 2015, month: 11, day: 29}
false # 2015/11/29 < 2015/11/29 #=> false
iex(3)> %{year: 2015, month: 11, day: 30} < %{year: 2015, month: 11, day: 29}
false # 2015/11/30 < 2015/11/29 #=> false
iex(4)> %{year: 2015, month: 12, day: 1} < %{year: 2015, month: 11, day: 29}
true # 2015/12/1 < 2015/11/29 #=> falseなのにtrueになってしまった!
```

```ex:年を跨ぐケース
iex(5)> %{year: 2015, month: 12, day: 28} < %{year: 2015, month: 12, day: 29}
true # 2015/12/28 < 2015/12/29 #=> true
iex(6)> %{year: 2015, month: 12, day: 29} < %{year: 2015, month: 12, day: 29}
false # 2015/12/29 < 2015/12/29 #=> false
iex(7)> %{year: 2015, month: 12, day: 30} < %{year: 2015, month: 12, day: 29}
false # 2015/12/30 < 2015/12/29 #=> false
iex(8)> %{year: 2015, month: 12, day: 31} < %{year: 2015, month: 12, day: 29}
false # 2015/12/31 < 2015/12/29 #=> false
iex(9)> %{year: 2016, month: 1, day: 1} < %{year: 2015, month: 12, day: 29}
true # 2016/1/1 < 2015/12/29 #=> falseなのにtrueになってしまった!
```

これらはMapでの比較をおこなったケースですが、```Timex.DateTime```でも同様の結果となります。

では日付の比較を行うにはどうすれば良いのかというと、日付の比較を行う為の関数```Date.compare```があるので、そちらを使えばOKです。

```ex:日付の比較
iex(1)> use Timex
nil
iex(2)> day_11_30 = Date.from({{2015,11,30},{0,0,0}})
%Timex.DateTime{calendar: :gregorian, day: 30, hour: 0, minute: 0, month: 11,
 ms: 0, second: 0,
 timezone: %Timex.TimezoneInfo{abbreviation: "UTC", from: :min,
  full_name: "UTC", offset_std: 0, offset_utc: 0, until: :max}, year: 2015}
iex(3)> day_12_01 = Date.from({{2015,12,1},{0,0,0}})
%Timex.DateTime{calendar: :gregorian, day: 1, hour: 0, minute: 0, month: 12,
 ms: 0, second: 0,
 timezone: %Timex.TimezoneInfo{abbreviation: "UTC", from: :min,
  full_name: "UTC", offset_std: 0, offset_utc: 0, until: :max}, year: 2015}
iex(4)> Date.compare(day_12_01, day_11_30)
1 # 第1引数の日付が、第2引数の日付より未来
iex(5)> Date.compare(day_11_30, day_12_01)
-1 # 第1引数の日付が、第2引数の日付より過去
```

また、```Date.compare```の代わりに、日付をsecに変換してその比較を行っても良いかもしれません。

# まとめ

日付の比較には不等号ではなく、```Date.compare```を使いましょう。