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

（この記事は [地平線に行く](https://yujisoftware.hatenablog.com/entry/2019/07/28/180017) とのマルチポストです）

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|<font color="red">**11**</font>|43|30|**"GMT"**|
|Mac|115|3|19|<font color="red">**20**</font>|43|30|**""**|

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

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

# PHP のドキュメント

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

> 注意:
> 内部では、 **この関数はシステムの C ライブラリ関数 strptime() をコールしています。 このライブラリ関数は、OS によって挙動が異なることがあります。**
> [PHP: strptime - Manual](https://www.php.net/manual/ja/function.strptime.php)

ただ、どのように挙動が異なるかは記載されていません。そのため、次に各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|<font color="red">**11**</font>|43|30|**"GMT"**|
|`Sun, 19 Apr 2015 11:43:30 JST`|115|3|19|<font color="red">**11**</font>|43|30|**"JST"**|

# Mac (BSD系UNIX) の場合

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

> 原文：
> 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)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/strptime.3.html)

ただし、文字列中のタイムゾーンとして指定できるのは `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|<font color="red">**20**</font>|43|30|**""**|
|`Sun, 19 Apr 2015 11:43:30 JST`|115|3|19|<font color="red">**20**</font>|43|30|**""**|


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

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

> date_parse_from_format() はこの問題の影響を受けないので、PHP 5.3.0 以降ではこちらの関数を使うことを推奨します。
> [PHP: strptime - Manual](https://www.php.net/manual/ja/function.strptime.php)

# まとめ

PHP は [Write once, Run anywhere](https://ja.wikipedia.org/wiki/Write_once,_run_anywhere) じゃなかった。