大きな数の表記(3桁区切り・万進・英語)

大きな整数を、3桁区切り・万進表記(万・億・兆・京)・英語(million・billion)の3通りで書き表す。

大きな整数2進小数の誤差

言語ごとの結果

正解表 16件のうち、正解と一致した件数です。

PHPPHP 8.4.26

よくある書き方13 / 16

直した書き方16 / 16

PythonPython 3.14.7

よくある書き方15 / 16

直した書き方16 / 16

GoGo 1.27.2

よくある書き方12 / 16

直した書き方16 / 16

JavaScriptNode 24.15.0

よくある書き方11 / 16

直した書き方16 / 16

なぜ割れたのか

よくある書き方が落ちたケースを、原因ごとにまとめた解説です(2本)。書いてある「落ちた言語」が今回の検証結果と一致することを確かめてから載せています。

大きな整数

大きな整数で、PHP は黙って上限に張りつき、JavaScript は桁が化け、Go は止まった

対象のケースc010c011c012c013c015 落ちた言語PHPGoJavaScript

入力の数字の文字列を、各言語の標準の方法で数に変えるところで、3通りに割れました。

言語使った変換10^21(10垓)を入れると
PHP(int) $value9,223,372,036,854,775,807 と表示(エラーなし)
JavaScriptNumber(value)1,000,000,000,000,000,000,000(たまたま正しい)
Gostrconv.ParseInt(value, 10, 64)エラーで止まる(value out of range)
Pythonint(value)1,000,000,000,000,000,000,000(正しい)

PHP: 64ビットの上限で、黙って頭打ちになる

PHP の整数は64ビットで、上限は 9,223,372,036,854,775,807(2^63 − 1)です。(int) でこれより大きい数字の文字列を変換すると、エラーも警告も出さずに上限の値になります。10^21 も 35桁の数も、同じ「922京3372兆368億5477万5807」と表示しました(c013・c015)。上限ちょうど + 1(c012)は、1少ない値になりました。

JavaScript: 2^53 を超えると、桁が化ける

JavaScript の Number は倍精度の小数なので、正確に表せる整数は 2^53(9,007,199,254,740,992)までです。2^53 + 1(c010)は末尾が …992 に変わり、35桁の数(c015)は万進表記が「1000溝8608兆5072億…」という、元の数とまったく違う値になりました。64ビットの上限(c011)では、3桁区切りが「…776,000」と下の桁が 0 で埋まりました。toLocaleString は、正確でない下の桁を 0 にして見せます。

Go: 止まるが、間違いは返さない

Go の strconv.ParseInt は、64ビットに入らない数をエラーにします。答えは返せませんが、間違った答えも返しません。この中では最も安全な失敗です。

どう直したか

桁数に上限のない整数で計算しました。PHP は GMP、JavaScript は BigInt、Go は math/big.Int、Python は int のままです。3桁区切りは、PHP の number_format や JavaScript の toLocaleString が大きな整数を正しく扱えないため、数字の文字列に自分で入れています。

2進小数の誤差

999,999,999,500 を英語で書くと「1000 billion」——割り算の小数で丸めが外れた

対象のケースc009 落ちた言語PythonGoJavaScript

999,999,999,500 は 999.9999995 billion です。小数第6位に四捨五入すると 1000 billion(元のツールと同じく、丸めで 1000 になっても単位は変えない)になります。Python・Go・JavaScript は 999.999999 billion を返しました。

なぜ外れたのか

999999999500 / 1000000000 を倍精度の小数で計算すると、999.9999995 ちょうどにはならず、わずかに小さい値になります。小数第7位の 5 が「5 より少し小さい」ので、四捨五入で切り捨てられました。

PHP だけは round() が小さな誤差を吸収して 1000 にしたため、この件は正解しました。ただし PHP も、大きな整数では別の理由で落ちています(大きな整数の解説を参照)。

どう直したか

割り算の小数を使わず、整数のまま「10^-6 の単位の商と余り」を求め、余りが割る数の半分以上なら繰り上げるようにしました。

正解表と、よくある書き方の答え

正解は実装より先に決めて固定しています。言語の列の ✓ は正解と一致、赤い値はその言語が実際に返した間違った答えです。

