画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本
$ python3 formatnumber.py 1234567.891
1,234,567.89整数
$ python3 formatnumber.py 1000
1,000.00小さい数
$ python3 formatnumber.py 0.5
0.501.005
$ python3 formatnumber.py 1.005
1.012.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.893桁ちょうど
$ python3 formatnumber.py 999
999.0010言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
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
formatnumber.py3.14.7 で 11 / 11 件の出力が正解と一致
# 受け取った数を、小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
import sys
from decimal import ROUND_HALF_UP, Decimal
# float は 1.005 を少し小さい数として持つので、10進数のまま計算する Decimal に、文字列から直す
d = Decimal(sys.argv[1])
# ROUND_HALF_UP は「ちょうど半分は 0 から遠いほうへ」(日本の四捨五入)
d = d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
if d.is_zero():
d = abs(d) # -0.001 は -0.00 になるので、マイナスを取る
print(f"{d:,.2f}")
動かし方(ファイルを保存したフォルダで) ・ Python を入れる
$ python3 formatnumber.py 1234567.891
formatnumber.go1.27.2 で 11 / 11 件の出力が正解と一致
// 受け取った数を、小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
// float64 は 1.005 を少し小さい数として持つので、文字列のまま整数部と小数部に分けて計算する
package main
import (
"fmt"
"os"
"strings"
)
func main() {
s := os.Args[1]
negative := strings.HasPrefix(s, "-")
intPart, fracPart, _ := strings.Cut(strings.TrimPrefix(s, "-"), ".")
frac := fracPart + "000"
// 「1234567.891」なら "123456789"(0.01 が何個か)。int64 に入らない桁数でも扱えるよう、文字列のまま持つ
digits := []byte(intPart + frac[:2])
if frac[2] >= '5' {
// 小数第3位が 5 以上なら、0 から遠いほうへ繰り上げる。筆算と同じく、下の桁から 9 を 0 にして繰り上げる
i := len(digits) - 1
for i >= 0 && digits[i] == '9' {
digits[i] = '0'
i--
}
if i < 0 {
digits = append([]byte{'1'}, digits...)
} else {
digits[i]++
}
}
d := strings.TrimLeft(string(digits), "0")
for len(d) < 3 { // 0.0x のときは、先頭に 0 を補う
d = "0" + d
}
intDigits := d[:len(d)-2]
// 右から3桁ごとにカンマを入れる
var b strings.Builder
for i, c := range intDigits {
if i > 0 && (len(intDigits)-i)%3 == 0 {
b.WriteString(",")
}
b.WriteRune(c)
}
sign := ""
if negative && strings.Trim(d, "0") != "" { // 0.00 にマイナスは付けない
sign = "-"
}
fmt.Printf("%s%s.%s\n", sign, b.String(), d[len(d)-2:])
}
動かし方(ファイルを保存したフォルダで) ・ Go を入れる
$ go run formatnumber.go 1234567.891
formatnumber.js24.15.0 で 11 / 11 件の出力が正解と一致
// 受け取った数を、小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
// 普通の数(Number)は 1.005 を少し小さい数として持つので、文字列のまま整数部と小数部に分けて計算する
const s = process.argv[2];
const negative = s.startsWith("-");
const [intPart, fracPart = ""] = s.replace("-", "").split(".");
const frac = fracPart.padEnd(3, "0");
// 「1234567.891」なら 123456789(0.01 が何個か)。大きい数でも1の位まで正しく持てる BigInt を使う
let cents = BigInt(intPart + frac.slice(0, 2));
if (frac[2] >= "5") {
cents += 1n; // 小数第3位が 5 以上なら、0 から遠いほうへ繰り上げる
}
const digits = cents.toString().padStart(3, "0");
const intStr = digits.slice(0, -2).replace(/\B(?=(\d{3})+$)/g, ","); // 右から3桁ごとにカンマ
const sign = negative && cents !== 0n ? "-" : ""; // 0.00 にマイナスは付けない
console.log(`${sign}${intStr}.${digits.slice(-2)}`);
動かし方(ファイルを保存したフォルダで) ・ JavaScript を入れる
$ node formatnumber.js 1234567.891
FormatNumber.java21.0.12 で 11 / 11 件の出力が正解と一致
// 受け取った数を小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Locale;
public class FormatNumber {
public static void main(String[] args) {
// double は 1.005 をぴったり持てないので、10進数のまま持つ BigDecimal で四捨五入する(HALF_UP は 0 から遠いほうへ)
BigDecimal x = new BigDecimal(args[0]).setScale(2, RoundingMode.HALF_UP);
// カンマと小数点の記号が国の設定で変わらないように、Locale.ROOT を指定する
System.out.println(String.format(Locale.ROOT, "%,.2f", x));
}
}
動かし方(ファイルを保存したフォルダで) ・ Java を入れる
$ java FormatNumber.java 1234567.891
formatnumber.cs10.0.402 で 11 / 11 件の出力が正解と一致
// 受け取った数を小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
using System.Globalization;
// double は 1.005 をぴったり持てないので、10進数のまま持つ decimal を使う
decimal x = decimal.Parse(args[0], CultureInfo.InvariantCulture);
// Math.Round は何も指定しないと「偶数のほうへ」丸めるので、AwayFromZero(0 から遠いほうへ)を指定する
x = Math.Round(x, 2, MidpointRounding.AwayFromZero);
// カンマと小数点の記号が国の設定で変わらないように、InvariantCulture を指定する
Console.WriteLine(x.ToString("N2", CultureInfo.InvariantCulture));
動かし方(ファイルを保存したフォルダで) ・ C# を入れる
$ dotnet run formatnumber.cs -- 1234567.891
formatnumber.rs1.99.0 で 11 / 11 件の出力が正解と一致
// 受け取った数を小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
// 数字の列に1を足す("199" → "200"、"99" → "100")
fn add_one(digits: &str) -> String {
let mut out: Vec<char> = digits.chars().collect();
let mut i = out.len();
loop {
if i == 0 {
out.insert(0, '1');
break;
}
i -= 1;
if out[i] == '9' {
out[i] = '0';
} else {
out[i] = char::from_digit(out[i].to_digit(10).unwrap() + 1, 10).unwrap();
break;
}
}
out.into_iter().collect()
}
fn main() {
let args: Vec<String> = std::env::args().collect();
// f64 は 1.005 をぴったり持てないので、文字列のまま10進数の桁を見て四捨五入する
let (negative, s) = match args[1].strip_prefix('-') {
Some(rest) => (true, rest.to_string()),
None => (false, args[1].clone()),
};
let (int_part, frac_part) = s.split_once('.').unwrap_or((&s, ""));
let frac = format!("{:0<3}", frac_part); // 小数を3桁以上にそろえる("5" → "500")
// 整数部と小数2桁をつないだ数字の列(1.005 なら "100")。小数第3位が5以上なら1を足す(0 から遠いほうへ)
let mut digits = format!("{}{}", int_part, &frac[..2]);
if frac.as_bytes()[2] >= b'5' {
digits = add_one(&digits);
}
let (int_digits, frac2) = digits.split_at(digits.len() - 2);
let int_digits = int_digits.trim_start_matches('0');
let int_digits = if int_digits.is_empty() { "0" } else { int_digits };
// 整数部に3桁ごとのカンマを入れる
let mut with_commas = String::new();
for (i, c) in int_digits.chars().enumerate() {
if i > 0 && (int_digits.len() - i) % 3 == 0 {
with_commas.push(',');
}
with_commas.push(c);
}
// 四捨五入して 0 になったときは「-0.00」にしない
let sign = if negative && (int_digits != "0" || frac2 != "00") { "-" } else { "" };
println!("{}{}.{}", sign, with_commas, frac2);
}
動かし方(ファイルを保存したフォルダで) ・ Rust を入れる
$ rustc formatnumber.rs && ./formatnumber 1234567.891
formatnumber.rb4.0.7 で 11 / 11 件の出力が正解と一致
# 受け取った数を、小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
# to_f(小数)は 1.005 を少し小さい数として持つので、文字列のまま整数部と小数部に分けて計算する
s = ARGV[0]
negative = s.start_with?("-")
int_part, frac_part = s.delete_prefix("-").split(".")
frac = (frac_part || "").ljust(3, "0")
# 「1234567.891」なら 123456789(0.01 が何個か)。Ruby の整数には上限が無い
cents = Integer(int_part + frac[0, 2], 10)
cents += 1 if frac[2] >= "5" # 小数第3位が 5 以上なら、0 から遠いほうへ繰り上げる
digits = cents.to_s.rjust(3, "0")
int_str = digits[0..-3].gsub(/(\d)(?=(\d{3})+\z)/, '\1,') # 右から3桁ごとにカンマ
sign = negative && cents != 0 ? "-" : "" # 0.00 にマイナスは付けない
puts "#{sign}#{int_str}.#{digits[-2..]}"
動かし方(ファイルを保存したフォルダで) ・ Ruby を入れる
$ ruby formatnumber.rb 1234567.891
formatnumber.kt2.4.21 で 11 / 11 件の出力が正解と一致
// 受け取った数を小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
import java.math.BigDecimal
import java.math.RoundingMode
import java.util.Locale
fun main(args: Array<String>) {
// Double は 1.005 をぴったり持てないので、10進数のまま持つ BigDecimal で四捨五入する(HALF_UP は 0 から遠いほうへ)
val x = BigDecimal(args[0]).setScale(2, RoundingMode.HALF_UP)
// カンマと小数点の記号が国の設定で変わらないように、Locale.ROOT を指定する
println("%,.2f".format(Locale.ROOT, x))
}
動かし方(ファイルを保存したフォルダで) ・ Kotlin を入れる
$ kotlinc formatnumber.kt -include-runtime -d formatnumber.jar && java -jar formatnumber.jar 1234567.891
formatnumber.swift6.3.3 で 11 / 11 件の出力が正解と一致
// 受け取った数を小数第2位までに四捨五入して、3桁ごとにカンマを入れて出す
// 数字の列に1を足す("199" → "200"、"99" → "100")
func addOne(_ digits: String) -> String {
var out = Array(digits)
var i = out.count
while true {
if i == 0 {
out.insert("1", at: 0)
break
}
i -= 1
if out[i] == "9" {
out[i] = "0"
} else {
out[i] = Character(String(out[i].wholeNumberValue! + 1))
break
}
}
return String(out)
}
// Double は 1.005 をぴったり持てないので、文字列のまま10進数の桁を見て四捨五入する
var s = CommandLine.arguments[1]
let negative = s.hasPrefix("-")
if negative {
s.removeFirst()
}
let parts = s.split(separator: ".", omittingEmptySubsequences: false)
let intPart = String(parts[0])
var frac = parts.count > 1 ? String(parts[1]) : ""
while frac.count < 3 {
frac += "0" // 小数を3桁以上にそろえる("5" → "500")
}
// 整数部と小数2桁をつないだ数字の列(1.005 なら "100")。小数第3位が5以上なら1を足す(0 から遠いほうへ)
var digits = intPart + frac.prefix(2)
if Array(frac)[2] >= "5" {
digits = addOne(digits)
}
var intDigits = String(digits.dropLast(2).drop(while: { $0 == "0" }))
if intDigits.isEmpty {
intDigits = "0"
}
let frac2 = String(digits.suffix(2))
// 整数部に3桁ごとのカンマを入れる
var withCommas = ""
for (i, c) in intDigits.enumerated() {
if i > 0 && (intDigits.count - i) % 3 == 0 {
withCommas += ","
}
withCommas.append(c)
}
// 四捨五入して 0 になったときは「-0.00」にしない
let sign = negative && (intDigits != "0" || frac2 != "00") ? "-" : ""
print("\(sign)\(withCommas).\(frac2)")
動かし方(ファイルを保存したフォルダで) ・ Swift を入れる
$ swift formatnumber.swift 1234567.891
言語でつまずきやすいところ
初心者がよく書く形(各言語で最初に習う書き方をそのまま使ったもの)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | 初心者がよく書く形の答え(10言語) |
|---|---|
基本1234567.891 |
|
整数1000 | ✓10言語すべて正解 |
小さい数0.5 | ✓10言語すべて正解 |
1.0051.005 |
|
2.6752.675 |
|
ちょうど半分0.125 |
|
マイナスのちょうど半分-0.125 |
|
繰り上がって桁が増える999999.995 |
|
マイナスが0になる-0.001 |
|
大きい数12345678901234567.89 |
|
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.005 | 0.125 | 999999.995 | -0.001 | 12345678901234567.89 |
|---|---|---|---|---|---|
| 正解 | 1.01 | 0.13 | 1,000,000.00 | 0.00 | 12,345,678,901,234,567.89 |
Python f"{x:,.2f}"・Go %.2f・Rust {:.2}・C# ToString("N2") | 1.00 | 0.12 | 999,999.99 | -0.00 | …568.00 |
Ruby format("%.2f") | 1.00 | 0.12 | 1,000,000.00 | -0.00 | …568.00 |
Java・Kotlin String.format("%,.2f")・JavaScript toLocaleString | 1.01 | 0.13 | 1,000,000.00 | -0.00 | …568.00 |
PHP number_format | 1.01 | 0.13 | 1,000,000.00 | 0.00 | …568.00 |
Swift NumberFormatter(Linux) | 1.005 | 0.125 | 999,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,234,567.9」)に変えてみましょう
- 先頭に「¥」を付けて、マイナスは「−¥1,234」のように出してみましょう