日時オブジェクトの扱い


バカ往く のある記述への反応で始まった inamode6 のやりとりを受けて ruby-list に「Date へのメソッド追加要望」という投稿がありました。そのスレを見ていて、Squeak システムの Smalltalk 言語で同様のことはどんなふうになっているのか興味を持ったので調べてみました。


クラス階層

本エントリーに関係のありそうなもののみ抜粋。なお、括弧内はそれぞれのクラスで宣言されているインスタンス変数名です。

ProtoObject #()
   Object #()
      Magnitude #()
         DateAndTime #('seconds' 'offset' 'jdn' 'nanos')
         Duration #('nanos' 'seconds')
         Time #('seconds' 'nanos')
         Timespan #('start' 'duration')
            Date #()
            Month #()
            Week #()
            Year #()

評価時点、あるいは任意の日時オブジェクトの取得

Time now
Date today
DateAndTime now

任意の日付などを文字列から変換して得たいときは、Date class >> #fromString: を使うか、String >> #asDate を用います。

Date fromString: '10/25/2005'   " => 25 October 2005 "
'10/25/2005' asDate             " => 25 October 2005 "


Date class >> #fromString: が、慣用フォーマットを想定しているのに対し、DateAndTime class >> #fromString:(および String >> #asDateAndTime )のほうは ISO-8601 に準ずる書式しか受け付けないようなので注意が必要でしょう(次に、想定されているフォーマットをそれぞれのメソッド中のコメントから抜粋して転載します)。

Date class >> #fromString: (String >> #asDate)
<day> <monthName> <year>    (5 April 1982; 5-APR-82)  
<monthName> <day> <year>    (April 5, 1982)  
<monthNumber> <day> <year>  (4/5/82) 
<day><monthName><year>      (5APR82)"
DateAndTime class >> #fromString: (String >> #asDateAndTime)
'-1199-01-05T20:33:14.321-05:00' asDateAndTime
' 2002-05-16T17:20:45.00000001+01:01' asDateAndTime
' 2002-05-16T17:20:45.00000001' asDateAndTime
' 2002-05-16T17:20' asDateAndTime
' 2002-05-16T17:20:45' asDateAndTime
' 2002-05-16T17:20:45+01:57' asDateAndTime
' 2002-05-16T17:20:45-02:34' asDateAndTime
' 2002-05-16T17:20:45+00:00' asDateAndTime
' 1997-04-26T01:02:03+01:02:3' asDateAndTime

各要素を指定しての生成ももちろん可能です。

Date year: 2005 month: 10 day: 25     " => 25 October 2005 "
Time hour: 12 minute: 55 second: 15   " => 12:55:15 pm "
DateAndTime year: 2005 month: 10 day: 25 hour: 12 minute: 55 second: 15

要素や属性の取り出し、換算

