レシピ1.2 日付けの正確な解析とファジィ解析

問題

日付けまたは日時を表す文字列をDateオブジェクトに変換したい。ただし、文字列のフォーマットは事前に分からない可能性がある。

解決
 日付けを表す文字列(日付文字列)をDate.parseまたはDateTime.parseに渡すのが最も効果的である。これらのメソッドは、経験則(ヒューリスティクス)に基づいて文字列のフォーマットを推測するが、その手際はなかなかのものだ。

require 'date'

Date.Parse('2/9/2007').to_s
# => "2007-02-09"

DateTime.parse('02-09-2007 12:30:44 AM').to_s
# => "2007-09-02T00:30:44Z"

DateTime.parse('02-09-2007 12:30:44 PM EST').to_s
# => "2007-09-02T12:30:44-0500"

Date.parse('Wednesday, January 10, 2001').to_s
# => "2001-01-10"

解説
 他のプログラミング言語では手間のかかる時間の解析も、parseによってだいぶ解消されるが、必ずしも期待通りの結果が得られる訳ではない。1つ目の例で、Date.parseが2/9/2007をヨーロッパの日付(日、月、年)ではなくアメリカの日付(月、日、年)と想定していることに注目しよう。また、parseは2桁で表される年の解釈を誤りがちだ。

Date.parse('2/9/07').to_s       # => "0007-02-09"

 たとえば、Date.parseではうまく行かないものの、すべての日付けが特定の方法でフォーマットされていることが分かっているとしよう。この場合は、標準のstrftimeディレクティブを使用してフォーマット文字列を作成し、それを日付文字列とともにDateTime.strptimeまたはDate.strptimeに渡せばよい。日付文字列がFOMAと文字列に適合すれば、DateオブジェクトかDateTimeオブジェクトが得られる。多くの言語やUNIXのdateコマンドもお日付を同じようにフォーマットするため、すでにおなじみのテクニックかもしれない。
 次に、一般的な日付と時刻のフォーマットをいくつか示す。

american_date = '%m/%d/%y'
Date.strptime('2/9/07, american_date').to_s               # "2007-02-09"
Datetime.strptime('2/9/05', american_date).to_s         # "2005-02-09T00:00:00Z"
Date.strptime('2/9/68', american_date).to_s               # "2068-02-09"
Date.strptime('2/9/69', american_date).to_s               # "1969-02-09"

european_date = '%d/%m/%y'
Date.strptime('2/9/07', european_date).to_s              # "2007-09-02"
Date.strptime('02/09/68', european_date).to_s          # "2068-09-02"
Date.strptime('2/9/69', european_date).to_s              # "1969-09-02"

four_digit_year_date = '%m/%d/%Y'
Date.strptime('2/9/2007', four_digit_year_date).to_s          # "2007-02-09"
Date.strptime('02/09/1968', four_digit_year_date).to_s      # "1968-02-09"
Date.strptime('2/9/69', four_digit_year_date).to_s              # "0069-02-09"

date_and_time = '%m-%d-%Y %H: %M: %S  %Z'
DateTime.strptime('02-09-2007 12:30:44 EST', date_and_time).to_s
# => "2007-02-09T12:30:44-0500"
DateTime.strptime('02-09-2007 12:30:44 PST', date_and_time).to_s
# => "2007-02-09T12:30:44-0800"
DateTime.strptime('02-09-2007 12:30:44 GMT', date_and_time).to_s
# => "2007-02-09T12:30:44Z"

twelve_hour_clock_time = '%m-%d-%Y  %I: %M: %S  %p'
DateTime.strptime('02-09-2007 12:30:44 AM', twelve_hour_clock_time).to_s
# => "2007-02-09T00:30:44Z"
DateTime.strptime('02-09-2007 12:30:44 PM', twlve_hour_clock_time).to_s
# => "2007-02-09T12:30:44Z"

word_date = ' %A, %B %d, %Y'
Date.strptime('Wednesday, January 10, 2001', word_date).to_s
# => "2001-01-10"

日付文字列のフォーマットをいくつかに絞り込める場合は、フォーマット文字列のリストをループにかけ、日付文字列を順番に解析してみよう。そうすれば、Date.parseのほうが高速になるし、このメソッドの想定を上書きすることが出来る。それでもDate.parseの法が高速なので、うまくいくのであれば、Date.parseを使用すること。

Date.parse('01/10/07').to_s                 # "0007-01-10"
Date.parse('2007 1 10').to_s
# ArgumentError: 3 elements of civil date are necessary

TRY_FORMATS = ['%d/ %m/ %y', '%Y %m %d']
def try_to_parse(s)
 parsed = nil
 TRY_FORMATS.each do |format|
  begin
   parsed = Date.strptime(s, format)
   break
  rescue ArgumentError
  end
 end
 return parsed
end

try_to_parse('1/10/07').to_s                         # => "2007-10-01"
trt_to_parse(2007 1 10).to_s                        # => "2007-01-10"

 一般的な日付のフォーマットの中には、strptimeフォーマット文字列では正確に表せないものがある。Rubyには、そうした日付文字列を解析するためのTimeクラスメソッドが定義されているため、カスタムコードを各必要はない。次に示すメソッドは、どれもTimeオブジェクトを返す。
 Time.rfc822は、RFC822/RFC2822、つまり、インターネット電子メールの標準フォーマットで日付文字列を解析する。RFC2822準拠の日付では、ロケール英語圏ではないとしても、月と曜日は常に英語で表される(「Tue」、「Jul」など)。

require 'time'
mail_received = 'Tue, 1 Jul 2003 10:52:37 +0200'
Time.rfc822(mail_received)
# => Tue Jul 01 04:52:37 EDT 2003

 HTTP標準であるRFC2616準拠の日付を解析するには、Time.httpdateを使用する。RFC2616準拠の日付とは、Last-ModifiedなどのHTTPヘッダーで目にするたぐいの日付だ。RFC2822の場合と同様、月と曜日の略記は常に英語で表される。

last_modified = 'Tue, 05 Sep 2006 16:05:51 GMT'
Time.httpdate(last_modified)
# => Tue Sep 05 12:05:51 EDT 2006

 ISO8601またはXMLスキーマーのフォーマットで表される日付を解析するには、Time.iso8601またはTime.emlschemaを使用する。

timestamp = '2001-04-17T19:23:17 .201Z'
t = Time.iso8601(timestamp)     # => Tue Apr 17 19:23:17 UTC 2001
t = sec                                     # => 17
t.tv_usec                                  # => 201000

 これらのTimeクラスメソッドと、同じ名前のインスタンスメソッドを混同しないように注意すること。クラスメソッドは文字列からTimeオブジェクトを生成するが、インスタンスメソッドはその逆で既存のTimeオブジェクトを文字列としてフォーマットする。

t = Time.at(1000000000)       # => Sat Sep 08 21:46:40 EDT 2001
t.rfc822                                 # => "Sat, 08 Sep 2001 21:46:40 -0400"
t.httpdate                              # => "Sun, 09 Sep 2001 01:46:40:GMT"
t.iso8601                               # => "2001-09-08T21:46:40-04:00"

参照
・Time#strftimeメソッドのRDocには、サポートされているstrftimeディレクティブのほとんどが列挙されている(ri Time#strftime)。詳細と完全なリストについては、レシピ1.3を参照のこと。