今年の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()ってどうしてるんだろう?ってのをちょっと調べてみました。
対象
GNU Coreutils の date コマンド
やっぱり代表的なのは、GNU Coreutils に入ってる (むかしはshellutilsでした) date コマンドのパーサです。
タイムスタンプをどうフォーマットするかは man date にも書いてあるし、
に標準化されているっぽい。そうじゃなくて、たとえば "+1day" みたいな、いいように日付と解釈してくれる文字列で、どういうものが許されるのかが知りたいです。本家、という意味では
あたりでしょうか。
ということでソース読め、と。
- coreutils-6.10 の lib/getdate.y
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
おおまかな様子
以下、ちゃんと調べた訳じゃないです。タイムゾーン周りは面倒なのでまったく見てません。
日付
相対指定
ここです、ここ! 重要なのは!
- よくあるの: "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