PHP で小数点以下の桁数を指定して切り上げ・切り捨てする方法

PHP で少数点以下の桁を指定して数値を切り上げ・切り捨てする方法についてです。 尚、今回動作確認には PHP のバージョンの 7.2 を使用しました。

組み込み関数

PHP では、数値を丸めるための組み込みの関数として次のものが用意されています。

  • round()
  • ceil()
  • floor()

名前から想像がつくとおり round() は四捨五入(等)、 ceil() は切り上げ、 floor() は切り捨てです。

PHP Manual のリファレンスによると宣言部はそれぞれ次のとおりです。

float round ( float $val [, int $precision = 0 [, int $mode = PHP_ROUND_HALF_UP ]] )
float ceil ( float $value )
float floor ( float $value )

引数を見るとわかるとおり、 round() は第 2 引数 $precision で桁数の指定ができますが、 ceil()floor() にはそのようなオプションはありません。

// round()
assert(round(3.1415) === 3.0);
assert(round(3.1415, 1) === 3.1);
assert(round(3.1415, 2) === 3.14);
assert(round(3.1415, 3) === 3.142);

// ceil()
assert(ceil(3.1415) === 4.0);

// floor()
assert(floor(3.1415) === 3.0);

つまり、切り上げ・切り捨てを桁数指定で行いたい場合は独自に実装する必要があります。

独自の関数

私が現時点で最もシンプルかつ信頼できそうだと思う、桁数指定ができる ceil()floor() の書き方は次のとおりです。

ceil_plus():

function ceil_plus($value, $precision = 1) {
  return round($value + 0.5 * pow(0.1, $precision), $precision, PHP_ROUND_HALF_DOWN);
}

floor_plus():

function floor_plus($value, $precision = 1) {
  return round($value - 0.5 * pow(0.1, $precision), $precision, PHP_ROUND_HALF_UP);
}

ロジックとしては、元の値に調整用の値( $precision0 の場合は +-0.5 )を加えてから round() を使っています。

次のようにすると、ある程度期待したとおりに動くことが確認できます。

assert(ceil_plus(3.1415, 0) === 4.0);
assert(ceil_plus(3.1415, 1) === 3.2);
assert(ceil_plus(3.1415, 2) === 3.15);
assert(ceil_plus(3.1415, 3) === 3.142);
assert(ceil_plus(3.1415, 4) === 3.1415);
assert(floor_plus(3.1415, 0) === 3.0);
assert(floor_plus(3.1415, 1) === 3.1);
assert(floor_plus(3.1415, 2) === 3.14);
assert(floor_plus(3.1415, 3) === 3.141);
assert(floor_plus(3.1415, 4) === 3.1415);

ceil_plus() で使われている round() の第 3 引数の PHP_ROUND_HALF_DOWN は「 0.5 ちょうどの場合は切り捨て」にするオプションです。 四捨五入ならぬ五捨六入と言う感じになります。 これは例えば、数値 3.50 を少数点以下 1 桁になるように切り上げたい場合に、 3.50 + 0.053.55 としてから round() を適用した結果が 3.5 になってもらいたいために指定しています。

floor_plus()PHP_ROUND_HALF_UP も同様のオプションで、こちらは「 0.5 ちょうどの場合は切り上げ」にすることを指定しています。 こちらは通常の四捨五入そのものという感じでしょうか。

注意点

この小数点が絡む切り上げ・切り捨ての問題についてはいくつか注意点があります。

注意点 A: sprintf() の挙動

sprintf() で浮動小数点数のフォーマット指定をすると切り捨てがきれいにできそうに思えたのですが、そんなことはありませんでした。

sprintf('%.3f', 3.1415);
// => 3.142

正確なロジックはわかりませんが、私の環境では四捨五入がされた後のような値が返ってきます。 なんだかありがたいようなありがたくないような感じですね。

ある程度の正確さを求めるときにはこの sprintf() を使うのはやめておいた方がよさそうです。

注意点 B: ceil()floor() の挙動

これは PHP にかぎらず浮動小数点数を扱う際の宿命ですが、丸め誤差により思わぬ挙動をすることがあります。

試しに 1 から 100 までの整数に対して、「 100 で割って 100 をかけてから ceil() を適用する」ということをすると、ズレた値が返ってくるものがいくつかあります。

foreach (range(1, 100) as $value) {
  $calc = ceil($value / 100 * 100);
  if ($calc != $value) {
    print("$value != $calc" . PHP_EOL);
  }
}
// =>
// 7 != 8
// 14 != 15
// 28 != 29
// 55 != 56
// 56 != 57

100 件中 5 件なので、この範囲だけで考えると 5% ぐらいの確率で値がズレるということになります。

ということは、小数点以下の桁数を指定できる ceil() の実装として人が真っ先に思いつくのは次のような関数だと思うのですが、これは結構な確率で値がズレるということになります。

function ceil_plus_2($value, $precision = 1) {
  $shift = pow(10, $precision);
  return ceil($value * $shift) / $shift;
}
ceil_plus_2(0.07, 2);
#> 0.08

ウェブ上ではこの ceil_plus_2() の実装を勧めているページをよく見ますが、このあたりの制約に十分に注意されているものが少ないような気がします。

理屈としては単純なことでも気をつけるべき点が意外と多いものですねぇ。