PHPを使って金額の計算を行う際、特に精度が重要な場合(例えば、経理アプリケーションなど)には、BCMath
関数が非常に有用です。しかしそれだけでは、規模の大きいなシステムで複雑な計算ロジックを変更する際に問題を引き起こすことがあります。そこで、金額を扱う際は「クラス」を使って抽象化・共通化しておいたほうが良さそうに感じていたので記事にしてみようと思います。
BCMath関数の基本的な使い方
BCMath
は、PHPの拡張モジュールであり、任意の精度で浮動小数点の計算を行うための関数群です。これにより、標準の浮動小数点演算では避けられない精度の問題を回避できます。特に丸め誤差により1円の差が発生して問題になることがたまにあります。
以下にBCMath
を使った基本的な計算例を示します。
例1: 足し算
<?php
$amount1 = '123.456';
$amount2 = '789.123';
$result = bcadd($amount1, $amount2, 2); // 2桁の精度で加算
echo $result; // 出力: '912.57'
例2: 掛け算
<?php
$amount1 = '100.00';
$amount2 = '1.25';
$result = bcmul($amount1, $amount2, 2); // 2桁の精度で掛け算
echo $result; // 出力: '125.00'
これらの関数はどれも、float
型の誤差を気にせず計算できるので金額計算に適しています。特に小数点以下の精度が重要な場合にはBCMath
が非常に有効です。
BCMath関数を使う際の注意点
BCMath
を使う際にはいくつか注意点があります。以下の点を意識して使用しましょう。
1. string型で値を渡す必要がある
BCMath
の関数に渡す値はstring
が期待されています。float
の値を渡してしまうと期待した結果にならない可能性があるので注意してください。
<?php
$num1 = 0; // (string) 0 => '0'
$num2 = -0.000005; // (string) -0.000005 => '-5.05E-6'
echo bcadd($num1, $num2, 6); // エラー: bcadd(): Argument #2 ($num2) is not well-formed
$num1 = 1.99999999999999; // (string) 1.99999999999999 => '2'
$num2 = 2.0; // (string) 2.0 => '2'
echo bcadd($num1, $num2, 20); // 出力: '4.00000000000000000000'
どうしてもfloat
の値を渡したい場合、例えば下記のように文字列に変換するなどの方法があります。
<?php
$num1 = 0; // (string) 0 => '0'
$num2 = sprintf('%.6F', -0.000005); // '-0.000005'
echo bcadd($num1, $num2, 6); // 出力: '-0.000005'
$num1 = sprintf('%.14F', 1.99999999999999); // '1.99999999999999'
$num2 = sprintf('%.1F', 2.0); // '2.0'
echo bcadd($num1, $num2, 20); // 出力: '3.99999999999999000000'
2. BCMath関数は処理が遅いので無闇に使わない
実感できる程ではないかもですがBCMath
は遅いと言われています。全ての計算をBCMath
の関数で行っているとパフォーマンスに影響が出ることも考えられるので、精度に問題がない場合は標準の数値型で計算するほうが良いと思います。
3. BCMath関数には丸め関数がない
BCMath
関数には、floor
、ceil
、round
のような丸め処理を行う関数がありません。もし、数値を丸める必要がある場合は、自分で文字列処理を使って実装するか、BCMath
の関数群を組み合わせて実装する必要があります。
※この後に記載しますがPHP8.4からは丸め関数が追加されています。
PHP8.4でのBCMathに関する改善点
2024年11月21日にリリース予定のPHP8.4でBCMath
に関していくつか改善点がありますので簡単にですが紹介します。
- Added BcMath\Number class.
クラスBcMath\Number
が追加されました。これはイミュータブルなオブジェクトであり、BCMath
の全ての計算メソッドを持っています。
<?php
use BCMath\Number;
$num = new Number('1');
$num2 = new Number('2');
$result = $num->add($num2);
echo $result->value; // 出力: '3'
- Added bcfloor(), bcceil(), bcround().
BCMath
に丸め関数bcfloor
、bcceil
、bcround
が追加されました。これで独自に丸め処理を実装する必要がなくなります。
- Added bcdivmod().
BCMath
に商と余りを取得する関数bcdivmod
が追加されました。Pythonのdivmod()
のようなものでしょうか。
- Improved performance of number conversions and operations.
浮動小数点数への変換や演算のパフォーマンスが向上しました。この改善の担当をされた方の話では、かなり高速化を実現されたようです。
現実的な範囲でそれなりに大きな桁数の場合、現時点では加算と減算は大体倍速、乗算は3倍速、除算は10倍速くらいになっています。
引用元:PHPコア開発者になって半年経ったので、php-srcでの活動を振り返る
金額を表現するのにクラスで抽象化する理由
やっと本題となりますが、金額を単なる数値として扱うのではなく、クラスを作成して抽象化することで、以下のようなメリットがあると考えています。
1. 意味的な明確化
数字だけで有効桁数や端数処理の決まり事などを表現することができません。クラスにすることで金額に関するルールを定義し、単なる数字ではなく金額として認識させることで、実装やコード理解の助けになると考えています。
2. 計算の一貫性を保つ
数字そのものに対して計算を行うと、異なる単位や小数点以下の精度に関する問題が発生することがあります。金額クラスを使うことで、計算方法を統一しやすく、常に同じ精度で計算を行えるようにすることができると考えています。
3. 保守性の向上
もし金額の計算方法に変更が必要になった場合、数値型を直接使っているコードだと、変更箇所を探すのが大変です。しかし、クラスを使って抽象化しておけば、クラスの中だけで変更が完結し、影響範囲を最小限に抑えることができると考えています。
簡単な金額クラスの例
以下は、イメージを掴んで貰うための簡単な例です。金額を管理し、BCMath
を使って足し算や引き算を行うことができるクラスです。
<?php
class Money
{
private string $amount;
private int $scale; // 有効桁数
// コンストラクタ
public function __construct(string $amount, int $scale = 2)
{
$this->amount = $amount;
$this->scale = $scale;
}
// 足し算
public function add(Money $other): Money
{
$result = bcadd($this->amount, $other->amount, $this->scale);
return new self($result);
}
// 引き算
public function subtract(Money $other): Money
{
$result = bcsub($this->amount, $other->amount, $this->scale);
return new self($result);
}
// 金額の表示
public function __toString(): string
{
return $this->amount;
}
// 金額の取得
public function getAmount(): string
{
return $this->amount;
}
}
このクラスに例えば税計算など計算ロジックを追加したりして、計算の一貫性を保つような実装をしていくと良い感じになるのではと思います。
クラスを使わない選択肢もある
もちろん、金額をクラスで扱う方法が常に最適というわけではありません。単純な計算や、小規模なアプリケーションでは、金額を数値として直接扱う方が簡潔で効率的な場合もあります。また大きな規模のアプリケーションなどでは変更コストが大きく、費用対効果が悪いと感じることもあると思います。
目的や効果を理解せず納得感を持たないまま、無理に取り入れようとしても上手くいかないと思います。中途半端で役に立たないものになってしまうので止めたほうが良いと思います。
まとめ
-
BCMath
関数を使うことで高精度な金額計算ができます - 金額をクラスで抽象化することで、意味的な明確化、計算の一貫性、保守性の向上が期待できます
- 無理にクラスを使う必要はなく納得感を持った上で取り組みましょう
金額計算の精度と可読性を保ちながら、より良いアーキテクチャを作るために、ぜひ金額をクラスで抽象化して管理する方法を検討して貰えればと思います。