76063


以下のようなPHPの記述をしました。  
出力される値の計算は単純で

[code]

    (1 - 0.25) * 5 = 3.75
    
[/code]

となります。ここでは小数点以下の桁数は1桁なので指定した桁数以下は四捨五入され

[code]

    $3.8
    
[/code]

と出力されると考えます。

[code]

    <?php
    
    $price = 5;
    $off = 0.25;  
    
    
    printf("Price: $%.1f", $price * (1 - $off));
    
    ?>
    
[/code]

＜表示結果＞

[code]

    $3.8
    
[/code]

で合っています。

* * *

ところが、変数 `$off` に 0.35 を代入してみます。

同様に計算すると

[code]

    (1 - 0.35) * 5 = 3.25
    
[/code]

なので同様に指定された小数点以下（1桁）以下を四捨五入して

[code]

    $3.3
    
[/code]

と返ってくると思っていると

[code]

    <?php
    
    $price = 5;
    $off = 0.35;  
    
    printf("Price: $%.1f", $price * (1 - $off));
    
    ?>
    
[/code]

＜表示結果＞

[code]

    $3.2
    
[/code]

と返ってきます。

* * *

ある解説書には「小数点以下で指定された桁数以下の値が5【より】大きい(3.251や3.250001)場合は切り上げ、5【以下】（この場合3.25）以下の場合は切り捨てる」というのもありました。  
しかし、上記した `$off = 0.25` の場合、計算結果は 3.75 なのに四捨五入され $3.8 と返ってきます。

ちなみに `round()` 関数を使うと両方とも四捨五入され各々 $3.8 , $3.3 と返ってきます。

色々尋ねてみると  
「0.25だと浮動小数点数で正確に表せる、0.35は2の冪乗の和じゃないので正確に表せまない。」  
「0.25や0.75など割り切れる数は丸める」と解説していただいたのですが、  
2の冪乗の和や、割り切れる数の意味が初学者の自分にはわかりません。

この挙動の違いを初心者でもわかるように教えていただけないでしょうか？  
よろしくお願いいたします。


CC BY-SA 4.0
2021-05-25T07:30:30.560
76054
2021-05-26T22:58:15.403
2021-05-26T00:57:27.037
3060
42150
question
php
PHP の printf() 関数で小数点以下の桁数を指定した場合について


この辺「十進数による表記（人間に理解しやすい）」と「二進数による表記（コンピュータにわかりやすい）」との差にすぎないのですが、それだけに不慣れな人にはわかりにくいというか。この辺はコンピュータハードウエアの実装の話なので
[php](/questions/tagged/php "'php' のタグが付いた質問を表示") に限らずほぼすべての言語で事情は変わりません。

以下で `^` はべき乗の意味とする

  * 十進数表記 `123.45` は `1*10^2 + 2*10^1 + 3*10^0 + 4*10^(-1) + 5*10^(-2)` の略表記
  * 二進数表記 `101.11` は `1*2^2 + 0*2^1 + 1*2^0 + 1*2^(-1) + 1*2^(-2)` の略表記

後者を１０進数表記で計算してみると `1*4 + 0*2 + 1*1 + 1*0.5 + 1*0.25 = 5.75` っスね。

位取りとは基数のべき乗のことで、小数点より左（上位桁）では正のべき、小数点より右（下位桁）では負のべきにするという定義（っつか読み方というか表記というか）です。なので二進数表記の小数点より右の桁を１０進数表記すると
`2^(-1) = 1/2 = 0.5` だったり `2^(-2) = 1/4 = 0.25`
だったりします。これを「２のべき乗の逆数」なり「２で割り切れる数」なりと呼ぶわけです。

二進数表記したとき、各桁の値は `0` か `1`
しかないわけです。すると二進数表記の有限桁数で厳密に表記できる数は「２のべき乗（の０~１倍）の和」のみということになります。この辺は十進数表記でも全く同じことが言えて「１０のべき乗（の０~９倍）の和」でないと有限桁数で厳密表記ができません。任意のｎに対してｎ進数表記の際に全く同じことが言えます。

