レシピ1.8 夏時間のチェック

問題

現在のロケールの時刻が夏時間かどうかを知りたい。

解決
 Timeオブジェクトを生成し、そのisdstメソッドをチェックする。

Time.local(2006,1,1)                   # => Sun Jan 01 00:00:00 EST 2006
Time.local(2006,1,1).isdst           # => false
Time.local(2006,10,1)                 # => Sun Oct 01 00:00:00 EDT 2006
Time.local(2006,10,1).isdst         # => true

解説
 UTC時間を表す Timeオブジェクトは、 isdstが呼び出されると常にfalseを返す。何故なら、UTCは一年中同じ時間だからだ。他のTimeオブジェクトは、Time オブジェクトの生成に使われたロケールの時間を調べる。通常は、オブジェクトを生成したコンピュータのシステムロケールである。ロケールの設定を変更する方法については、レシピ1.7を参照のこと。次のコードは、アメリカの夏時間に関連するルールの一部を具体的に示している。

eastern = Time.local(2006, 10, 1)           # => Sun Oct 01 00:00:00 EDT 2006
eastern.isdst                                          # => true

ENV[' TZ '] 'US/Pacific'
pacific = Time.local(2006, 10, 1)            # => Sun Oct 01 00:00:00 PDT 2006
pacific.isdst                                          # => true

# ナバホ居留地を除き、アリゾナ州では夏時間を使用しない
ENV[' TZ '] 'America/Phoenix'
arizona = TIme.local(2006, 10, 1)            # => Sun Oct  01 00:00:00 MST 2006
arizona.isdst                                          # => faluse

# 最後に、元のタイムゾーンに戻す
ENV[' TZ '] = nil

 Timeクラスに使用されているCライブラリは、特定のタイムゾーンロケールの歴史にまたがる、複雑な時間ルールに対処している。例えば、アメリカで夏時間が制定されたのは1918年のことだが、その後間もなく、ほとんどのロケールで廃止された。Cライブラリが使用する「zoneinfo」ファイルには、他の多くのルールとともに、この情報が含まれている。

# 夏時間は1918年3月31日に施行された
Time.local(1918, 3, 31).isdst                  # => faluse
Time.local(1918, 4, 1).isdst                    # => true
Time.local(1919, 4, 1).isdst                    # => true

# 連邦法は1919年に廃止されたが、
# 一部の地域では夏時間を使用し続けていた
ENV[' TZ '] = 'US/Pacific'
Time.local(1920, 4, 1)                             # => Thu Apr 01 00:00:00 PAT 1920

ENV[' TZ '] = nil
Time.local(1920, 4, 1)                             # => ThuApr 01 00:00:00 EDT 1920

# 第二次世界大戦中に再び夏時間が導入された
Time.local(1942, 2, 9)                             # => Mon Feb 09 00:00:00 EST 1942
Time.local(1942, 2, 10)                           # => Tue Feb 10 00:00:00 EWT 1942
# EWTは”Eastern War Time”の略

 アメリカでは、2007年から夏時間を3月から11月までに拡大する法案が2005年に可決された。zoneinfoファイルがどれくらい古いかによっては、2007年以降の日付に対して生成されるTimeオブジェクトに新しい法律が反映されない可能性がある。

Time.local(2007, 3, 13)                 # => Tue Mar 13 00:00:00 EDT 2007
# コンピュータによっては、この時間がESTであると誤解するかもしれない

 これは一般的な観念を例証している。議員たちは法案を可決させることしか眼中にない。従って、Timeオブジェクトが1年以上先の時間を表すとしたら、isdstが正確であることを当てにすべきではない。実際にその時刻になった時、そのロケールでは夏時間のルールが変更されているかもしれない。
 DateクラスにはCライブラリが使用されておらず、タイムゾーンロケールを全く認識しないため、夏時間のことも認識しない。



参照

