CODE Polyglot / 入門 / 第13回

数を見やすく — 3桁区切りと小数第2位の四捨五入

受け取った数を、小数第2位までに四捨五入して、3桁ごとにカンマを入れて出します。「1.005」が「1.00」になる、「-0.001」が「-0.00」になる、18桁の数の終わりが変わる、といったずれが、言語と関数によって起きます。

画面にこう出れば正解

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

基本

$ python3 formatnumber.py 1234567.891
1,234,567.89

整数

$ python3 formatnumber.py 1000
1,000.00

小さい数

$ python3 formatnumber.py 0.5
0.50

1.005

$ python3 formatnumber.py 1.005
1.01

2.675

$ python3 formatnumber.py 2.675
2.68

ちょうど半分

$ python3 formatnumber.py 0.125
0.13

マイナスのちょうど半分

$ python3 formatnumber.py -0.125
-0.13

繰り上がって桁が増える

$ python3 formatnumber.py 999999.995
1,000,000.00

マイナスが0になる

$ python3 formatnumber.py -0.001
0.00

大きい数

$ python3 formatnumber.py 12345678901234567.89
12,345,678,901,234,567.89

3桁ちょうど

$ python3 formatnumber.py 999
999.00

10言語のプログラム

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

formatnumber.php8.4.26 で 11 / 11 件の出力が正解と一致

<?php
// 受け取った数を、小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
// (float) では大きい数の下の桁が失われるので、文字列のまま整数部と小数部に分けて計算する
$s = $argv[1];
$negative = str_starts_with($s, '-');
$parts = explode('.', ltrim($s, '-'));
$frac = str_pad($parts[1] ?? '', 3, '0');

// 「1234567.891」なら "123456789"(0.01 が何個か)。PHP の整数に入らない桁数でも扱えるよう、文字列のまま持つ
$digits = $parts[0] . substr($frac, 0, 2);
if ($frac[2] >= '5') {
    // 小数第3位が 5 以上なら、0 から遠いほうへ繰り上げる。筆算と同じく、下の桁から 9 を 0 にして繰り上げる
    $i = strlen($digits) - 1;
    while ($i >= 0 && $digits[$i] === '9') {
        $digits[$i] = '0';
        $i--;
    }
    if ($i < 0) {
        $digits = '1' . $digits;
    } else {
        $digits[$i] = (string) ((int) $digits[$i] + 1);
    }
}

$digits = str_pad(ltrim($digits, '0'), 3, '0', STR_PAD_LEFT); // 先頭の 0 を取り、0.0x のときは 0 を補う
$intStr = preg_replace('/(\d)(?=(\d{3})+\z)/', '$1,', substr($digits, 0, -2)); // 右から3桁ごとにカンマ
$sign = $negative && trim($digits, '0') !== '' ? '-' : ''; // 0.00 にマイナスは付けない
echo $sign . $intStr . '.' . substr($digits, -2) . "\n";

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

$ php formatnumber.php 1234567.891

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

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

入力初心者がよく書く形の答え(10言語)
基本1234567.891
  • Swift1,234,567.891
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin✓ 正解
整数1000

✓10言語すべて正解

小さい数0.5

✓10言語すべて正解

1.0051.005
  • Python・Go・C#・Rust・Ruby1.00
  • Swift1.005
  • PHP・JavaScript・Java・Kotlin✓ 正解
2.6752.675
  • Python・Go・C#・Rust2.67
  • Swift2.675
  • PHP・JavaScript・Java・Ruby・Kotlin✓ 正解
ちょうど半分0.125
  • Python・Go・C#・Rust・Ruby0.12
  • Swift0.125
  • PHP・JavaScript・Java・Kotlin✓ 正解
マイナスのちょうど半分-0.125
  • Python・Go・C#・Rust・Ruby-0.12
  • Swift-0.125
  • PHP・JavaScript・Java・Kotlin✓ 正解
繰り上がって桁が増える999999.995
  • Python・Go・C#・Rust999,999.99
  • Swift999,999.995
  • PHP・JavaScript・Java・Ruby・Kotlin✓ 正解
マイナスが0になる-0.001
  • Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin-0.00
  • Swift-0.001
  • PHP✓ 正解
大きい数12345678901234567.89
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift12,345,678,901,234,568.00
3桁ちょうど999

✓10言語すべて正解

この回でやること

  • 小数第2位までに四捨五入する
  • 3桁ごとにカンマを入れる
  • 小数の計算で起きるずれを知る

この回の決まり

  • 入力は「1234567.891」のような10進の数(形は正しいものとする)
  • 小数第3位を四捨五入する。ちょうど半分(0.125 など)は、0 から遠いほうへ(0.13・−0.13)
  • 小数は必ず2桁出す(1000 は「1,000.00」)。四捨五入して 0 になったときは「0.00」(「-0.00」にしない)

解説

小数は、ぴったりには入っていない

多くの言語の普通の小数(float・double)は、2進数で数を持っています。「1.005」は2進数ではぴったり表せず、実際には 1.00499999999999989… という少し小さい数として入っています。これを小数第2位に四捨五入すると、「1.00」になります。

10進数のまま計算する型(Python の Decimal・Java の BigDecimal・C# の decimal など)を使うか、文字列のまま桁を数えて四捨五入します。

「ちょうど半分」の扱い

0.125 は2進数でぴったり表せる数です。それでも「0.12」になることがあります。ちょうど半分のときに「偶数のほうへ丸める」(銀行家の丸め)関数があるためです。日本の「四捨五入」は、0 から遠いほうへ丸めます。

言語ごとの結果

各言語で、3桁区切りと小数2桁を出すいちばん普通の書き方(小数は float・double に直してから)を使った結果です。

書き方1.0050.125999999.995-0.00112345678901234567.89
正解1.010.131,000,000.000.0012,345,678,901,234,567.89
Python f"{x:,.2f}"・Go %.2f・Rust {:.2}・C# ToString("N2")1.000.12999,999.99-0.00…568.00
Ruby format("%.2f")1.000.121,000,000.00-0.00…568.00
Java・Kotlin String.format("%,.2f")・JavaScript toLocaleString1.010.131,000,000.00-0.00…568.00
PHP number_format1.010.131,000,000.000.00…568.00
Swift NumberFormatter(Linux)1.0050.125999,999.995-0.001…568.00
  • Java・Kotlin・JavaScript・PHP は、1.005 も 0.125 も正しく丸めました。中身の2進数の値ではなく、「1.005」という短い10進の書き方を元に丸めているためと考えられます
  • それでも、17桁を超える数の終わり(…567.89 が …568.00 になる)は、10言語すべてで外れました。double には、10進で16〜17桁ほどしか入らないためです。-0.00 も PHP 以外で出ました
  • Swift の NumberFormatter は、Mac では小数2桁に丸めましたが、Linux では桁数の指定が効かず、入力をそのまま出しました

どの「普通の書き方」も、11件すべては通りませんでした。正しいプログラムは、全言語とも小数(double)を使わずに、10進数の型か、文字列のまま桁を数えて四捨五入しています。

練習

  1. 小数第1位まで(例「1,234,567.9」)に変えてみましょう
  2. 先頭に「¥」を付けて、マイナスは「−¥1,234」のように出してみましょう