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関数を使うことで高精度な金額計算ができます
- 金額をクラスで抽象化することで、意味的な明確化、計算の一貫性、保守性の向上が期待できます
- 無理にクラスを使う必要はなく納得感を持った上で取り組みましょう
金額計算の精度と可読性を保ちながら、より良いアーキテクチャを作るために、ぜひ金額をクラスで抽象化して管理する方法を検討して貰えればと思います。