a DateAndTime と a Date とで共通して使えるメッセージは、asDate、asDateAndTime、asDuration、asMonth、asTime、asTimeStamp、asWeek、asYear、day、dayOfMonth、dayOfWeek、dayOfWeekName、dayOfYear、daysInMonth、daysInYear、daysLeftInYear、firstDayOfMonth、isLeapYear、julianDayNumber、month、monthAbbreviation、monthIndex、monthName、year など。ただし、メッセージ month については、a DateAndTime が monthIndex を返すにのに対し、a Date では a Month を返す違いがあります(後述の #addMonths: のバグも、おそらくこのことと関連あり、か?)。

Date today monthIndex        " => 10 "
DateAndTime now monthIndex   " => 10 "
Date today month             " => October 2005 "
Date today month class       " => Month "
DateAndTime now month        " => 10 "
DateAndTime now month class  " => SmallInteger "


他方で、a DateAndTime と a Time とで共通して使えるメッセージは、asDate、asDateAndTime、asDuration、asMonth、asNanoSeconds、asSeconds、asTime、asTimeStamp、asWeek、asYear、duration、hour、hour12、hour24、hours、meridianAbbreviation、minute、minutes、nanoSecond、second、seconds、ticks 。

Time now minutes          " => 49 "
DateAndTime now minutes   " => 49 "

月名、曜日の扱い

インデックスと英語名の両方で取得できます。今回、Ruby で話題になっている #isWednesday のようなメソッドを用意するところまでは踏み込んでいません。

Date today monthIndex         " => 10 "
Date today monthName          " => #October"
Date today monthAbbreviation  " => 'Oct' "
Date today weekday        " => #Wednesday"
Date today weekdayIndex   " => 4 "


曜日の扱いについて、ちょっと便利なところでは「前の土曜日」のような指定ができる #previous: があります。

Date today previous: #Saturday   " => 22 October 2005 "

が、なぜか #next: はありません。orz


過去や未来の取得

ナノ秒、ミリ秒、秒、分、時、日、週 については、Number の converting カテゴリに a Duration 向けの変換用メソッドが用意されているので、a DateAndTime と a Date については、これらを使って #+ や #- による表現がが可能です。

Date today + 4 days
Date today - 3 weeks


a Date についてはこれでいいのですが、残念ながら a Time のほうには #+ や #- がなぜだか未定義なため、同じことをするには a DateAndTime を介して行なう必要があります。

Time now + 30 seconds   " => MessageNotUnderstood: Time>>+ "
Time now - 4 hours      " => MessageNotUnderstood: Time>>- "
(DateAndTime now + 30 seconds) asTime   " => 12:24:40 pm "
(DateAndTime now - 4 hours) asTime      " =>  8:24:17 am "


また、やはりなぜか 月、年 は a Number からの変換用メソッドが用意されていないので、

Date today + 3 months

のようなことはできません。代わりに月については、Date >> #addMonths: があるのでこれを使えばいいと思うのですが、現状では壊れて使えません。だめじゃん。orz

Date today addMonths: 4   " Error! "


ただ、これは単に前述の #month がらみの問題(Date >> #month が monthIndex ではなく a Month を返してしまう…)なので、Date >> #addMonths: の month を monthIndex に置き換えてやればとりあえず、動かすことができます。近いうちに修正されるでしょう。

Date >> addMonths: monthCount 
   ^ Date
      newDay: self dayOfMonth
      month: self monthIndex + monthCount - 1 \\ 12 + 1
      year: self year + (monthCount + self monthIndex - 1 // 12)
Date today addMonths: 4   " => 26 February 2006 "

これを直すと副次的に、#onPreviousMonth、#onNextMonth も使えるようになります。

Date today onNextMonth       " => 26 November 2005 "
Date today onPreviousMonth   " => 26 September 2005 "

日付間の差分

a Date では専用の #subtractDate: を使えば、そのまま日数が出てきます。また、a Date、a DateAndTime とも #- が相応に定義されているので「過去や未来の取得」のところで紹介した計算のほかに、差分も同様にして計算できます。

Date today subtractDate: '2/24/1993' asDate   " => 4627 "
(DateAndTime now - '2/24/1993' asDate) day    " => 4627 "

週、月、年 のオブジェクトとその取得

SqueakSmalltalk に特徴的なのは、「今月の日曜日は何回?」に関連したエントリー(id:sumim:20051014:p1)でも使用した a Month や a Week、a Year と言ったオブジェクトたちです。これらは、おのおののクラスに current を送って評価時点のオブジェクトを得ることが可能です。a Date や a DateAndTime に、asYear、asMonth、asWeek を送信しても相当オブジェクトが得られます。また、a Year については an Integer に asYear を送信することで、指定した年を表わすオブジェクトを得ることも可能です。

Year current    " => a Year (2005) "
Month current   " => October 2005 "
Week current    " => a Week starting: 2005-10-23T00:00:00+00:00 "
Date today month     " => October 2005 "
Date today asMonth   " => October 2005 "
Date today asWeek    " => a Week starting: 2005-10-23T00:00:00+00:00 "
Date today week      " => a Week starting: 2005-10-23T00:00:00+00:00 "
Date today asYear    " => a Year (2005) "
1925 asYear     " => a Year (1925) "


これらのオブジェクトたちからは、さらに細かい単位のオブジェクト群やその数をたぐることができます。

Year current months   " => #(January 2005 February 2005 March 2005 April 2005 
                             May 2005 June 2005 July 2005 August 2005 
                             September 2005 October 2005 November 2005 December 2005)
Year current weeks size   " => 53 "
Year current dates size   " => 365 "
Year current daysInYear     " => 365 "
Month current daysInMonth   " => 31 "