有限桁数で厳密表記できない数を正確に表記するには無限桁（循環小数）が必要で、なので有限桁数しか扱えない計算機では（例 `double` は 64bit
数）近似値となってしまいます。この辺の説明をした例が
[浮動小数点による演算で、意図した結果と異なるのはなぜか](https://ja.stackoverflow.com/questions/73212/)
にあったりしますのでこっちも参照。近似値は真値より小さくなることも大きくなることもあります。


CC BY-SA 4.0
2021-05-26T00:06:29.503
76063
2021-05-26T00:32:37.080
2021-05-26T00:32:37.080
3060
8589
76054
answer


`$off` の値を `0.15〜0.95` を変化させた場合の `printf("%.1f", $v)` と `round($v, 1)`
の結果の違いを見てみます。

[code]

    <?php
    
    $price = 5.0;
    $off = array(0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95);
    foreach ($off as $o) {
      $v = $price * (1 - $o);
      printf("%.20f, %.1f, %.1f, %.1f\n",
             $v, $v, round($v, 1), round($v, 1, PHP_ROUND_HALF_EVEN));  
    }
    
[/code]

**実行結果**

$v | printf("%.1f", $v) | round($v, 1) | round($v, 1, PHP_ROUND_HALF_EVEN)  
---|---|---|---  
4.25000000000000000000 | 4.2 | 4.3 | 4.2  
3.75000000000000000000 | 3.8 | 3.8 | 3.8  
3.25000000000000000000 | 3.2 | 3.3 | 3.2  
2.75000000000000000000 | 2.8 | 2.8 | 2.8  
2.25000000000000000000 | 2.2 | 2.3 | 2.2  
1.75000000000000000000 | 1.8 | 1.8 | 1.8  
1.25000000000000000000 | 1.2 | 1.3 | 1.2  
0.75000000000000011102 | 0.8 | 0.8 | 0.8  
0.25000000000000022204 | 0.3 | 0.3 | 0.2  
  
**Rounding mode**

[PHP: round](https://www.php.net/manual/ja/function.round.php)

> **mode**
>
> 次の定数のいずれかを使って、丸めのモードを指定します。
>
> **PHP_ROUND_HALF_UP(default)**
>
> 端数が5(半分)の場合、 num をゼロから離れる方向に丸めます。 1.5 は 2 に、そして -1.5 は -2 になります。
>
> **PHP_ROUND_HALF_EVEN**
>
> 端数が5(半分)の場合、 num をもっとも近い偶数に丸めます。 1.5 や 2.5 は 2 になります。

参考: [Round half to
even](https://en.wikipedia.org/wiki/Rounding#Round_half_to_even)

`0.25` の場合に `printf("%.1f", $v)` と `round($v, 1, PHP_ROUND_HALF_EVEN)`
の値が異なりますが、これは `round()` が小数点以下15桁までの値で判定しているためです。

結局、rounding mode の違いという事になります。

なお、`printf("%.1f", $v)` の処理は最終的に [zend_dtoa()](https://github.com/php/php-
src/blob/PHP-8.0.7/Zend/zend_strtod.c#L3714)
で行われていて、[冒頭部分のコメント](https://github.com/php/php-
src/blob/PHP-8.0.7/Zend/zend_strtod.c#L34)には以下の様な記載があります。

> /* strtod for IEEE-, VAX-, and IBM-arithmetic machines.  
>  * (Note that IEEE arithmetic is disabled by gcc's -ffast-math flag.)  
>  *  
>  * This strtod returns a nearest machine number to the input decimal  
>  * string (or sets errno to ERANGE). **With IEEE arithmetic, ties are  
>  * broken by the IEEE round-even rule**. Otherwise ties are broken by  
>  * biased rounding (add half and chop).


CC BY-SA 4.0
2021-05-26T22:23:12.437
76090
2021-05-26T22:58:15.403
2021-05-26T22:58:15.403
76054
answer