BLOG

ブログ

2022/01/11 技術系

【PHP】小数点演算には気をつけよう

この記事を書いた人 D.T

今更感の強い話題かもしれませんが、PHPで小数点計算を実行する際は、本当に正しい数値になっているのか気をつけましょうという話です。

発生した事象

単純な加算を行なって、指定した期間の合計を表示するロジックを組んでいたところ、下記の画像のような結果となりました。

<?php
$records = array(
    [
        "date" => "2021/01/01",
        "amount" => 13.05
    ],
    [
        "date" => "2021/01/02",
        "amount" => 17.05
    ],
    [
        "date" => "2021/01/03",
        "amount" => 14.3
    ],
);

$totalAmount = 0;

foreach ($records as $record) {
    $totalAmount += $record['amount'];
}

echo(number_format($totalAmount, 15)); 
// 結果:44.400000000006
小数点第二位までの表示をさせたいのですが、小数点が続いてしまっています。

44.400000000000006 の場合、問題としては顕在化しにくいですが、 floor関数などで小数点以下を丸めたい場合、 内部的な精度の誤差を認識していなければ、意図しない結果となってしまうかもしれません。

<?php
echo(floor((0.1 + 0.7) * 10)); 
// 結果:7 

この例の場合、8を期待するのですが、 0.1 + 0.7の計算結果は内部的な精度の誤差で0.799999999999999933386618522491… となるため、 floor関数で丸めた結果、7となってしまいます。

なぜ発生したのか

浮動小数点の精度が計算結果に誤差を生じさせているようでした。

十進数で正確な小数を表せる有利数であっても、二進数の浮動小数点としては正確に表現できないようです。

浮動小数点とは

浮動小数点数の精度は有限です。 システムに依存しますが、PHP は通常 IEEE 754 倍精度フォーマットを使います。 この形式は、1.11e-16 のオーダーでの丸め処理で誤差が発生します。 複雑な算術演算をすると、誤差はさらに大きくなるでしょう。そしてもちろん、 いくつかの演算を組み合わせる場合にも誤差を考慮しなければなりません。

さらに、十進数では正確な小数で表せる有理数、たとえば 0.1 や 0.7 は、 二進数の浮動小数点数としては正確に表現できません。 これは、仮数部をいくら大きくしても同じです。 したがって、それを内部的な二進数表現に変換する際には、どうしても多少精度が落ちてしまいます。 その結果、不思議な結果を引き起こすことがあります。たとえば、 floor((0.1+0.7)*10) の結果はたいてい 7 となるでしょう。おそらくは 8 を想定していらっしゃるでしょうが、そのようにはなりません。 これは、(この計算結果の) 内部的な値が 7.9999999999999991118… のようになっているからです。

引用:php公式リファレンス https://www.php.net/manual/ja/language.types.float.php

解決方法

結論として、BCMath関数を利用することで解決することができます。

BCMath関数:https://www.php.net/manual/ja/ref.bc.php

簡単な計算を例にとって説明します。まずは加算(bcadd)を例に計算させてみます。

<?php
foreach ($records as $record) {
  $oldAmount = $total[$record->date]['amount'];
  $total[$record->date]['amount'] = floatval(bcadd($record->amount, $oldAmount, 2));
}

// 結果:44.40

次に、減算(bcsub)を利用して計算させてみます。

<?php 
echo 1000 - 999.4;
// 結果:0.60000000000002
<?php
echo bcsub('1000', '999.4', 2);
// 結果:0.60

BCMath関数を利用する場合の引数はString型である必要があります。小数点を第何位まで表示させたいかを第三引数に定義します。

合計値(加算)の場合は、bcaddを利用して浮動小数点の精度を上げることができます。また、BCMath関数の結果はString型となります。結果を再計算させたい場合はフォーマットが必要となる場合もあるため、ご注意ください。

補足

Homesteadを使用せずに、Laravelアプリケーションを構築している場合は、BCMath PHPの拡張が必要となります。今回は、BCMath関数を利用するためにbcmath_compatライブラリを使用しました。

composer require phpseclib/bcmath_compat

インストール完了後、Laravel内でも利用することができます。

GitHub:https://github.com/phpseclib/bcmath_compat

まとめ

round関数floor関数を使用して解消できないかと考えたのですが、期待する計算結果とはなりませんでした。内部的な精度の誤差を意識して、計算結果が正しいかを確認しながら小数点演算の処理は進めていくべきだなと改めて感じました。

アーカイブ