はじめに
この記事はElixir Advent Calendar 2015の23日目の記事です。
Timexとは
Elixirで時間や日付を扱う場合、よく使われるのがTimexです。それなりのサービスやアプリを作る際には、大抵時間や日付の処理が必要になってきますので、利用頻度の高いライブラリだと思います。
Timexを使ってみてヒヤリ・ハットな挙動がいくつかあったので、その紹介をします。
ケース1: タイムゾーンの取得
Timexでタイムゾーンの情報を取得するにはDate.local
を使います。
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
の様な記述のケースがあり、その場合は
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の場合だと以下の様に記述できます
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で同様の記述をすると、結果は逆になってしまいます。
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の不等号での比較について、もうすこしわかりやすく簡略化した例が以下になります。
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の比較の定義についてはこちらを参照していただくとして、この比較が恐ろしいのは、一見比較の処理は実行されているように「見える」所です。
例えば以下の様に月や年を跨いだ比較の場合は、挙動が月や年を跨ぐ時に変わってしまいます。
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になってしまった!
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です。
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
を使いましょう。