ケース入力正解 PHPPythonGoJavaScript
c001基本・1億2000万
value
120000000
plain
120,000,000
ja
1億2000万
en
120 million
✓✓✓✓
c0020 境界値
value
0
plain
0
ja
0
en
0
✓✓✓✓
c0031000未満 単位を付けない
value
999
plain
999
ja
999
en
999
✓✓✓✓
c004ちょうど1000 en は 1 thousand。ja は万に届かない
value
1000
plain
1,000
ja
1000
en
1 thousand
✓✓✓✓
c0051万2345
value
12345
plain
12,345
ja
1万2345
en
12.345 thousand
✓✓✓✓
c006途中の組が0 ja は「1億1」。0の組(0000万)は書かない
value
100000001
plain
100,000,001
ja
1億1
en
100.000001 million
✓✓✓✓
c0073兆4500億
value
3450000000000
plain
3,450,000,000,000
ja
3兆4500億
en
3.45 trillion
✓✓✓✓
c008en の丸めが .5 ちょうど 1.2345675 billion → 1.234568。小数で割ると 1.23456749… になりうる
value
1234567500
plain
1,234,567,500
ja
12億3456万7500
en
1.234568 billion
✓✓✓✓
c009en の丸めで 1000 になる 999.9999995 billion → 1000 billion(単位は変えない) なぜ? 解説を読む
value
999999999500
plain
999,999,999,500
ja
9999億9999万9500
en
1000 billion
✓en"999.999999 billion"en"999.999999 billion"en"999.999999 billion"
c0102^53 + 1 倍精度浮動小数点で正確に表せない最小の正の整数 なぜ? 解説を読む
value
9007199254740993
plain
9,007,199,254,740,993
ja
9007兆1992億5474万993
en
9.007199 quadrillion
✓✓✓plain"9,007,199,254,740,992"ja"9007兆1992億5474万992"
c01164ビット整数の上限 2^63 − 1 なぜ? 解説を読む
value
9223372036854775807
plain
9,223,372,036,854,775,807
ja
922京3372兆368億5477万5807
en
9.223372 quintillion
✓✓✓plain"9,223,372,036,854,776,000"ja"922京3372兆368億5477万5808"
c01264ビット整数の上限 + 1 64ビット整数に入らない なぜ? 解説を読む
value
9223372036854775808
plain
9,223,372,036,854,775,808
ja
922京3372兆368億5477万5808
en
9.223372 quintillion
plain"9,223,372,036,854,775,807"ja"922京3372兆368億5477万5807"✓エラーplain"9,223,372,036,854,776,000"
c01310^21(10垓) quintillion より上の単位は使わない → 1000 quintillion なぜ? 解説を読む
value
1000000000000000000000
plain
1,000,000,000,000,000,000,000
ja
10垓
en
1000 quintillion
plain"9,223,372,036,854,775,807"ja"922京3372兆368億5477万5807"en"9.223372 quintillion"✓エラー✓
c014負の数
value
-120000000
plain
-120,000,000
ja
-1億2000万
en
-120 million
✓✓✓✓
c01535桁 溝まで使う。en は丸めで 100000000000000000 quintillion なぜ? 解説を読む
value
99999999999999999999999999999999999
plain
99,999,999,999,999,999,999,999,999,999,999,999
ja
999溝9999穣9999秭9999垓9999京9999兆9999億9999万9999
en
100000000000000000 quintillion
plain"9,223,372,036,854,775,807"ja"922京3372兆368億5477万5807"en"9.223372 quintillion"✓エラーplain"100,000,000,000,000,000,000,000,000,000,000,000"ja"1000溝8608兆5072億5248万5552"
c016途中に0の組が続く ja は「100兆1」
value
100000000000001
plain
100,000,000,000,001
ja
100兆1
en
100 trillion
✓✓✓✓

コード

「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。

Numbers.php+35 −21よくある書き方(98aef44)→ 直した書き方

