hnwの日記

PHPの日付文字列の解釈ルールがドキュメント化されました

PHPのstrtotime関数やDateTimeクラスは、様々なフォーマットの日付文字列を解釈し、時刻として取り扱うことができます。たとえば次のような文字列を解釈することができます。

  • "Wednesday July 23rd, 2008"(=2008年7月23日)
  • "first Wednesday July 23rd, 2008"(=2008年7月30日、PHP5.1.0以降)
  • "first Wednesday of July 23rd, 2008"(=2008年7月2日、PHP5.3.0以降)


こうした日付の解釈はPHP独自の実装です*1。当初はGNUの日付記法に準拠していたようですが*2、今や別物といっていいでしょう。にもかかわらず、これまで解釈ルールの詳細を説明するドキュメントが存在しない状態が続いていました。


そのため、仕様を推測する根拠はstrtotime関数のサンプルコードとPHPソースコードのテストケースくらいしかありませんでした。ごく最近まで機能追加やバグ修正が入っていたこともあり、ソースコードから読み取った挙動さえ仕様とは言えない状態だったのです。実際、PHP5.2.7から'+1 Monday'の解釈結果が変わったのはバグではないかというバグレポートを出したことがあるのですが、今までの挙動がバグでPHP5.2.7から修正されたんだと言われてしまいました(PHP Bugs: #43452)。


この解釈ルールについての文書が、2010年3月頃になってようやくPHPマニュアルに追加されました(英語版:「Supported Date and Time Formats」)。これを受けて、PHPマニュアルの和訳について議論しているPHP-doc ML上で、僕がこの和訳に立候補しまして、2010年の11月末あたりからこの和訳も公開されています(和訳:「サポートされている日付と時刻の書式」)。


仕様が文書化されていれば、少なくとも仕様として書いてあることは未来にわたって変わりにくいと期待できますから、利用する上での基準にもなります。このブログでもstrtotime関数の仕様の曖昧さについて「strtotime関数との付き合い方」という記事で注意喚起したことがありました。当時は、かなり限定的な書式のみ使うのが安全だろうと紹介しましたが、今回この解釈ルールが公開されたことで、安心して使える範囲が広がったと言えるでしょう。


本稿では、この解釈ルールの中で注目すべき点について3点紹介します。

日付文字列を構成する書式の大半が公開された

日付文字列は、複数の部分に分割して解釈されます。たとえば「2011-01-02 12:34:56 JST +2 hours」であれば、次のように分解できます。

  • 2011-01-02(ハイフンで区切られた、年、月、日)
  • 12:34:56(時、分、秒)
  • JSTタイムゾーン
  • +2 hours(数字+時間の単位)


これまで、どんな要素をどんな組み合わせで使うことができるのか、その書式が不明確でした。これまではソースを読んだ人しか知らなかった書式(たとえばISO 8601の週番号の指定など)も公開されたので、その意味では一歩前進と言えそうです。序数がfirstからtwelfthまで使えるであるとか、一生使いそうにない仕様が明らかになるのは少し面白いですね。


もっとも、秒の指定が59秒までとなっているけれども実際は60秒も受け取れるであるとか、実情に沿っていない部分もあります。書式としても完全に網羅できていないような気がしているので、そのあたりは徐々にツッコミを入れていく必要がありそうです。

相対指定の処理は相対指定以外の解釈よりも後

相対的な書式」には相対的な書式として、「ある月の最終日」「来週の月曜日」「2日前」といった書式の例が載っています。


さらに、次のような注意書きがあります。

相対的な記述は、相対的でない記述の 後で 処理されます。 このため、 "+1 week july 2008" と "july 2008 +1 week" とは同一になります。


PHP: 相対的な書式 - Manual


こうした相対指定を解釈するためには基準日時が必要なので、まず相対的でない記述を解釈してから相対的な記述を解釈するという仕様は自然だと言えます。ただ、書式が相対指定とそれ以外に分類できること、両者に優先順位があること、これらが明文化されたことに価値があると個人的には思っています。

曜日の解釈は特殊な相対指定

日付文字列に曜日が混ざっていた場合の解釈は比較的複雑です。曜日を含んだ書式が何種類かあり、それぞれごとに少しずつルールが異なっているためです。


まずは曜日が単体で登場した場合について説明します。たとえば「Monday」とだけ書いたときは、今日を基準日として、次の月曜日の意味になります。「Sunday July 23rd, 2008」であれば、2008-07-23を基準として次の日曜日になりますから、2008-07-27ということになります。


ただし、基準日と指定された曜日が同じ曜日だった場合は、その当日の意味になります。たとえば、「Wednesday July 23rd, 2008」の場合、2008-07-23は水曜日ですから、その日のまま解釈します。メールのDateヘッダにもこのようにその日の曜日を含んだ日付文字列が登場しますが、こうした文字列もPHPは正しく解釈できるというわけです。これは少し面白い解釈ルールだと感じました。


一方で、「first Wednesday July 23rd, 2008」の場合はfirst Wednesdayの部分が「序数+曜日」という別の相対指定の書式になります。この書き方の場合、基準日と同じ曜日であっても、未来の指定になるため、2008-07-30になります。


さらに、「first Wednesday of July 23rd, 2008」となると、first Wednesday ofが「序数+曜日+of」という書式と解釈されてその月のn番目の曜日を計算します。つまり、まず絶対指定部分を解釈して2008-07-23となるのですが、日の部分は無視して7月の最初の水曜日を返すため、結果として2008-07-02となります。こんなのを見た人は確実に混乱すると思うので、この書式を使う場合は年と月だけ書くのがいいと思います。


他にも似たようで少し違うパターンについての説明が「相対的な書式」の2つめの「注意」として書いてあります。カオスすぎるのでチラ見程度で十分でしょう。

注意点など

この文書について気になる点がいくつかあります。


まず、どの書式がどのバージョンから利用できるかという情報が一切ありません。PHP5.3.0以降を使う分には問題ないと思いますが、それ以前のバージョンを使う場合は気をつけてください。


また、現時点では「March 1st 2011 -1day +1month」のように相対的な記述が複数あった場合の解釈順序は説明がありません。私の手元にある大半のPHPのバージョンでは順番にかかわらず+1monthの方を先に解釈するようですが、たまたまなのか仕様なのかわかりません。


このような場合でも、たとえば下記のようにDateTimeクラスのmodifyメソッドを利用することで、相対指定を期待通りの順番で解釈させることができます。

<?php
date_default_timezone_set('Asia/Tokyo');
$t = new DateTime("March 1st, 2011");
$t->modify("+1month");
$t->modify("-1day");
echo $t->format("c"); // 2011-03-31T00:00:00+09:00

まとめ

  • PHPの日付文字列の解釈ルールがドキュメント化されました
    • ざっと目を通した方がいいと思います!
    • PHP 5.3以降でないと使えない記述もあるので注意
  • 曜日や序数のような相対指定も文書化されたので、使う上での安心感が少し増した
  • 相対的な記述の解釈は、他の部分の解釈が終わったあと
    • 相対的な書式を2個以上含んでいる場合の挙動は不明確


僕自身も腑に落ちない点がいくつかあるので、何か気づいたことがあったら教えてください!

*1:PHP 5.3.5現在、解釈のコア部分はre2cで書かれています。興味を持った方はext/date/lib/parse_date.reを眺めてみてください。

*2:過去のstrtotimeのマニュアルにはそのような記述があり、「Date Input Formats」へリンクされていました。