はじめに
2025年7月19日開催のPHPカンファレンス関西に参加したので参加したセッションをまとめてみます。
このセッションでは、会計処理などで行う四則演算の際に生まれる小さな誤差を防ぐための工夫について、具体的な事例とともに紹介されており、とても勉強になったので改めて自分でもまとめました。
概要
このセッションでは、実際に開発・運用に携わってこられた会計システムにおいて引き起こされる問題と、それに対処するための技術的な解決策について紹介いただきました。
0.1を10回足しても、1にならない問題
下記のように0.1を10回足し合わせた数を1ドルに直しても、1にならないらしい。
実際に見てみるとたしかに想定していない数値が。
$yen = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
$dollar = $yen * 150;
var_dump($dollar);
echo $dollar;
気になったので自分でもint型、String型に変換してみるとこのような結果。
このように想定外の結果になると
- 謎に帳簿が合わなくなる
- 納税額が変わる
などの不測の事態につながります。
税率の改定や、複数税率(軽減税率)への対応など、既存のシステムから変更がある場合に計算結果が違う可能性を予測していないと危なそうです。
具体的な対策について
これらを踏まえて、セッションでは以下のよう対策を挙げられていました。
・算術演算子はなるべく使用しない
・できていいことだけできるように設計でカバーする
BCMathの使用考える
BCmathはいろんな計算ができるPHPの拡張機能です。
例えばbcmul()でさっきの計算を行ってみると、
$yen = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
$exRate = 150;
$dollar = bcmul($yen, $exRate);
var_dump($dollar);
echo $dollar."<br>";
結果は下記です。戻り値もString型で返ってきました。
注意点
BCMathを使用すると若干処理速度が落ちてしまうようです。(が正確に計算できるほうがよさそうですね。)
比較
下記の結果は等価にならない。
$addResult = 0.1 + 0.2;
if($addResult === 0.3) {
echo "等価だよ";
} else {
echo "等価ではない";
}
var_dump($addResult);
結果は下記です。
↑のように、0.1 + 0.2の結果が循環小数になってfloat型になるため。
bccomp()を使用することで、比較をおこなうことができます。
if(bccomp($addResult, 0.3, 2) === 0) {
echo "等価だよ";
} else {
echo "等価ではない";
}
bccompは0,1,-1のいずれかを戻り値として返却します。
今回のケースだと
//第1引数が第2引数より小さい場合
echo "0.2の場合";
echo bccomp($addResult, 0.2, 2);
//第1引数が第2引数と同じ場合
echo "0.3の場合";
echo bccomp($addResult, 0.3, 2);
//第1引数が第2引数より大さい場合
echo "0.4の場合";
echo bccomp($addResult, 0.4, 2);
型変換
0.1を10回足し合わせる計算をすると0.999999...のような値になるが、更にこれを型変換すると
のようになる。
このような場合、sprintfで出力すると下記のようになる。
var_dump(sprintf("%.16f", $yen));
ためしに
0.000001を10回足し合わせるとこうなる。
var_dump(sprintf("%.20f", $yen));
更に小さな数字を扱うと、Eを含んだ数値になってしまうことがある。
sprintfを使うことで回避できる。
var_dump(sprintf("%.20f", $yen));
sprintfで計算してひと安心かというと、そうでもなく
↑のように桁数を増やすとおかしくなることがあるのでなるべく変換しない設計でカバーしたい。
できていいことだけできるように設計でカバーする
実際にBCMathを使うとなると結構難しい。全員で共通の書き方できるか、使うの忘れて普通に四則演算で計算してしまったりしないかとか。
プリミティブ型の使用について
int,Stringなどの汎用的な方なので、いくらでも変更できてしまう。壊れやすい。
なので他の型を使えないだろうかと考える。
BCMath / Numberクラスを使う
PHP8.4からしか使えませんが、Number型にすることで数値が入ること明示的にわかるようになります。(外部ライブラリ)
//BCMath/Numberクラスを使用
$value_1 = new Number("0.1");
$value_2 = new Number("0.3");
$sum = $value_1->add($value_2);
型にコードの意味をもたせる
Number型にすることで数値であることまでは絞れるようになりますが、
- マイナスの値なのか
- 膨大な数値が来るのか
などはわからないので、Valueオブジェクトを使うことを検討する。Valueオブジェクトにすると、「0以上で、小数点は2桁まで」といったビジネスルールを閉じ込めることができる。
Valueオブジェクトを使う
数値をオブジェクトとして扱うと、よりわかりやすく意味をもたせることができます。
ただ、すべての処理に対してValueオブジェクトを使えばいいわけではなく、
例えば「リクエストされた値に対しては、Valueオブジェクト扱い、内部で処理するときはBCMathを使う」など、
システムに合わせて使い所を考える必要がありそうです。
チーム開発の場合、完璧でなくてもいいのでドキュメントにユビキタス言語をまとめたりして、開発における言葉の認識を周りの人と合わせていくことが大事だということでした。
まとめでお話されていた「できることを縛る(カプセル化する)」ことで自分の実装に自信が持てるようにするのが大事なのではないかという言葉が特に印象に残りました。
まとめ・感想
お金の取り扱いというテーマにとどまらず、ソフトウェア開発全般に通じる話が詰まっていました。
貴重な学びを得られたセッションでした!