これは何
ドキュメントをよく読まなかったところから始まった、浮動小数点数のお話である。だからちゃんとドキュメント読めと何回言ったら(ry
発端
OpenWeatherMapのAPIを使用して、実装中のサービスに天気予報を表示することを考えていた。
OWMのAPIはかなり多くの情報を返してくるのだが、全部は必要ないし、使用しているフレームワークの関係上、必要な情報だけを配列にする処理を書く必要があった。
情報の取得に使用したのは、OpenWeatherMap-PHP-APIである。PHP用に作られたAPI集だ。返ってきたデータを元に、天気予報なら\Iterable
の形で返してくれる。
今回の問題を引き起こしたのが、API自体が返してきた値である。以下は、実際にOWMのAPIが返してくる天気予報データ(JSON)の一部である。
"temp":{
"day":15.2,
"min":10.11,
"max":15.2,
"night":10.11,
"eve":15.04,
"morn":15.2
},
今回興味があるのはmax
およびmin
だ。それぞれ最高気温と最低気温を示す。そしてこの数値をPHPはfloat
として解釈する。
ところが今回スペースの関係上整数で表示したいと考えた。そこで、気温を四捨五入した上で整数表示することにした。
四捨五入できる関数はPHPにすでにある。
round関数(float round ( float $val [, int $precision = 0 [, int $mode = PHP_ROUND_HALF_UP ]] )
)だ。
Optionalな引数を全部無視すれば、四捨五入で「整数」にしてくれる(...と思っていた)。
現象
さて、このround
関数を使って先ほどのデータのmax
, min
を四捨五入するとどうなるか。データは$dataという配列の中に入っているとしよう。
round($data['temp']['max']) // => 15
round($data['temp']['min']) // => 10
当然である。
ところが、これが北国に対して秋から冬、もしくは冬から春にかけて実行されるとこんなデータが取得される。
次のデータを考えよう(関係ないデータは省いた)。
"temp":{
"min":-0.44,
"max":4.15,
},
これをroundするとどうなるか?
round($data['temp']['max']) // => 4
round($data['temp']['min']) // => -0 <- WTF?
What the 🤬 PHP?
...と思ったのだが、これはPHPに限った話ではなく、浮動小数点数の仕様である1。-0
が存在するべきパターンがあるということで実装されたわけだ。
ここでPHPのround
の定義を振り返ると
float round ( float $val [, int $precision = 0 [, int $mode = PHP_ROUND_HALF_UP ]] )
となっている。どこをどう見たってfloatで返ってくると書いてあるではないか。
ちなみに
四捨五入の結果を-0
と記述するべきパターンの一つが、まさに今回ハマった「四捨五入された温度が氷点下であることを表す」というパターンである。
Informally, one may use the notation “−0” for a negative value that was rounded to zero.
Signed Zero - Wikipedia(英語版), Scientific Uses
どうしたか
今回のユースケースにおいては、氷点下であることが重要であるわけでもないので、想定される数値範囲的にもint
へのキャストで十分だった。
(int)round($data['temp']['min']) // => 0
-
とはいえなんで整数に見える形で返ってきたのかが非常に疑問なのだが。 ↩