2010年11月8日月曜日

Haskell の日付型

Ruby, Python, Java の日付型

Java で日付型の値を扱う場合、何だかゴテゴテとしたコードを書く必要がある。これに対し、Ruby や Python の記述はシンプルでスッキリとしている。

例えば Ruby で `2010年4月1日’に対応した日付オブジェクトを使いたい場合、

require 'date'
puts Date.new(2010, 4, 1)

Python では、

import datetime
print datetime.date(2010, 4, 1)

ちなみに Java だと、

import java.util.Calendar;
import java.text.SimpleDateFormat;

public class TestDate {
    public static void main(String[] args){
	Calendar cal = Calendar.getInstance();
	cal.set(2010, Calendar.APRIL, 1);
	System.out.println
	    (new SimpleDateFormat
	     ("yyyy-MM-dd").format(cal.getTime()));
    }
}

うーん、ややこしい。 (+_+)

Java の日付時刻に関することは以下も参考に。

 

Data.Time

Haskell でも同じく Date 型がないかと探してみると、Data.Time の階層に日付を扱うためのモジュールがある。

の 4 つのモジュールに分かれており、上 3 つ はその名前通りの用途に用いることができそう。最後の LocalTiem モジュールの中には、日付を含まない時間を表す型が定義されていた。

(cf. 時刻を扱う - 特定の日付に属さない時間 )

 

日付から Day 型への変換

Data.Time.Calendar には日付に対応した Day 型が定義されている。

コンストラクタは、

ModifiedJulianDay {toModifiedJulianDay :: Integer} 

この型の説明には、

The Modified Julian Day is a standard count of days, with zero being the day 1858-11-17.

とある。 Modified Julian Day とは 「 ユリウス日(Julian Day)」 によると、

ユリウス日(ユリウス通日)とは紀元前4713年1月1日からの連続した通し番号の日数です。

ユリウス通日 – Wikipedia

数年にわたる2点の日数を計算するのに便利で、天文学年代学などで使われている。ユリウス通日では桁が多すぎるため、ユリウス通日から2400000.5を引いた修正ユリウス日(MJD)も広く使われている。

内部的にはこの数値が Day 型に使われているようだ。

 

fromGregorian 関数

日付からこの Day 型に変換するには、

fromGregorian :: Integer -> Int -> Int –> Day

この関数を使ってみる。

import Data.Time.Calendar
main = print $ fromGregorian 2010 4 1

 

toModifiedJulianDay 関数

fromGregorian 関数によって Day 型に変換された値は、セレクタ toModifiedJulianDay により修正ユリウス日を得られる。

フリーゲルの公式 によると、

例えば、2004年1月1日はy=2003、m=13、d=1なので

\lfloor 365.25 \times 2003 \rfloor + \lfloor 2003 / 400 \rfloor -  \lfloor 2003 / 100 \rfloor + \lfloor 30.59 ( 13 - 2 ) \rfloor + 1 - 678912
= 731595 + 5 − 20 + 336 + 1 − 678912 = 53005

となり、53005が修正ユリウス日となる。

確かめてみる。

Prelude> :m Data.Time.Calendar
Prelude Data.Time.Calendar> toModifiedJulianDay $ fromGregorian 2004 1 1
53005

 

グレゴリオ暦

それにしても Haskell では、Ruby や Python のようにシンプルに Date というような名前のコンストラクタではなく、fromGregorian というゴツイ関数名が付けられているのだろう?

Gregorian とは 「グレゴリオ暦」 のことを指し、

現在、日常使われているグレゴリオ暦は、1582年に制定された事実上の世界標準の暦です。 このグレゴリオ暦の前身はユリウス暦で、そのユリウス暦は、古代ローマ暦にエジプト 発祥の太陽暦を導入したものです。

日本では、1872年からグレゴリオ暦が採用されています。

普段意識せずに使っているのがグレゴリオ暦で、その前に使われていたのがユリウス暦だと。

なぜこの暦が使われるようになったかと言えば、グレゴリオ暦 - Wikipedia によると、

それまで用いられていたユリウス暦では、通常の年(平年)は1年を365日とし、4年に1回を閏年として366日とし、平均年を365.25日としていた。…

しかし太陽年は約365.2422日であるため、ユリウス暦の方式では1000年で約8日の誤差が生じる。これにより、比較的頻繁に補正することが必要であった。…

これに対して、新たに定められたグレゴリオ暦では、平年は1年を365日とし、4年に1回を閏年とするところまではユリウス暦と変わらないものの、さらに調整を加えて平均年を365.2425日とした。この調整とは「西暦紀元(西暦)の年数が100で割り切れてかつ400では割り切れない年は閏年としない[2]」というルールを加えることである。これはすなわち、ユリウス暦の方式では閏年とされる年であっても400年間に3回は閏年とせずに平年に戻すということである[3]

プログラムの入門書でよく見かける 「うるう年を判定するプログラム」 の判定の理屈は、このグレゴリオ暦に基いている。

ちなみに、2000年問題 – Wikipedia においても、

直接の原因は、プログラム内でを扱う際の年数の表現方法である。年数の表現をグレゴリオ暦の下二桁のみで行っている場合、2000年が内部で00年となり、これを1900年とみなしてしまい、例えば「データベースを日付順に並び替える処理をすると、順序が狂う」などの誤作動につながる可能性があるとされた。

また、現行の太陽暦であるグレゴリオ暦では

  1. 年が4で割り切れる年は閏年とする
  2. (1)のうち、年が100で割り切れる年は閏年としない
  3. (2)のうち、年が400で割り切れる年はこれを適用しない(つまり閏年とする)

というルールがあり、このため2000年は閏年だったが、誤って1と2のみを適用し、閏年としなかったプログラムが存在したため、この対応も併せて必要とされた。

 

他の言語における定義

Date - Rubyリファレンスマニュアル では、

new([year[, mon[, mday[, start]]]])

暦日付に相当する日付オブジェクトを生成します。

… 最後の引数は、グレゴリオ暦をつかい始めた日をあらわすユリウス日です。グレゴリオ暦の指定として真、ユリウス暦の指定として偽を与えることもできます。省略した場合は、Date::ITALY (1582年10月15日) になります。

Python の 6.10.3 date オブジェクト には、

日付は理想的なカレンダー、すなわち現在のグレゴリオ暦を過去と未来の両方向に無限に延長したもので表されます。 … この暦法は、全ての計算における基本カレンダーである、 … "予期的グレゴリオ (proleptic Gregorian)" 暦の定義に一致します。

Java の Calendar (Java Platform SE 6) における getInstance() メソッドにより返されるカレンダーオブジェクトを文字列表現にすると、

java.util.GregorianCalendar …

このクラスは GregorianCalendar (Java Platform SE 6) によると、

GregorianCalendar は、Calendar の具象サブクラスであり、世界のほとんどの地域で使用される標準的なカレンダシステムを提供します。 

GregorianCalendar は、グレゴリオ暦とユリウス暦をサポートするハイブリッドカレンダシステムで、単一の変わり目を処理します。

 

Data.Time の使用例

まずは Data.Time 関連のモジュールを読み込む。

Prelude> :m Data.Time
Prelude Data.Time>

Data/Time.hs を見てわかる通り、このモジュールで下位のモジュールがエクスポートされている。

 

日付の加算・減算

例えば、「2010年4月1日の 100 日後の日付」 を求めたい場合は、CalendaraddDays 関数を使い、

Prelude Data.Time> addDays 100 $ fromGregorian 2010 4 1
2010-07-10

「大晦日まで後何日か?」 求めたいなら diffDays 関数。

Prelude Data.Time> fromGregorian 2010 12 31 `diffDays` fromGregorian 2010 4 1
274

 

現在の日付・時刻は?

Ruby で現在の時刻を求める場合、

require 'date'
puts DateTime.now

同じように Haskell でも ClockgetCurrentTime 関数を使うと現在の時刻が表示される。

Prelude Data.Time> getCurrentTime
2010-11-08 03:09:48.769875 UTC

ただし、UTCTime 型の値が返される。この型の値は 協定世界時 – Wikipedia

協定世界時(きょうていせかいじ、UTC - Universal Time, Coordinated)とはセシウム原子時計が刻む国際原子時(TAI)をもとに、天文学的に決められる世界時(UT1)との差が1秒未満となるよう国際協定により人工的に維持されている世界共通の標準時である。…

世界各地の標準時はこれを基準として決めている。例えば、日本標準時は(JST)で協定世界時より9時間進んでおり、「+0900(JST)」のように表示する。

日本標準時を取得するには LocalTimegetZonedTime 関数を利用する。

Prelude Data.Time> getZonedTime
2010-11-08 12:10:21.06675 JST

 

LocalTime と ZonedTiem の関係

LocalTime 

A simple day and time aggregate, where the day is of the specified parameter, and the time is a TimeOfDay. Conversion of this (as local civil time) to UTC depends on the time zone.

LocalTime は日付と時刻を持つ型。これに TimeZone を加えると、世界標準の UTCTime になる。

タイムゾーンとは、

共通の標準時を使う地域全体を「等時帯」、「時間帯」または「タイムゾーン(time zone)」といい、その地域の標準時を示す際にはUTCとの差で示すことが多い。

(標準時 - Wikipedia)

例えば、日本は UTC + 9 で、この設定をする関数が hoursToTimeZone

localTimeToUTC 関数に TimeZone LocalTime 「2010年4月1日 6時0分0秒」 を与えて UTCTime を得る。

Prelude Data.Time> let tz = hoursToTimeZone 9
Prelude Data.Time> let lt = LocalTime (fromGregorian 2010 4 1) (TimeOfDay 6 0 0)
Prelude Data.Time> localTimeToUTC tz lt
2010-03-31 21:00:00 UTC

日本は UTC よりも 9 時間早いので、前日の 21 時となる。

 

現在からの日数を求める

先ほどと同様に、「現在の日付から 100 日後」 と、「現在から大晦日まで何日あるか」 を調べたい。

予め ZonedTime から日付と時刻に分解する補助的な関数を定義。

dayAndTime :: ZonedTime -> (Day, TimeOfDay)
dayAndTime zt = let lt = zonedTimeToLocalTime zt
                    day  = localDay lt
                    time = localTimeOfDay lt
                in (day, time)

day  = fst . dayAndTime
time = snd . dayAndTime

これを用いて、

main = do
-- 現在の時刻を取得 now <- getZonedTime -- 現在の日付から 100 日後は? print $ addDays 100 (day now) -- 現在から大晦日までは何日? print $ fromGregorian 2010 12 31 `diffDays` (day now)

 

後何分?

「現在の時刻から、特定の時刻まで後どれくらの時間があるか」 を調べたい。

時刻 t1 から t2 までの長さを求める diffTime を定義。

diffTime :: TimeOfDay -> TimeOfDay -> TimeOfDay
diffTime t1 t2 = timeToTimeOfDay 
                 (timeOfDayToTime t2 - timeOfDayToTime t1)

これを使い 「今から 22 時までの時間」 を求める例。

print $ diffTime (time now) (TimeOfDay 22 0 0) 

 

フォーマット

日付 `2010-04-01’ の表示を変更して `2010—04—01’ のような書式にしたい。

予め System.Locale をインポートしておき、defaultTimeLocale 関数を使えるようにしておく。

import System.Locale

formatTime 関数を使い、

print $ formatTime defaultTimeLocale "%Y--%m--%d" 
          $ fromGregorian 2010 4 1

 

全体のコード

関連記事