・レシピ1.7
・「zoneinfo」データベースに含まれている情報(http://www.twinsun.com/tz/tz-link.htm

レシピ1.5 日付の反復

指定された時点から別の時点を割り出したい。

解決
 Rubyのタイムオブジェクトはすべて、数値のように範囲内で使用できる。DateオブジェクトとDateTimeオブジェクトは1日のインクリメントで反復し、Timeオブジェクトは1秒のインクリメントで反復する。

require 'date'
(Date.new(1776, 7, 2).. Date.new(1776, 7, 4)).each { |x| puts x }
#     1776-07-02
#     1776-07-03
#     1776-07-04

span = DateTime.new(1776, 7, 2, 1, 30, 15)..DateTime.new(1776, 7, 4, 7, 0, 0)
apan.each { |x| puts x }
#     1776-07-02T01:30:15Z
#     1776-07-03T01:30:15Z
#     1776-07-04T01:30:15Z

(Time.at(100)..Time.at(102)).each { | x | puts x }
#     Wed Dec 31 19:01:40 EST 1969
#     Wed Dec 31 19:01:41 EST 1969
#     Wed Dec 31 19:01:42 EST 1969

 RubyのDateクラスには、stepとuptoが定義されている。これらは数値で使用する便利な反復し(イテレータ)メソッドと同じ働きをする。

the_first = Date.new(2004, 1, 1)
the_fifth = Date.new(2004, 1, 5)

the_first.upto(the_fifth) { |x| puts x }
#     2004-01-01
#     2004-01-02
#     2004-01-03
#     2004-01-04
#     2004-01-05

解説
Rubyの日付オブジェクトは、内部では数値として格納される。こうしたオブジェクト範囲は、数値の安易の要に扱われる。DateオブジェクトとDateTimeオブジェクトの内部表現はユリウス日であり、これらのオブジェクトの範囲をループにかけると1日ずつインクリメントされる。Timeオブジェクトの内部表現はUNIXエポックからの経過秒数であり、Timeオブジェクトの範囲をループにかけると、1秒ずつインクリメントされる。
 Timeオブジェクトには、stepメソッドやuptoメソッドは定義されていないが、それらを追加するのは簡単だ。

class Time
 def step(other_time, increment) 
  raise ArgumentError, "step can't be 0" if increment == 0
   increasing = self < other_time
   if (increasing && increment < 0) || (!increasing && increment > 0)
    yield self
    return
  end
  d = self
  begin
   yield d
   d += increment
  while (increasing ? d >= other_time : d >= other_time)
 end

 def while (increasing ? d >= other_time : d >= other_time)
 end

 def upto(other_time)
  step(other_time, 1) { |x| puts x }
 end
end

the_first = Time.local(2004, 1, 1)
the_second = Time.local(2004, 1, 2)
the_first.step(the_second, 60 * 60 * 6) { |x| puts x }
# Thu Jan 01 00:00:00 EST 2004
# Thu Jan 01 06:00:00 EST 2004
# Thu Jan 01 12:00:00 EST 2004
# Thu Jan 01 18:00:00 EST 2004
# Fri Jan 02 00:00:00 EST 2004

the_first.upto(the_first) { |x| puts x }
# Thu Jan 01 00:00:00 EST 2004

レシピ1.3 日付の出力

問題

日付オブジェクトを文字列として出力したい

解決
 日付を確認したいだけであれば、Time#to_sまたはDate#to_sを呼び出せば、フォーマットで面倒な思いをせずにすむ。

require 'date'
Time.now.to_s              # => "Sat Mar 18 19:05:50 EST 2006"
DateTime.now.to_s       # => "2006-03-18T19:05:50-0500"

特定のフォーマットの日付が必要である場合は、タイムフォーマットディレクティブが含まれた文字列として、そのフォーマットを定義する必要がある。フォーマット文字列をTime#strftimeまたはDate#strftimeに渡すと、フォーマットディレクティブがTimeまたはDateTimeオブジェクトの該当する部分に沖かえられた文字列が返される。
 フォーマットディレクティブは、パーセント記号と文字(%x)で構成される。フォーマット文字列に含まれているフォーマットディレクティブ以外の物は全てリテラルとして扱われる。

Time.gm(2006).strftime('The year is %Y!')       # => "The year is 2006!"

「解説」では、Time#strftimeとDate#strftimeによって定義された全てのタイムフォーマットディレクティブを示す。例えば、2005年12月31日午後1時30分(GMT)の日付に対する一般的なタイムフォーマット文字列は、次のようになる。

time = Time.gm(2005, 12, 31, 13, 22, 33)
american_date = '%D'
time.strftime(american_date)                                 # => "12/31/05"
european_date = '%d/ %m/ %y'
time.strftime(european_date)                                # => "31/12/05"
four_digit_year_date = '%m/ %d/ %Y'
time.strftime(four_digit_year_date)                        # => "12/31/2005"
date_and_time = '%m- %d- %Y %H: %M: %S %Z'
time.strftime(date_and_time)                                 # => "12-31-2005 13:22:33 GMT"
twelve_hour_clock_time = '%m- %d- %Y %I:%M:%S %p'
time.strftime(twelve_hour_clock_time)                    # => "12-31-2005 01:22:33 PM"
word_date = '%A, %B %d, %Y'
time.strftime(word_date)                                       # => "Saturday, December 31, 2005"

解説
 印刷用紙、パーサー、ユーザーは、日付のフォーマットに非常にうるさいことがある。日付を標準フォーマットで表すと読みやすくなるし、エラーを発見しやすい。フォーマットを1つに決めるを、あいまいさ(4/12は12月4日か、それとも4月12日か)もなくなる。
 require 'time'を実行すると、Timeオブジェクトで一般的な日付表示標準のための特殊なフォーマットメソッド(Time#rfc822、Time#httpdate、Time#iso8601)を使用できるようになる。これらのメソッドを使用すれば、電子メール、HTTP、XML企画に準拠したフォーマットで日付を簡単に出力できる。

require 'time'
time.rfc822                   # => "Sat, 31 Dec 2005 13:22:33:-0000"
time.httpdate                # => "Sat, 31 Dec 2005 13:22:33:GMT"
time.iso8601                 # => "2005-12-31T13:22:33Z"

 DateTimeは、これら3つのフォーマットのうち、1つだけを提供する。ISO8601は、DateTimeオブジェクトのデフォルトの文字列表現(#to_sの呼び出しによって得られるもの)である。つまり、DateTimeオブジェクトをTimeオブジェクトにわざわざ変換しなくても、XMLドキュメントに簡単に出力できる。
 他の2つのフォーマットについては、DateTimeオブジェクトをTimeオブジェクトに変換するのが得策である(詳細については、レシピ1.9を参照)。RFC822とHTTPの日付は、過去または未来のそう遠くない日付に使用されることがほとんどなので、32ビットのタイムカウンタを搭載したシステム上であっても、DateTimeオブジェクトはTimeオブジェクトによってサポートされる1901~2037年の範囲に収まるだろう。
 日付のフォーマットをカスタマイズしなければならない場合もある。Time#strftimeとDate#strftimeは、フォーマット文字列で使用するためのディレクティブを多数定義している。以下の表にそれらを示す。これらのディレクティブは、フォーマット文字列で自由に組み合わせることができる。
 これらのディレクティブの中には、他のプログラミング言語でもおなじみのものがあるかもしれない。C以降の保母全ての言語に、これらのディレクティブの一部を使用するstrftime実装が含まれている。ディレクティブの中にはRuby独自のものもある。

必要なフォーマットに対応するフォーマットディレクティブがない場合は、Rubyコードを書いて対処しなければならない。たとえば、例の日付を「The 31st of December」のようにフォーマットしたいとしよう。日を序数として出力するための特別なフォーマット文字列はないが、Rubyコードを使用して、正しい答えが得られるフォーマット文字列を組み立てることが出来る。

class Time
 def day_ordinal_suffix
  if day == 11 or day == 12
   return "th"
  else
   case day %10
   when 1 then return "st"
   when 2 then return "nd"
   when 3 then return "rd"
   else return "th"
   end
  end
 end
end

time .strftime("The %e#{time.day_ordinal_suffix} of %B")
# => "The 31st of December"

 実際のフォーマット文字列は日ウケによって異なる。この場合は、"The %est of %B"という結果が得られるが、その他の日付では、"The %end of %B"、"The %est of %B"、"The %eth of %B"のいずれかになる。



参照
・Timeオブジェクトでは、一般的な日付フォーマットを解析し、それらを出力することが出来る。strftime、httpdate、iso8661の出力を解析する方法については、レシピ1.2を参照のこと。
・レシピ1.11

レシピ1.7 タイムゾーンの変換

問題

タイムオブジェクトを変更して、他のタイムゾーンで同じ瞬間を表したい。

解決
 最も一般的なタイムゾーン変換は、ローカルタイムからUCTへの変換とUTCからローカルタイムへの変換である。これらの変換はTimeでもDateTimeでも簡単に行える。
 Time#gmtimeのメソッドは、Timeオブジェクトを直接変更して、UCTに変換する。Time#localtimeメソッドは逆方向への変換を行う。

now = Time.now                         # => Sat Mar 18 20:15:58 EST 2006
now = now.gmtime                     # => Sun Mar 19 01:15:58 UTC 2006
now = now.localtme                    # => Sat Mar 18 20:15:58 EST 2006

 DateTime.new_offsetメソッドは、DateTimeオブジェクトをあるタイムゾーンから別のタイムゾーンに変換する。このメソッドには、変換先のタイムゾーンUTCからのオフセットを渡さなければならない。ローカルタイムをUTCに変換するには、0を渡す。DateTimeオブジェクトは不変なので、このメソッドによって、タイムゾーンのオフセット以外は返還前のDateTimeオブジェクトと全く同じオブジェクトが新たに生成される。
 UTCのDateTimeオブジェクトをローカルタイムに変換するには、DateTimeオブジェクトでoffsetを呼び出すことだ。オフセットは通常、分母が24の有理数となる。

local = DateTime.now
utc = local.new_offset
local.offset                                      # => Rational(-5, 24)
local_from_utc = utc.new_offset(local.offset)
local_from_utc.to_s                          # => "2006-03-18T20:15:58-0500"
local == local_from_utc                    # => true

解説
 Time.at、Time.local、Time.mktime、Time.new、およびTime.nowで生成されるTimeオブジェクトは、現在のシステムのタイムゾーンに基づいて生成される。Time.gmとTime.utcで生成されるTimeオブジェクトは任意のタイムゾーンを表せるが、ローカルタイムやUTC以外のTimeオブジェクトでタイムゾーンを使用するのは難しい。

# ローカル(東部標準)時間を太平洋標準時間に変換する
eastern = DateTime.now
eastern.to_s                       # => "2006-03-18T20:15:58-0500"

pacific_offset = Rational(7, 24)
pacific = eastern.new_offset(pacific_offset)
pacific.to_s                         # => "2006-03-18T18:15:58-0700"

 Datetime#new_offsetを使用すれば、任意のタイムゾーンオフセット間での変換が可能である。このため、タイムゾーン変換では、DateTimeオブジェクトをし雄牛、必要に応じてTimeオブジェクトに再変換するのが最も簡単である。しかし、DateTimeオブジェクトはタイムゾーンを数値のUTCオフセットとしてのみ理解する。タイムゾーンが「WET」、「Zulu」、「Asia/Taskent」と呼ばれていることしか分からない場合、日付と時刻をUTCに変換するにはどうすればよいだろうか。
 UNIXシステムでは、現在のプロセスの「システム」タイムゾーンを一時的に変更することが出来る。Timeクラスに使用されているCライブラリは、膨大な数のタイムゾーンを認識する(利用可能なタイムゾーンを調べたい場合、この「aoneinfo」データベースは通常、/usr/shere/zoneinfo/にある。)環境変数TZを適切な値に設定し、コンピュータが他のタイムゾーンン井あるかのようにTimeクラスを動作させれば、この知識を引き出すことができる。このトリックを利用して、CライブラリがサポートしているタイムゾーンにTimeオブジェクトを変換する方法は、次のようになる。

class Time
 def convert_zone(to_zone)
  original_zone = ENV["TZ"]
  utc_time = dup.gmtime
  ENV["TZ"] = to_zone
  to_zone_time = utc_time.localtime
  ENV["TZ"] = original_zone
  return to_zone_time
 end
end

 ローカルタイム(東部標準)から世界各地のタイムゾーンへの変換を行ってみよう。

t = Time.at(1000000000)                        # => Sat Sep 08 21:46:40 EDT 2001

t.convert_zone("US/Pacific")                    # => Sat Sep 08 18:46:40 PDT 2001
time.convert_zone("US/Alaska")               # => Sun Sep 08 17:46:40 AKDT 2001
t.convert_zone("UTC")                             # => Sun Sep 09 01:46:40 UTC 2001
t.convert_zone("Turkey")                          # = Sun Sep 09 04:46:40 EEST 2001

インドなどの一部のタイムゾーンではほとんどのタイムゾーンに対して30分のオフセットを使用することに注意しよう。

t.convert_zone("Asia/Calucutta")              # => Sun Sep 09 07:16:40 IST 2001

 Timeオブジェクトを生成する前にTZ環境変数を設定すると、任意のタイムゾーンの時刻を表すことが出来る。次のコードは、「実際」にどのタイムゾーンを使用するかに関わらず、ラゴス時間をシンガポール時間に変換する。

ENV["TZ"] = "Africa/Lagos"
t = Time.at(1000000000)                        # => Sun Sep 09 02:46:40 WAT 2001
ENV["TZ"] = nil

t.convert_zone("Singapore")                     # => Sun Sep 09 09:46:40 SGT 2001

# 前の時間と同じことを検証する
t.convert_zone("US/Eastern")                   # => Sat Sep 08 21:46:40 EDT 2001

 TZ環境変数はプロセスに対してグローバルなので、複数のスレッドで同時にタイムゾーンを変換しようとすると、問題が発生する。



参照
・レシピ1.8、1.9
・「zoneinfo」に含まれている情報(http://www.twinsun.com/tz/tz-link.htm

レシピ1.6 任意の日付からの経過日数

問題

特定の日付から何日が経過したか、あるいは未来の
日付まであと何日残っているか知りたい。


解決
 新しい方お日付から古い方の日付を引く。Timeオブジェクトを使用している場合、結果は浮動小数点で表された秒数になるため、それを1日の秒数で割る。

def last_modified(file)
 t1 = File.stat(file).ctime
 t2 = Time.now
 elapsed = (t2-t1)/(60*60*24)
 puts "#{file} was last modified #{elapsed} days ago."
end

last_modified("/etc/passwd")
# /etc/passwd was last modified 125.873605469919 days ago.
last_modified("/home/leonardr/")
# /home/leonardr/ was last modified 0.113293513796296 days ago.

DateTimeオブジェクトを使用している場合、結果は有理数となる。これを表示するには、恐らく整数、または浮動小数点に変換することになるだろう。

require 'date'
def advent_calendar(date=DateTime.now)
 christmas = DatetTime.new(date.year, 12, 25)
 christmas = DateTime.new(date.year+1, 12, 25) if date > christmas
 differnce = (christmas-date).to_i
 if difference == 0
  puts "Today is Christmas."
 else
  puts "Only #{difference} day#{"s" unless difference==1} until Christmas."
 end
end

advent_calencar(DateTime.new(2006, 12, 24))
# Only 1 day until Christmas.
advent_calender(DateTime.new(2006, 12, 25))
# Today is Christmas.
advent_calendar(DateTime.new(2006, 12, 26))
# Only 364 days until Christmas.

解説
 時間の内部表現は数値なので、ある時間から別の時間を引くと数値が得られる。両方の数値は同じ物「(0時)からの経過時間」として評価されるため、その数値は実際に何かを意味するはずだ。すなわち、時系列上で2つの時間を区切る秒数または日数となる。
 もちろん、これは他の時間間隔にもあてはまる。時間差を時単位で表示するとしたら、Timeオブジェクトの場合は、時間差を1時間の秒数(3,600またはRailを使用する場合は1.hour)で割る。DateTimeオブジェクトの場合は、1時間あたりの日数で割る(つまり、時間差に24を掛ける。)

sent = DateTime.new(2006, 10, 4, 3, 15)
received = DateTime.new(2006, 10, 5, 16, 33)
elapsed = (received-sent) * 24
puts "You responded to my email #{elapsed.to_f} hours after I sent it."
#You responded to my email 37.3 hours after I sent it.

時間の間隔にdivmodを使用して、更に細かく区切ることも出来る。筆者は大学生だった頃期末試験までに残された勉強時間を表示するスクリプトを書いた。このメソッドは、予定されたイベントまでの日数、時間数、糞数、秒数をカウントダウンする。

require 'date'
def remaining(date, event)
 intervals = [["day", 1], ["hour", 24], ["minute", 60], ["second", 60]]
 elapsed = DateTime.now -date
 tense = elapsed > 0 ? "since" : "until"
 interval = 1.0
 parts = intervals.collect do |name, new_interval|
  interval /= new_interval
  number, elapsed = elapsed.abs.divmod(interval)
 "#{number.to_i} #{name}#{"s" unless number ==1}"
 end
 puts "#{parts.join(", ")} #{tense} #{event}."
end

remaining(DateTime.new(2006, 4, 15, 0, 0, 0, DateTime.new.offset),
                "the book deadline")
# 27 days,  4 hours, 16minutes, 9 secondsuntil the book deadline.
remaining(DateTime.new(1999, 4, 23, 8, 0, 0, DateTime.now.offset),
                 "the Math 114A final")
# 2521 days, 11 hours, 43minutes, 50 seconds since the Math 114A final.

参照
・レシピ1.5

レシピ1.5 日付の演算

問題

2つの日付の時間差を割り出したい。または、日付に数値を足して、過去または未来の日付を割り出したい。

 Timeオブジェクトと数値の加算または減算を行うと、その秒数が足されるか引かれることになる。

require 'date'
y2k = Time.gm(2000, 1, 1)             # => Sat Jan 01 00:00:00 UTC 2000
j2k + 1                                          # => Sat Jan 01 00:00:00 UTC 2000
y2k - 1                                          # => Fri Dec 31 23:59:59 UTC 1999
y2k + (60 * 60 * 24 * 365)              # => Sun Dec 31 00:00:00 UTC 2000

y2k_dt = DateTime.new(2000, 1, 1)             
(y2k_dt + 1).to_s                                         # => "2000-01-02T00:00:00Z"
(y2k_dt - 1).to_s                                          # => "1999-12-31T00:00:00Z"
(y2k_dt + 0.5).to_s                                       # => "2000-01-01T12:00:00Z"
(y2k_dt + 365).to_s                                     # => "2000-12-31T00:00:00Z"

 Timeから別のTimeを引くと、、2つの日付の感覚が秒数で得られる。Dateから別のDateを引くと、感覚が日数で得られる。

day_one = Time.gm(1999, 12, 31)
day_two = Time.gm(2000, 1, 1)
day_two - day_one                                       # 86400.0
day_one - day_two                                       # -86400.0

#現在と10秒後の時間を比較する
before_time = TIme.now
before_datetime = DateTime.now
sleep(10)
Time.now - before_time                                # => 10.003414
DateTime.now - before_datetime                   # => Rational(5001557, 43200000000)

 Ruby on Railsの必須条件であるactivesupport gemには、NumericやTimeで時間を操作するための便利な関数が数多く定義されている。(Facets Moreライブラリについても同じである。)

require 'rubygems'
require 'active_support'

10.days.ago                                       # => Wed Mar 08 19:54:17 EST 2006
1.month.from_now                              # => Mon Apr 17 20:54:17 EDT 2006
2.weeks.since(Time.local(2006, 1, 1))   # => Sun Jan 15 00:00:00 EST 2006
y2k - 1.day                                        # => Fri Dec 31 00:00:00 UTC 1999
y2k + 6.3.years                                  # => Thu Apr 20 01:48:00 UTC 2006
6.3.years.since y2k                             # => Thu Apr 20 01:48:00 UTC 2006

解説
 Ruby日付の演算は、タイムオブジェクトの内部表現が数値であることをうまく利用している。日付への数値の加算や2つの日付の差分は、この内部表現の数値に対する加算や減算として処理される。TImeに1を足すと1秒を足すことになり、DateTimeに1を足すと1日を足すことになるのはこのためである。TImeは0時からの経過病数として格納され、DateまたはDateTimeは(別の)0時からの経過秒数として格納される。
 全ての算術演算が日付に対して意味を持つとは限らない。内部表現である数値を掛け合わせれば「2つの日付の乗算」が可能だが、実際の時間に置き換えても意味をなさないため、このような演算子は定義されていない。数値を実世界に置き換えて考えれば、妥当な操作は自ずと制限される。DateTimeオブジェクトに右シフト演算子や左シフト演算子を使用すると、その日付けに対して特定の月数が足されるか引かれる。

(y2k_dt >> 1).to_s                         # => "2000-02-01T00:00:00Z"
(y2k_dt << 1).to_s                         # => "1999-12-01T00:00:00Z"

 activesupportのNumeric#monthメソッドでお同様の結果が得られるが、このメソッドは「1ヶ月」が30日であると想定しており、特定の月の長さに対応しない。

y2k + 1.month                              # => Mon Jan 31 00:00:00 UTC 2000
y2k - 1.month                               # => Thu Dec 02 00:00:00 UTC 1999

対照的に、月の日数が短い(例えば、31日から開始し、次の月が30日しかない)場合、標準ライブラリは新しい月の最後の日を使用する。

# 9月は30日までしかない
halloween = Date.new(2000, 10, 31)
(halloween << 1).to_s                                  # => "2000-09-30"
(halloween >> 1).to_s                                  # => "2000-11-30"
(halloween >>2).to_s                                   # => "2000-12-31"

leap_year_day = Date.new(1996, 2, 29)
(leap_year_day << 1).to_s                            # => "1996-01-29"
(leap_year_day >> 1).to_s                            # => "1996-03-29"
(leap_year_day >> 12).to_s                          # => "1997-02-28"
(leap_year_day << 12 * 4).to_s                     # => "1992-02-29"

参照
・レシピ1.4、レシピ1.6
・RailのActiveSupport::CoreExtentions::Numeric::TimeのRDoc
(http://api.rubyonrails.com/classes/ActiveSupport/CoreExtensions/Numeric/Time.html)

レシピ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を参照のこと。