Ruby の Time.parse で文字列を Time に変換するときのエラーチェック

2007/06/22 4:52am

Ruby では、文字列から Time オブジェクト への変換には、time ライブラリ によって拡張される Time.parse を使う。

require "time"

Time.parse("2007/6/21 19:23")
# => Thu Jun 21 19:23:00 +0900 2007

Time.parse は内部的に ParseDate.parsedate を利用しているので、さまざまなフォーマットをサポートしており、それなりに便利だ。逆に、サポートされていないフォーマットの文字列を変換するときは、事前に正規表現で変換するなどの泥臭い作業になる。

Time.parse("Sat Aug 28 21:45:09 1999")
# => Sat Aug 28 21:45:09 +0900 1999
Time.parse("08/28/1999")
# => Sat Aug 28 00:00:00 +0900 1999

他にも問題がある。

変換対象の文字列が日付形式ではなかった場合は、当然 Time に変換できないわけだが、そういうときでもエラーを検知する仕組みがないのだ。

Time.parse("Happy Hacking!")
# => Thu Jun 21 19:33:12 +0900 2007

このように、明らかに日付ではない文字列を渡しても、例外が発生するでもなく、Time オブジェクトが返ってくる。

Time.parse のマニュアルによると、

Time.parse(date, now=Time.now)

与えられた時刻に上位の要素がなかったり壊れていた場合、now の該当要素が使われます。

とのことで、解析に失敗した場合は「時刻の要素がない」が判断され、引数 now(この場合、デフォルトの Time.now)がそのまま返ってきているようだ。

では、この引数 nownil を渡せば、失敗時に nil が返ってくるだろうか?

Time.parse("Happy Hacking!", nil)
# => Thu Jan 01 00:00:00 +0900 1970

違った。

実は Time.parse は、それぞれの時刻要素のデフォルトとして 1970-01-01 00:00:00:00 を指定している。また、この値は Time の起算時間(Unix epoch)に見えるが、結果は地方時間で返ってくるので必ずしもそうではない(日本の場合、1970-01-01 00:00:00 +0900)。

これだと困るので、解析に失敗した場合は nil を返す Time.parse を書いてみた。

require "parsedate"
require "time"

# The Unix epoch is the time 00:00:00 UTC on January 1, 1970
UNIX_EPOCH_TIME = Time.at(0)

# Strict version of +Time.parse+, returns +nil+ when parsing is failed.
def strict_parsetime(string)
  # +Time.parse+ returns localtime "1970/01/01 00:00:00" when parsing is failed.
  # So, ugly, I check whether returned value is UNIX epoch.
  time = Time.parse(string, UNIX_EPOCH_TIME) rescue nil
  if UNIX_EPOCH_TIME == time then
    # Previous +Time.parse+ possibly failed.
    time = nil unless (ParseDate.parsedate(string)[0] rescue nil)
  end
  time
end

実装としては、TIme.parse の第二引数に特定の日時を指定し、結果がこの値なら再度 ParseDate.parsedate して本当にエラーなのかチェックしている。

ちゃんとした方法がありそうなんだけどなー。