ここに parsedate の日付解析の手法についての覚書 (をまとめたもの) を残しておきます。

歴史

その前に parsedate の歴史です。

元の parsedate (ここでは parsedate1 と呼びましょう) は、まつもとさんが書かれたものだと思います。僕が最初に見たときには、20行くらいのたいへん素朴なものでした。 parsedate は、少しづつ手を入れられて徐々にコードは膨れあがりました。一度整理する必要があると感じて、思いきって parsedate2 として書きなおすことにしました。しかしこれも速度的に問題があり、再び、parsedate3 として書きなおしました。これが現在の parsedate です。ただし、date2 3.x では、parsedate の本体は、date/format.rb に移動しています。

日付の走査

parsedate2 は rbison で書かれていました。ちゃんとした (?) 構文解析の手順を踏んでいるのです。遅かったのはそのためです。 parsedate3 は、parsedate1 と同じです。基本的に、正規表現との照合を必要なだけ繰り返すだけです。これは速いのですが、安易にパターンを増してしまいがちで、よく考えて整理しておかないと、すぐに訳がわからなくなります。 parsedate2 はボツになったのですが、 parsedate3 を書くためにたいへん役立ったと思います。

parsedate2 と parsedate3 は、手法の違いから、速度以外にも、それぞれ長所と短所があります。

parsedate2 は、“09 AM” をちゃんと9時と認識します。 parsedate2 では、こういった表現を比較的素直に受けとれると感じました。もちろん、parsedate3 でも、やろうと思えばできるはずですが、わざわざ、そこまでする必要性は感じられないので、やっていません。 parsedate2 のやりかたでは、簡単にできると感じたので、そのようにしてみただけです。 GNU date などでつかわれている get_date() は yacc で書かれています。 GNU date も、“09 AM” を受けつけます。さらには、“09” も受けつけます。

parsedate3 は、ノイズに強いです。

Date: Mon, 20 Mar 2000 23:29:46 +0900
February 04, 2001 at 10:59 AM PST

といった入力も大丈夫です。 parsedate2 や get_date() では難しいです。

ちなみに、以下の日付は、parsedate2、parsedate3 ともに大丈夫ですが、 get_date() では受けつけてもらえません。 parsedate もなかなかがんばっています。

Sept. 23
October 23rd
2001-02-08 23:55:21+09:00
2001-02-08T23:55:21Z
H11.02.08T23:55:21Z
12-January-1990, 04:00 WET

一方、get_date() は、“yesterday” のような相対的な表現も受けつけます。

日付の分類

エンディアンネス

年月日の順番です。以下のパターンがあります。

  • 年月日 (日本、ISO 8601 など)
  • 日月年 (欧州、RFC 822/850 など)
  • 月日年 (米国、asctime(3) や date(1) など)
年月日 (日本、ISO 8601 など)

parsedate では、ISO 8601 のよくつかわれる形式 (自分の考えです) と JIS X 0301 拡張の一部を受けつけます。

「ISO 8601 のよくつかわれる形式」とは

ISO 8601 (1988、あるいは 2000) における、暦日付 (calendar date) の完全表記 (Complete representation) の基本形式 (Basic format) と拡張形式 (Extended format) の両方、および上位省略表記 (Truncated representations) における、ある世紀の特定の年月日 (A specific date in the current century)。拡張形式では、拡大表記を許します。

CCYYMMDD
CCYY-MM-DD
YYMMDD
YY-MM-DD

地方時の時刻 (Local time of the day) の完全表記、および下位省略表記 (Representations with reduced precision) のそれぞれ基本形式と拡張形式の両方。ただし、基本形式は日付との組み合せにおいてのみ有効である (単独では、日付の基本形式と区別がつかないので)。

最新版では、コンマ、およびピリオドにつづく秒の端数があってもよいです (たとえば、“11:22:33,44” など)。ただし、parsedate では、結果に反映されません。

hhmmss
hh:mm:ss
hhmm
hh:mm

協定世界時 (Coordinated Universal Time)、および地方時と協定世界時との差 (Differences between local time and Coordinated Universal Time) の基本形式、拡張形式の表記。

Z

+hhmm
+hh
-hhmm
-hh

+hh:mm
+hh
-hh:mm
-hh

上記の書式の範囲内における日付と時刻の組み合せ (Combinations of date and time of the day representations) の表記。

