いいように日付を解釈してくれるもの

今年のleap second祭りはけっこう盛り上がりましたね。個人的なMVPはうるう秒のNTT時報‐ニコニコ動画(ββ)だったと思います。

相対的な日付って

さて、10日後っていつだっけ?って思ったときに

% date -d "+10day" +%Y/%m/%d
2009/01/31

とか

% phsh
<?php date('Y/m/d', strtotime('+10day'));
string(10) "2009/01/31"

などとみなさん日々やっていると思いますが、意外にできないのが、『今月の月末って何日?』問題です。個人的にphpでやる最適解は、『今月の1日の1ヶ月後の前日』ってことで

<?php
$base = time();
$date = date('Y/m/d', strtotime('+1month '.date('Ym', $base).'01') - 1); 
var_dump($date);

なんてキモいコード書いたりします。もっとわかりやすい方法ないのかなーと思い、strtotime()ってどうしてるんだろう?ってのをちょっと調べてみました。

対象

なんでgnuphpだけなの? という突っ込みはなしで…。

GNU Coreutils の date コマンド

やっぱり代表的なのは、GNU Coreutils に入ってる (むかしはshellutilsでした) date コマンドのパーサです。

タイムスタンプをどうフォーマットするかは man date にも書いてあるし、

に標準化されているっぽい。そうじゃなくて、たとえば "+1day" みたいな、いいように日付と解釈してくれる文字列で、どういうものが許されるのかが知りたいです。本家、という意味では

あたりでしょうか。

ということでソース読め、と。

php4 の strtotime()

なにげに php4 と php5 で strtotime() の実装はかわっていて、そもそもパースに失敗したときに php5 では false が返ってくると仕様もかわっていたのでした。php4 では coreutils に似た感じで yacc による実装となってます。

  • PHP_4_4 (branch: 1.34.2.8.2) の ext/standard/parsedate.y
php5 の strtotime()

php5 では re2c でできてて、個人的には読みやすいです。

  • PHP_5_2 (branch: 1.26.2.27.2) の ext/date/lib/parse_date.re

おおまかな様子

以下、ちゃんと調べた訳じゃないです。タイムゾーン周りは面倒なのでまったく見てません。

日付
  • まあ無難なもの: "2009/01/10", "2009-01-10", "Jan 10, 2009" とか
    • "-01" は signed number あつかいされててちょっと面白い。
  • 年が2桁のもの: "09/01/10"
    • 2000年問題って話もありますが、そもそもどっちが年で日なのか日常生活でも迷いますよね。
    • coreutils, php4は、最初が4桁以上ならY/m/d, そうでなけれればm/d/Yと見なす。Y は 1969-2068 の間で補完
    • php5では1970-2069で補完してるようだが、2038年問題もあるので、そう大きくはかわらなそう
時刻

ここはそんなに面白いものはないかも。というよりタイムゾーン周りなんだろうな。

  • 無難なもの: "12:34:56", "12:34 PM"
相対指定

ここです、ここ! 重要なのは!

  • よくあるの: "2day ago", "next month", "1 year", "-1hour"
    • ago はその単位だけ前、agoなしなら後、という意味になります
    • 現在時刻からの相対指定になります
  • さらに基準時刻を指定: "next month 2009/01/01", "2009/01/01 +10day"
    • これがよく使うやつ。
  • 数序詞(tORDINAL)まわり
    • this = 0 (this month は今月ってこと)
    • next = first = +1
    • second: php5 のみ +2 で、それ以外はtokenとして除外
    • "1st Jan" 形式はphpのみ。
      • ちなみに 1,2,3,... + st,nd,rd なので、 1nd とか 2st とかも通っちゃいます。
  • fortnight
    • この単語知らなかったんですけど、2週間っていみだそうです。weekと同じ日の単位で、14になってました。
    • php5だと、fort"h"night ってのもある…。typo対策なのかな?
特別なフォーマット

これは圧倒的にphp5の勝利!!

  • @timestamp形式: @1232491250 とか
    • php4のみ非対応
  • Apacheの Common Log Format (CLF): "10/Oct/2000:13:55:36 -0700"
    • php5のみ対応。
    • ふつうの common, combined などで使ってるやつです。
  • ISO week date形式: "2009-W01-6" (2009年の第1週の6日目=土曜日)
    • これが意外に便利なんですよね。ちなみに 2009-W01-1 は 2008/12/29 になります。
  • PostgreSQL年とその日までの累計形式: "2009.100" (2009年の100日目)
    • 知らねー

注意が必要な例

% cat aho.php
<?php
$str = $_SERVER['argv'][1];
echo 'php4: '.`php4 -r 'printf("%s %s\\n", strtotime("$str") > 0 ? "ok" : "ng", date("Y/m/d H:i:s", strtotime("$str")));'`;
echo 'php5: '.`php5 -r 'printf("%s %s\\n", strtotime("$str") > 0 ? "ok" : "ng", date("Y/m/d H:i:s", strtotime("$str")));'`;
echo 'date: '.`date=$(date -d"$str" +"%Y/%m/%d %H:%M:%S" 2>&1) && echo -n 'ok ' || echo -n 'ng '; echo \$date`;

なかんじで比較してみました。若干再掲のものも含む。

todayの意味

これ、すっごい気をつけないと死にます。日付はいいんだけど、時間がヤバい。

  • today
