はじめに
PHP + JavaScript + MySQL というありふれた構成のWebシステムで、どこかで1ずれる問題が発生したので、小数の切り上げ・切り捨て・四捨五入について調べ直した。
標準関数
端数処理は、どの言語でも大体同じ関数名。
| 処理 | PHP | JavaScript | MySQL |
|---|---|---|---|
| 切り上げ | ceil | Math.ceil | CEILING / CEIL |
| 切り捨て | floor | Math.floor | FLOOR |
| 四捨五入 | round | Math.round | ROUND |
正の数1.5を丸めた比較
| 関数 | PHP | JavaScript | MySQL |
|---|---|---|---|
| ceil | 2 | 2 | 2 |
| floor | 1 | 1 | 1 |
| round | 2 | 2 | 2 |
結果に差はない。
負の数-1.5を丸めた比較
| 関数 | PHP | JavaScript | MySQL |
|---|---|---|---|
| ceil | -1 | -1 | -1 |
| floor | -2 | -2 | -2 |
| round | -2 | -1 | -2 |
結果は、jsの四捨五入だけ異なった。負の数だとちょうど0.5まで切り捨てられるらしい。
ceilが数が大きくなる方向で、floorが数が小さくなる方向というのは共通。
浮動小数点数問題
PHPとJavaScriptは共にIEEE754という規格の64bit浮動小数点が使われている。
小数の計算がとにかく合わない。全然合わない。
例えば、1.1×100の計算結果。
JavaScriptだと、「110.00000000000001」となる。
PHPだと、「110」と一見正しいように見えて、1.1 * 100 === 110 は false になる。
PHPはデフォルトでprecision(精度)が14桁で丸め表示されるようで、17桁に変更するとJavaScriptと同じ「110.00000000000001」となった。
var_dump(1.1 * 100); // float(110)
var_dump(ceil(1.1 * 100)); // float(111)
ini_set('precision', 17);
var_dump(1.1 * 100); // float(110.00000000000001)
小数を正しく計算するには
古いシステムで、小数点以下第2位までの数が入った変数に対して、100をかけて整数にしてから計算して、計算結果を100で割るというような処理をしているのを見かけたことがある。先程のように小数に対して100をかけただけで誤差が出ることがあるため、計算精度を上げることにはならないはず。
PHPならBCMath、JavaScriptならbignumber.jsやdecimal.jsなどの有名なライブラリを計算用途に合わせて導入するのが簡単で安全。
MySQLでは
計算精度が必要ならDECIMAL型を使用するだけ。
ちなみに、SQL内に小数を書いた場合、DECIMAL型となる。
SELECT 1.1 * 100; -- 110.0
SELECT 100 * 1.10; -- 110.00
上記はDECIMAL×INTEGER、INTEGER×DECIMALの計算となり、結果はDECIMAL型となる。
結局何が問題だったのか
前提として、計算結果が小数の場合は切り上げして整数にする必要がある項目だった。以下のように、マイナスの場合は0から遠い方向に丸める必要があった。
- 1.5 → 2
- -1.5 → -2
このことをシステム内で「切り上げ」と呼んでいた。
正の数が来ても負の数が来ても上記のような端数処理できる共通関数が、PHP用とjs用にそれぞれ用意されていた。それにも関わらず、SQL側で単純にCEILしている箇所があり、マイナスの値の時に誤差が発生した。
確かに、DECIMAL型のカラムを使った計算では誤差は起きない。ただし、標準のCEIL関数だと、-1.5は-1となってしまう。
結論
保守性を考えると、端数処理はSQLで行ってはダメ。ロジック側に寄せるべき。
切り上げ=ceil、切り捨て=floorと思っている人もいるので、負の数の「切り上げ」「切り捨て」はどちらに丸めることを意味するのか周知徹底が必要。