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

ダブランとTRPGを愛する、いわゆる「紙一重」の元web会社員が、
夢を叶えようとしたり趣味にいそしんだりする場末のブログ。

HN 餅。
好きなポケモン ダブラン・バタフリーたん・リザードン先輩・ピンクの悪魔ラッキー

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

Web > PHP 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)