ど素人から毛を生やす。<延>

PHPとjsで浮動小数点誤差問題と闘う

Web > PHP 2020年1月28日(最終更新:4年前)

2020年1月28日に作成されたページです。
情報が古かったり、僕が今以上のど素人だった頃の記事だったりする可能性があります。

どもです。

ウェブシステム上でお金の計算をしたい。
となれば、厳密な数値を算出する必要があります。

が、一般的にコンピュータ計算では浮動小数点による小数計算誤差問題がつきものです。
これの対策をしないと、切り捨てや切り上げの段階で最下桁が狂う可能性があります。

MySQLなど他言語では、DECIMAL型という桁数を厳密に指定して誤差を回避するデータ型があります。
が、PHPやJSは浮動小数点型のfloatやdoubleしかありません。さて、どうしよう。

PHPの場合 BCMath(任意精度数学関数)を使う

$t1 = 1800 * 1.08;
var_dump($t1); //float(1944)
var_dump(rtrim(sprintf('%.40f', $t1), '0')); //string(45) "1944.0000000000002273736754432320594787597656"

$t2 = bcmul(1800, 1.08, 2);
var_dump($t2); //string(7) "1944.00"

BCMath関数を利用すると、結果を文字列で取得します。
PHPは数字の文字列を数値扱いでしてくれますので、そのまま切り上げ切り捨てできますし、結果も狂いません。

ちなみに、BCMath関数は有効桁数未満を「切り捨て」します。
これが切り上げや四捨五入だと、丸めたい桁数+2桁まで取らなければいけませんが、切り捨てなので+1桁を第三引数に指定しましょう。

なお、小数計算に対応した計算関数ではgmp関数もあるのですが、こっちのがマイナーっぽいです。

両方ともバニラのPHPには存在しないため、別途インストールが必要。でもBCMathは標準装備している会社が多いみたいです。
僕のケースもBCMathは実装済みでした。

参照:[PHP マニュアル]BCMath 任意精度数学関数
参考:[Qiita]浮動小数点数の乗算でハマった話

最終手段はString型変換

var_dump((string)(1800 * 1.08)); //string(4) "1944"

関数がインストールされていなくて、かつインストールできない場合は、計算結果を文字列に変換してしまいましょう。
だいたいのケースはこれで上手くいきます。(100%保障はありません)

先の例で、厳密には小数点以下の数値を持っていた$t1が、var_dumpではfloat(1944)と出力しました。
String型に変換すると、この丸められた「1944」を文字列にするので、結果としてBCMathと同じ結果になります。

繰り返しますが、100%保障はありません。
たまに小数点第一位くらいから狂う計算とかあります。その場合はString取得では狂います。

基本的にはBCMathを使うのがベストです。

JSの場合 Ajaxで投げる or 計算用関数を自作 or ライブラリを使う。

さて、問題はJSの方です。
実はDECIMAL型も無ければ、PHPのような計算用関数もありません。

マジで無いです。

仕方ないからString(1800 * 1.08)しようか…

>> 1944.0000000000002 <<

PHPみたいに表示用に丸めてくれないので! 文字列にしても無駄です!!

JSで実現しようとすると、かなり面倒な関数を書かなきゃならないようですね。
こちらでやり方が解説されています。

自分で関数をゴリゴリ書くか、世にある小数点計算用ライブラリを使うか。

僕はもうAjaxで投げてPHPに処理させるで良いかな! と思います!
JSで計算する必要があるときって、基本的に、一度に一つや二つの計算しかしないので効率度外視。
場合によってはPHP側の計算用関数と同じ処理を通せるので、メンテ性も高まって一石二鳥です。

この記事は役に立ちましたか?
  • _(:3」∠)_ 面白かった (0)
  • (・∀・) 参考になった (0)
  • (`・ω・´) 役に立った (0)