@@ 変更前 3行目〜 / 変更後 3行目〜 @@
33
44 /**
55 * 大きな数の表記(3桁区切り・万進・英語)。
6− * v1: よくある書き方((int) で読み、number_format で区切り、英語表記は割り算して round($x, 6))。
6+ * v2: GMP(任意精度の整数)で計算する。(int) は 64ビットの上限で黙って頭打ちになり、
7+ * number_format も int / float しか受け取れないので、区切りは数字の文字列に対して自分で入れる。
78 * @return array{plain:string, ja:string, en:string}
89 */
10+function commafy(string $digits): string
11+{
12+ return preg_replace('/\B(?=(\d{3})+(?!\d))/', ',', $digits);
13+}
14+
15+function enScale(GMP $a): string
16+{
17+ static $units = [18 => 'quintillion', 15 => 'quadrillion', 12 => 'trillion', 9 => 'billion', 6 => 'million', 3 => 'thousand'];
18+ if ($a < 1000) {
19+ return commafy(gmp_strval($a));
20+ }
21+ foreach ($units as $e => $word) {
22+ if ($a >= gmp_pow(10, $e)) {
23+ break;
24+ }
25+ }
26+ $d = gmp_pow(10, $e);
27+ [$q, $r] = gmp_div_qr($a * 1000000, $d);
28+ if ($r * 2 >= $d) {
29+ $q += 1; // 四捨五入(0.5 ちょうどは繰り上げ)
30+ }
31+ [$ip, $fp] = gmp_div_qr($q, 1000000);
32+ $frac = rtrim(str_pad(gmp_strval($fp), 6, '0', STR_PAD_LEFT), '0');
33+ return gmp_strval($ip) . ($frac !== '' ? '.' . $frac : '') . ' ' . $word;
34+}
35+
936 function convertNumber(string $value): array
1037 {
1138 $ja = ['', '万', '億', '兆', '京', '垓', '秭', '穣', '溝'];
12− $enUnits = [18 => 'quintillion', 15 => 'quadrillion', 12 => 'trillion', 9 => 'billion', 6 => 'million', 3 => 'thousand'];
13− $n = (int) $value;
14− $sign = $n < 0 ? '-' : '';
15− $a = abs($n);
39+ $n = gmp_init($value, 10);
40+ $sign = gmp_sign($n) < 0 ? '-' : '';
41+ $a = gmp_abs($n);
1642 $parts = [];
1743 for ($i = 0, $rest = $a; $rest > 0; $i++) {
18− $g = $rest % 10000;
19− $rest = intdiv($rest, 10000);
20− if ($g) {
21− array_unshift($parts, $g . $ja[$i]);
44+ [$rest, $g] = gmp_div_qr($rest, 10000);
45+ if ($g > 0) {
46+ array_unshift($parts, gmp_strval($g) . $ja[$i]);
2247 }
2348 }
24− if ($a < 1000) {
25− $en = number_format($a);
26− } else {
27− foreach ($enUnits as $e => $word) {
28− if ($a >= 10 ** $e) {
29− break;
30− }
31− }
32− $x = round($a / 10 ** $e, 6);
33− $en = rtrim(rtrim(number_format($x, 6, '.', ''), '0'), '.') . ' ' . $word;
34− }
35− return ['plain' => number_format($n), 'ja' => $sign . (implode('', $parts) ?: '0'), 'en' => $sign . $en];
49+ return ['plain' => $sign . commafy(gmp_strval($a)), 'ja' => $sign . (implode('', $parts) ?: '0'), 'en' => $sign . enScale($a)];
3650 }

仕様

入力

項目型説明
value文字列整数(- のみ前に付けてよい。区切りや小数は付けない)。絶対値は 10^36 未満

JSON の数値ではなく文字列で渡す。JSON の数値は読み込みの時点で桁が失われる言語があるため。

出力

項目型定義例(120000000)
plain文字列3桁ごとのカンマ区切り"120,000,000"
ja文字列万進表記。4桁ごとに 万・億・兆・京・垓・秭・穣・溝 を付け、0 の組は省く"1億2000万"
en文字列英語の short scale(thousand・million・billion・trillion・quadrillion・quintillion)。下の規則"120 million"
  • 負の数は先頭に - を付ける。0 はどれも "0"
  • 値は正確に保つ。2^53(9,007,199,254,740,992)や 64ビット整数の上限(9,223,372,036,854,775,807)を超えても、1桁も変えてはいけない

en の規則(元ツールと同じ)

  1. 整数部の桁数から単位を決める。10^3 以上なら thousand、10^6 以上なら million … 10^18 以上なら quintillion(それより上の単位は使わない)。10^3 未満は単位を付けず3桁区切りのまま
  2. 値を 10^(単位の指数) で割り、小数第6位に四捨五入する(0.0000005 ちょうどは繰り上げ)。末尾の 0 と小数点は省く
  3. 丸めで 1000 になっても単位は変えない(例: 999,999,999,500 → "1000 billion")

期待値の作り方

expect は oracle.py で、Python の整数(桁数の上限がない)で上の定義をそのまま計算して生成した(2026-10-10)。実装ではない。runner は使わない。