PHP の小数演算で結果がおかしくなるパターンと対応策

株式会社オズビジョンのユッコ (@terra_yucco) です。

先週は一週間有休をいただき、漆黒の FF14 をがっつりプレイしてきました。

今週からまた、ぽつぽつ Qiita を更新していきます。


小数演算

人間の計算は基本的に 10 進数の上に成り立ちますが、コンピュータは内部的には値を 2 進数で保持します。

その結果、小数の演算では、想定する値が算出されないことがあります。


不具合のあるコード

以下は元値 (1000) と係数 (0.7 や 0.6) によりパーセントで該当する値を求めたいコードになります。

$ php -a

Interactive shell

php > echo floor(1000 * (0.7 / 100));
6
php > echo floor(1000 * (0.6 / 100));
6

1 例目は個人的には 7 になってほしいです。というか皆さんそうだと思います。


対応するには

他にもいくつか解はあるようですが、この問題に対応した際には、改修コストなどを考えて string キャストを選択しました。

php > echo floor((string)(1000 * 0.6) / 100);

6
php > echo floor((string)(1000 * 0.7) / 100);
7

無事に 7 になりました。


影響範囲を調査するためのコード

結果は膨大になってしまうので割愛しますが、どのような係数を指定したときに値が想定からずれるのかを調べるため、以下のテストコードを書いて範囲を調べました。

(これがこの記事のメイン)


  • 内部を 0-50 50-100 の 2 つに分割しているのは、1 つにすると vagrant のメモリが不足したため

  • 今から見返すと 50 が 2 回登場している (ノ∀`)アチャー

最初の方だけ列挙すると、以下のケースで、string キャストありなしで差分が出ました。


  • 0.7

  • 1.4

  • 2.8

  • 4.1

  • 5.6

  • 7.1

  • 7.3

  • 8.2

  • 9.7

  • 11.2

<?php

/**
* 小数計算処理全般のテスト
*/

class CalculatorTest extends CIUnit_TestCase
{
/**
* @param $計算元値
* @param $係数
* @param $計算結果小数
*
* @dataProvider 小数算出処理のバリエーション用データ_0_50
*/

public function 小数算出処理のテスト_0_50($計算元値, $係数, $計算結果小数)
{
$target = new Calculator();
$this->assertEquals(
$計算結果小数,
$target->decimals(
$計算元値,
$係数
)
);
}

/**
* @return array
*/

public function 小数算出処理のバリエーション用データ_0_50()
{
$data = array();
for ($i = 0; $i <= 50; $i = round($i + 0.1, 2)) {
array_push($data, array(
'計算元値' => 1000,
'係数' => $i,
'計算結果小数' => (string)(1000 * $i) / 100)
);
}
return $data;
}

/**
* @param $計算元値
* @param $係数
* @param $計算結果小数
*
* @dataProvider 小数算出処理のバリエーション用データ_50_100
*/

public function 小数算出処理のテスト_50_100($計算元値, $係数, $計算結果小数)
{
$target = new Calculator();
$this->assertEquals(
$計算結果小数,
$target->decimals(
$計算元値,
$係数
)
);
}

/**
* @return array
*/

public function 小数算出処理のバリエーション用データ_50_100()
{
$data = array();
for ($i = 50; $i <= 100; $i = round($i + 0.1, 2)) {
array_push($data, array(
'計算元値' => 1000,
'係数' => $i,
'計算結果小数' => (string)(1000 * $i) / 100)
);
}
return $data;
}
}


Conclusion

小数の計算をする場合には、こういうケースもあるので気を付けよう、というお話でした。

きちんとやるのであれば BCMath を入れるのが本来はよさそうです。