CODE Polyglot / 入門 / 第6回

合計と平均 — 0.1 を10回足しても 1 にならない

受け取った数の合計と平均を出します。平均は小数第2位まで(四捨五入)。0.1 を10回足すと、10言語のうち9言語が 0.9999999999999999 と表示しました。

画面にこう出れば正解

どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。

0.1 を10回

$ python3 average.py 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
合計: 1
平均: 0.1

0.1 と 0.2

$ python3 average.py 0.1 0.2
合計: 0.3
平均: 0.15

割り切れない平均

$ python3 average.py 1 2 2
合計: 5
平均: 1.67

お金の計算

$ python3 average.py 19.99 5.01
合計: 25
平均: 12.5

負の数を含む

$ python3 average.py -1.5 0.5 2
合計: 1
平均: 0.33

1つだけ

$ python3 average.py 3.33
合計: 3.33
平均: 3.33

10言語のプログラム

どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。

average.php8.4.26 で 6 / 6 件の出力が正解と一致

<?php
// 受け取った数の合計と、平均(小数第2位で四捨五入)を出す
$nums = array_slice($argv, 1);

// float(2進数の小数)で足すとずれる。bcmath の関数は、数を十進の文字列のまま正確に計算する
// (最後の 10 は「小数第10位まで計算する」という意味)
$total = '0';
foreach ($nums as $n) {
    $total = bcadd($total, $n, 10);
}
$avg = bcround(bcdiv($total, (string) count($nums), 10), 2); // bcround は 0.5 を切り上げる四捨五入

// 末尾の 0 と小数点を消して出す(1.0000000000 → 1、0.10 → 0.1)
function show(string $s): string
{
    return str_contains($s, '.') ? rtrim(rtrim($s, '0'), '.') : $s;
}

echo "合計: " . show($total) . "\n";
echo "平均: " . show($avg) . "\n";

動かし方(ファイルを保存したフォルダで) ・ PHP を入れる

$ php average.php 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1

言語でつまずきやすいところ

初心者がよく書く形(各言語で最初に習う書き方をそのまま使ったもの)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。

入力初心者がよく書く形の答え(10言語)
0.1 を10回0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
  • Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift合計: 0.9999999999999999
  • PHP✓ 正解
0.1 と 0.20.1 0.2
  • Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift合計: 0.30000000000000004
  • PHP✓ 正解
割り切れない平均1 2 2

✓10言語すべて正解

お金の計算19.99 5.01

✓10言語すべて正解

負の数を含む-1.5 0.5 2

✓10言語すべて正解

1つだけ3.33

✓10言語すべて正解

この回でやること

  • 受け取った値を、小数の数に変える
  • 繰り返しで合計を求め、個数で割って平均を求める
  • 小数を、決めた桁で四捨五入して表示する

解説

小数は、コンピュータの中で少しずれる

0.1 は、コンピュータの中の2進数では正確に表せません。ほんの少しずれた値として足されるので、10回足すと 1 ではなく 0.9999999999999999 になります。

0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1
→ 0.9999999999999999

PHP の echo は、小数を有効数字14桁に丸めて表示するので「1」と出ます。ただし、中の値は同じようにずれています(0.1 * 10 == 1 は成り立っても、10回足した値と 1 を == で比べると false です)。

正確に足すには

  • お金のように桁が決まっている数は、整数(円・銭)に直して足す
  • 十進の小数をそのまま扱える型を使う(Python の Decimal、Java・Kotlin の BigDecimal、C# の decimal、Ruby の BigDecimal など)

この回の正しいプログラムは、どの言語も「ずれない方法」で足しています。

四捨五入にも注意

この回の正解の例では起きていませんが、平均を小数第2位で四捨五入するとき、言語の round が「偶数への丸め」をすることがあります(Python の round(0.125, 2) は 0.12、C# の Math.Round(0.125, 2) も 0.12)。

練習

  1. 最大値と最小値も出してみましょう
  2. 平均を小数第1位までにしてみましょう