strptime は環境によって挙動が違う

PHP には、日付文字列をパースするための strptime という関数があります。 これを使って、Sun, 19 Apr 2015 11:43:30 GMT という文字列を %a, %d %b %Y %H:%M:%S %Z というフォーマットでパースした結果、以下の通り環境によって異なる結果になりました。 strptime は環境によって挙動が違うんですね。

OS tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Linux 115 3 19 11 43 30 "GMT"
Mac 115 3 19 20 43 30 ""

Linux の場合は GMT で返ってきましたが、Mac の場合は JST で返ってきました。 (どちらも、OS のタイムゾーン設定は JST です)

どうしてこのような違いが起きたのか、ドキュメントを確認してみました。

PHP のドキュメント

ちゃんとOSにより挙動が異なることが明記されています。

注意: 内部では、 この関数はシステムの C ライブラリ関数 strptime() をコールしています。 このライブラリ関数は、OS によって挙動が異なることがあります。 PHP: strptime - Manual

ただ、どのように挙動が異なるかは記載されていません。そのため、次に各OSのドキュメントを確認してみました。

Linux の場合

フォーマットの指定として %Z (タイムゾーン) を指定することはできるが、無視すると書かれています。

原文: For reasons of symmetry, glibc tries to support for strptime() the same format characters as for strftime(3). (In most cases the corresponding fields are parsed, but no field in tm is changed.) This leads to %Z The timezone name.

日本語: 対象性のために、glibc では strptime() に strftime(3) と同じフォーマット文字をサポートさせようとしている。多くの場合、対応するフィールドが解釈されるが、tm フィールドは変更されない。使用可能なフォーマット文字を以下に示す。 %Z タイムゾーン

そのため、文字列中のタイムゾーンの指定が GMT だろうが JST だろうが、以下のように同じ結果が返ってきます。「11時って書いてあるんだから11時だろ」という解釈です。

入力 tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Sun, 19 Apr 2015 11:43:30 GMT 115 3 19 11 43 30 "GMT"
Sun, 19 Apr 2015 11:43:30 JST 115 3 19 11 43 30 "JST"

Mac (BSDUNIX) の場合

現地のタイムゾーンに変換してくれるそうです。

原文: The strptime() function parses the string in the buffer buf, according to the string pointed to by format, and fills in the elements of the struc-ture structure ture pointed to by tm. The resulting values will be relative to the local time zone.

日本語: strptime() 関数は、formatが指す文字列に従ってバッファbuf内の文字列を解析し、timeptr が指す構造体の要素にあてはめます。 結果の値は現地のタイムゾーンを基準にしたものになります。

Mac OS X Manual Page For strptime(3)

ただし、文字列中のタイムゾーンとして指定できるのは GMT かローカルタイムゾーン (今回の場合は JST) のみだそうです。

原文: The %Z format specifier only accepts time zone abbreviations of the local time zone, or the value "GMT". This limitation is because of ambiguity due to of the over loading of time zone abbreviations. One such example is EST which is both Eastern Standard Time and Eastern Australia Summer Time.

日本語: %Z フォーマット指定子は、ローカルタイムゾーンタイムゾーンの省略形、または値 "GMT"のみを受け入れます。 この制限は、タイムゾーンの略語のオーバーロードによるあいまいさのためです。 そのような例の1つは、東部標準時と東オーストラリアの夏時間の両方であるESTです。

やってみると、こうなりました。「GMT の11時って書いてあるんだから、現地時間の20時だろ」という解釈です。

入力 tm_year tm_mon tm_mday tm_hour tm_min tm_sec unparsed
Sun, 19 Apr 2015 11:43:30 GMT 115 3 19 20 43 30 ""
Sun, 19 Apr 2015 11:43:30 JST 115 3 19 20 43 30 ""

OSによらず、同じ結果になるようにしたい場合は?

マニュアルに書いてありました。date_parse_from_format 関数を使いましょう。

date_parse_from_format() はこの問題の影響を受けないので、PHP 5.3.0 以降ではこちらの関数を使うことを推奨します。 PHP: strptime - Manual

まとめ

PHP は Write once, Run anywhere じゃなかった。