ただし、parsedate は、ISO 8601 の解釈を厳格には適用しません。たとえば、ISO 8601 では、2000年の改訂版で、一切間隔 (空白) を含まない、と明言していますが、parsedate は、 “1985-04-12 10:15:30” も “1985-04-12T10:15:30” も同じように受けとります。

「JIS X0301 拡張の一部」とは

JIS X0301 (1992、あるいは 2002) における、元号による日付の拡張形式 (元号を識別する記号があるもの)。

スラッシュで区切ったもの

スラッシュで数字を区切る形式は、基本的に米国式の月日年と解釈されますが、 “2003/02/18” のようにあきらかに年が最初にあると判るものは年月日と解釈されます。 ここで頼りにしているのは 2003 という数値の大きさではなく、4桁の数字で表現されている、という事です。

日月年 (欧州、RFC 822/850 など)

欧州といっても、今のところ英語の月名しか知りません。 parsedate は、RFC 822 や RFC 850 を受けつけます。ダッシュで数字を区切る形式は、基本的に ISO 8601 と解釈されますが、 “18-02-2003” のようにあきらかに年が最後にあると判るものは日月年と解釈されます。

月日年 (米国、asctime(3) や date(1) など)

parsedate は、asctime(3)、ctime(3)、date(1) などの書式を受けつけます。 あくまでこれ等の伝統的な書式についてであって、国際化された出力などに対応するものではありません。

年のあつかい

年はしばしば省略されます。もちろん、月の日、あるいは、月と日の両方が省略される場合もありますが、 parsedate の実際のつかわれかたからすれば、それらはまったく重要ではないといえるでしょう。年の省略は、どのようなパターンでも許されているわけではありません。月が名前になっていて、年と月を並べただけのものは受けつけます。 parsedate1 は “MM/DD” を許していたことから、このパターンも受けつけます (スラッシュで区切ったものだけです。ダッシュで区切ったものはダメです)。また、“MMDD” も受けつけます。

その他

曜日、時刻およびタイムゾーン (時差) は付加的な存在といえます。

曜日は、省略月名などを含め一意な名前であれば、とくに注意すべきことはないでしょう。今現在は、曜日、月名ともに英語のみの受けつけなので問題ありませんが、様々な地域の曜日、月名を受けつけようとすれば、その出現位置なども考慮しなければならないかもしれません。

時刻は数字をコロンで区切った形式で、秒はどのような場合でも省略可能です。午前午後を示す “AM”/“PM” を添えることもできます。

タイムゾーン (時差) は時刻の後に添えます。 ISO 8601 や RFC 822 でつかわれる形式などを受けつけます。

解析の実際

伝統的な date(1) の書式を考えてみましょう。つぎのようなものです。

Tue Feb 18 00:03:05 JST 2003

まず、付加的な曜日をとり除きます。するとつぎのようになります。

Feb 18 00:03:05 JST 2003

この日付を左から見てゆくと、年が省略された日付のあとに時刻が現われているように見えます。 parsedate は、“Feb 18” を正しい日付として受けつけます。しかし、その前に時刻とそれに伴なうタイムゾーンをとり除いてみましょう。するとつぎのようになります。

Feb 18 2003

米国式の月日年のパターンをみつけました。このように考えれば、以下の微妙に違うふたつの日付も基本的に同じだとわかります。

Tue Feb 18 21:03:05 2003
Feb 18 2003 09:03:05 AM

このように整理すれば、むやみやたらとパターンを増さずに済みます。

課題

parsedate は、米国寄り、とみられる解釈をします。たとえば、月名などは、英語のものしかありませんし、日付の要素の順番もそうです。

月名や曜日名は、欧州のものだけでも結構あります。 parsedate では、月名や曜日名が一意であることは解析の手掛りです。とくに略名のことを考えると、それらをひとまとめにして一意であるということはできません。

また、以下の日付は、2002年3月4日を、日本式、欧州式、米国式で表現したものですが、もちろん区別はつきません。

02/03/04
04/03/02
03/04/02

範囲検査を一切しません。 2002年13月32日、といった日付も有効です。今のところ、必要ないと考えています。

漢字を含む日付も解釈できません。ロケールなどを考慮すべきなのでしょうか。

対応が簡単そうでも、実装していないものもあります。たとえば、ISO 8601 の 年間通算日 (年日付) や 暦週日付の拡張形式は、特徴的で、対応は簡単そうですが、あまり需要はなさそうです。