画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は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.10.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.331つだけ
$ python3 average.py 3.33
合計: 3.33
平均: 3.3310言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
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
average.py3.14.7 で 6 / 6 件の出力が正解と一致
# 受け取った数の合計と、平均(小数第2位で四捨五入)を出す
import sys
from decimal import ROUND_HALF_UP, Decimal
# float(2進数の小数)で足すと 0.1 を10回で 0.9999999999999999 になる。Decimal は十進のまま正確に足せる
nums = [Decimal(a) for a in sys.argv[1:]]
total = sum(nums)
# round は「偶数への丸め」をするので、quantize で ROUND_HALF_UP(0.5 は切り上げ)を指定する
avg = (total / len(nums)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def show(d):
return f"{d.normalize():f}" # 末尾の 0 を消して出す(1.0 → 1、0.10 → 0.1)
print(f"合計: {show(total)}")
print(f"平均: {show(avg)}")
動かし方(ファイルを保存したフォルダで) ・ Python を入れる
$ python3 average.py 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.go1.27.2 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と、平均(小数第2位で四捨五入)を出す
package main
import (
"fmt"
"math/big"
"os"
"strings"
)
// 末尾の 0 と小数点を消して出す(1.00 → 1、0.10 → 0.1)
func show(s string) string {
if strings.Contains(s, ".") {
s = strings.TrimSuffix(strings.TrimRight(s, "0"), ".")
}
return s
}
func main() {
nums := os.Args[1:]
// float64(2進数の小数)で足すと 0.1 を10回で 0.9999999999999999 になる。
// big.Rat は「分数」で持つので、"0.1" を 1/10 のまま正確に足せる
total := new(big.Rat)
for _, a := range nums {
n, _ := new(big.Rat).SetString(a)
total.Add(total, n)
}
avg := new(big.Rat).Quo(total, big.NewRat(int64(len(nums)), 1))
fmt.Println("合計:", show(total.FloatString(20)))
fmt.Println("平均:", show(avg.FloatString(2))) // FloatString(2) は小数第2位で四捨五入する
}
動かし方(ファイルを保存したフォルダで) ・ Go を入れる
$ go run average.go 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.js24.15.0 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と、平均(小数第2位で四捨五入)を出す
// Number(2進数の小数)で足すと 0.1 を10回で 0.9999999999999999 になる。
// そこで小数点を外して整数(BigInt)にしてから足し、出すときに小数点を戻す
const args = process.argv.slice(2);
const places = Math.max(...args.map((a) => (a.split(".")[1] ?? "").length)); // 小数点より下の、いちばん多い桁数
const scale = 10n ** BigInt(places);
// "1.5" → 15n のように、小数点を外して整数にする(桁数は places にそろえる)
const toInt = (a) => {
const [i, f = ""] = a.split(".");
return BigInt(i + f.padEnd(places, "0"));
};
// 整数 n を「小数第 digits 位まで」の文字列に戻す(末尾の 0 は消す)
const show = (n, digits) => {
const sign = n < 0n ? "-" : "";
const s = (n < 0n ? -n : n).toString().padStart(digits + 1, "0");
const intPart = s.slice(0, s.length - digits);
const fracPart = s.slice(s.length - digits).replace(/0+$/, "");
return sign + intPart + (fracPart ? "." + fracPart : "");
};
let total = 0n;
for (const a of args) {
total += toInt(a);
}
// 平均を 1/100 の単位で求めて四捨五入する(0.5 は切り上げ)。BigInt の / は切り捨てなので、半分を足してから割る
const den = BigInt(args.length) * scale;
const abs = total < 0n ? -total : total;
let avg = (abs * 100n * 2n + den) / (den * 2n);
if (total < 0n) avg = -avg;
console.log(`合計: ${show(total, places)}`);
console.log(`平均: ${show(avg, 2)}`);
動かし方(ファイルを保存したフォルダで) ・ JavaScript を入れる
$ node average.js 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
Average.java21.0.12 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と平均(小数第2位で四捨五入)を出す
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Average {
public static void main(String[] args) {
// double は 0.1 を正確に表せずずれるので、十進の小数をそのまま扱える BigDecimal で足す
BigDecimal sum = BigDecimal.ZERO;
for (String a : args) {
sum = sum.add(new BigDecimal(a));
}
// HALF_UP は 0.5 を切り上げる四捨五入。stripTrailingZeros で 1.0 → 1、12.50 → 12.5 にする
BigDecimal avg = sum.divide(BigDecimal.valueOf(args.length), 2, RoundingMode.HALF_UP);
System.out.println("合計: " + sum.stripTrailingZeros().toPlainString());
System.out.println("平均: " + avg.stripTrailingZeros().toPlainString());
}
}
動かし方(ファイルを保存したフォルダで) ・ Java を入れる
$ java Average.java 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.cs10.0.402 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と平均(小数第2位で四捨五入)を出す
using System.Globalization;
// double は 0.1 を正確に表せずずれるので、十進の小数をそのまま扱える decimal で足す
decimal sum = 0;
foreach (string a in args)
{
sum += decimal.Parse(a, CultureInfo.InvariantCulture);
}
// Math.Round の既定は偶数への丸めなので、AwayFromZero で 0.5 を切り上げる
decimal avg = Math.Round(sum / args.Length, 2, MidpointRounding.AwayFromZero);
// "0.##" の形で、1.0 → 1、12.50 → 12.5 のように余分な 0 を付けずに出す
Console.WriteLine($"合計: {sum.ToString("0.############################", CultureInfo.InvariantCulture)}");
Console.WriteLine($"平均: {avg.ToString("0.##", CultureInfo.InvariantCulture)}");
動かし方(ファイルを保存したフォルダで) ・ C# を入れる
$ dotnet run average.cs -- 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.rs1.99.0 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と平均(小数第2位で四捨五入)を出す
// f64 は 0.1 を正確に表せずずれる。Rust の標準には十進の小数の型が無いので、100万分の1を1とする整数に直して足す
const SCALE: i64 = 1_000_000; // 小数第6位まで扱う
// "19.99" → 19990000 のように、整数に直す
fn to_units(s: &str) -> i64 {
let minus = s.starts_with('-');
let s = s.trim_start_matches('-');
let (int_part, frac_part) = s.split_once('.').unwrap_or((s, ""));
let frac = format!("{:0<6}", frac_part); // 右を 0 で埋めて6桁にする
let v = int_part.parse::<i64>().unwrap() * SCALE + frac[..6].parse::<i64>().unwrap();
if minus { -v } else { v }
}
// 19990000 → "19.99" のように、小数に戻す(余分な 0 は付けない)
fn show(units: i64) -> String {
let sign = if units < 0 { "-" } else { "" };
let a = units.abs();
let frac = format!("{:06}", a % SCALE);
let frac = frac.trim_end_matches('0');
if frac.is_empty() {
format!("{sign}{}", a / SCALE)
} else {
format!("{sign}{}.{frac}", a / SCALE)
}
}
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let sum: i64 = args.iter().map(|a| to_units(a)).sum();
// 平均を 0.01 の単位で四捨五入する(0.5 は 0 から遠い方へ)。整数の割り算は切り捨てなので、半分を足してから割る
let n = args.len() as i64;
let step = SCALE / 100;
let hundredths = (sum.abs() * 2 / (n * step) + 1) / 2;
let avg = if sum < 0 { -hundredths } else { hundredths } * step;
println!("合計: {}", show(sum));
println!("平均: {}", show(avg));
}
動かし方(ファイルを保存したフォルダで) ・ Rust を入れる
$ rustc average.rs && ./average 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.rb4.0.7 で 6 / 6 件の出力が正解と一致
# 受け取った数の合計と、平均(小数第2位で四捨五入)を出す
require "bigdecimal"
# to_f(2進数の小数)で足すと 0.1 を10回で 0.9999999999999999 になる。BigDecimal は十進のまま正確に足せる
nums = ARGV.map { |a| BigDecimal(a) }
total = nums.sum(BigDecimal("0"))
avg = (total / nums.length).round(2, BigDecimal::ROUND_HALF_UP)
# to_s("F") は 1 を "1.0" と出すので、最後の ".0" を消す
def show(d)
d.to_s("F").delete_suffix(".0")
end
puts "合計: #{show(total)}"
puts "平均: #{show(avg)}"
動かし方(ファイルを保存したフォルダで) ・ Ruby を入れる
$ ruby average.rb 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.kt2.4.21 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と平均(小数第2位で四捨五入)を出す
import java.math.BigDecimal
import java.math.RoundingMode
fun main(args: Array<String>) {
// Double は 0.1 を正確に表せずずれるので、十進の小数をそのまま扱える BigDecimal で足す
var sum = BigDecimal.ZERO
for (a in args) {
sum += BigDecimal(a)
}
// HALF_UP は 0.5 を切り上げる四捨五入。stripTrailingZeros で 1.0 → 1、12.50 → 12.5 にする
val avg = sum.divide(BigDecimal(args.size), 2, RoundingMode.HALF_UP)
println("合計: ${sum.stripTrailingZeros().toPlainString()}")
println("平均: ${avg.stripTrailingZeros().toPlainString()}")
}
動かし方(ファイルを保存したフォルダで) ・ Kotlin を入れる
$ kotlinc average.kt -include-runtime -d average.jar && java -jar average.jar 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
average.swift6.3.3 で 6 / 6 件の出力が正解と一致
// 受け取った数の合計と平均(小数第2位で四捨五入)を出す
import Foundation
let args = CommandLine.arguments.dropFirst()
// Double は 0.1 を正確に表せずずれるので、十進の小数をそのまま扱える Decimal で足す
var sum = Decimal(0)
for a in args {
sum += Decimal(string: a)!
}
// NSDecimalRound の .plain は、0.5 を 0 から遠い方へ丸める四捨五入
var quotient = sum / Decimal(args.count)
var avg = Decimal()
NSDecimalRound(&avg, "ient, 2, .plain)
print("合計: \(sum)")
print("平均: \(avg)")
動かし方(ファイルを保存したフォルダで) ・ Swift を入れる
$ swift average.swift 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 |
|
0.1 と 0.20.1 0.2 |
|
割り切れない平均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位までにしてみましょう