% php aho.php 'today'    
php4: ok 2009/01/21 07:57:02
php5: ok 2009/01/21 00:00:00
date: ok 2009/01/21 07:57:02
  • now
% php aho.php 'now'     
php4: ok 2009/01/21 08:00:20
php5: ok 2009/01/21 08:00:20
date: ok 2009/01/21 08:00:20

あと、php5のみ noon でお昼の12時が使えます。

2桁の年

最後の2桁が年になるってことです。Y2Kとか関係ないからね!

% php aho.php '01/02/03'
php4: ok 2003/01/02 00:00:00
php5: ok 2003/01/02 00:00:00
date: ok 2003/01/02 00:00:00
  • 1970年付近
% php aho.php '01/01/68'
php4: ng 1970/01/01 08:59:59
php5: ng 1970/01/01 09:00:00
date: ng date: invalid date `01/01/68'
% php aho.php '01/01/69'
php4: ng 1969/01/01 00:00:00
php5: ng 1970/01/01 09:00:00
date: ok 1969/01/01 00:00:00
% php aho.php '01/01/70'
php4: ng 1970/01/01 00:00:00
php5: ng 1970/01/01 00:00:00
date: ok 1970/01/01 00:00:00
  • 2038年付近
% php aho.php '01/01/38'
php4: ok 2038/01/01 00:00:00
php5: ok 2038/01/01 00:00:00
date: ok 2038/01/01 00:00:00
% php aho.php '01/01/39'
php4: ok 2038/01/19 12:14:07
php5: ng 1970/01/01 09:00:00
date: ng date: invalid date `01/01/39'
序数詞

first, secondと1st, 2ndの扱いはphp4/5で微妙に違うみたい。php5のほうがちゃんと意味を解釈しようとしてる気がします。また、秒と紛らわしいsecondに果敢にもチャンレジしてるのはphp5だけ。

  • 1st jan形式 (1月の初日)
% php aho.php 'first jan' 
php4: ok 2009/01/01 00:00:00
php5: ng 1970/01/01 09:00:00
date: ng date: invalid date `first jan'
% php aho.php '1st jan'    
php4: ok 2009/01/01 00:00:00
php5: ok 2009/01/01 00:00:00
date: ng date: invalid date `1st jan'
  • first friday形式 (今を基準として、次の最初の金曜日)
% php aho.php 'first friday'
php4: ok 2009/01/23 00:00:00
php5: ok 2009/01/23 00:00:00
date: ok 2009/01/23 00:00:00
% php aho.php '1st friday'           
php4: ok 2009/01/23 00:00:00
php5: ng 1970/01/01 09:00:00
date: ng date: invalid date `1st friday'
  • second friday
% php aho.php 'second friday'
php4: ok 2009/01/23 00:00:01
php5: ok 2009/01/30 00:00:00
date: ok 2009/01/23 00:00:01
    • ようするに、1回目の金曜なのか、次の金曜の1秒後なのか、ってことですね。
  • 単体の序数詞
% php aho.php '1nd'       
php4: ok 2009/01/21 01:00:00
php5: ng 1970/01/01 09:00:00
date: ng date: invalid date `1nd'
    • 意味が不明確、ってことで、php5から廃止されたのかな?
うるう年、うるう秒と無効な日時
% php aho.php '2009/01/01 08:59:60' 
php4: ok 2009/01/01 09:00:00
php5: ok 2009/01/01 09:00:00
date: ng date: invalid date `2009/01/01 08:59:60'
  • うるう年: これは当然。
% php aho.php '2008/02/29'          
php4: ok 2008/02/29 00:00:00
php5: ok 2008/02/29 00:00:00
date: ok 2008/02/29 00:00:00
  • 無効な日付。うーん、なんとも言えない。
% php aho.php '2009/2/29' 
php4: ok 2009/03/01 00:00:00
php5: ok 2009/03/01 00:00:00
date: ng date: invalid date `2009/2/29'
% php aho.php '32th jan'   
php4: ok 2009/02/01 00:00:00
php5: ng 1970/01/01 09:00:00
date: ng date: invalid date `32th jan'
その他
  • 今年の100日目
% php aho.php '2009.100'
php4: ng 1970/01/01 08:59:59
php5: ok 2009/04/10 00:00:00
date: ng date: invalid date `2009.100'
  • 今年の3週目の日曜
% php aho.php '2009-W03-1'    
php4: ng 1970/01/01 08:59:59
php5: ok 2009/01/12 00:00:00
date: ng date: invalid date `2009-W03-1'
  • ちなみにネストできます。"+1day" を 1000 回ネストしたくらいじゃどれも余裕。
% php aho.php '+1day -3day +10day'           
php4: ok 2009/01/29 08:39:00
php5: ok 2009/01/29 08:39:00
date: ok 2009/01/29 08:39:00

まとめ

けっきょく『今月の月末って何日?』はどうなった? 月末=最後の日ってことで、「最後」ぽいものが last くらいなんですけど、lastはどれも序数詞の "-1" 扱いでした。ってことで、無理なのね…。

なんでphpなの? って話はあいかわらずおいとくと、けっこう4と5で挙動が違うので、なんとなく動いてるからokなんて思ってると意外にハマるかもね。validator書いてる人は気をつけましょうw