parseIntが基数2で12を1と読む理由 進数変換の打ち切りと負数

JavaScript で parseInt("12", 2) を実行すると 1 が返る。12 は 2 進数として正しくないのに、エラーにも NaN にもならない。入力チェックのつもりで parseInt を使っていると、壊れた値がそのまま通る。

進数変換は短いコードで書けるので、細かい挙動が見落とされやすい。この記事では、parseInt、Number、BigInt、Python の int が同じ文字列をどう読むかを並べて測り、負数を 16 進にするときの落とし穴も確かめる。最後に、不正な入力を必ず弾く変換関数を示す。

検証環境は Windows 11、Node.js v24.11.1、Python 3.11.9 である。

parseInt は読めるところまでで止まる

MDN の説明どおり、parseInt は指定した基数の数字として読めない文字に当たると、その文字以降を無視して、そこまでの値を返す。先頭の文字から読めないときだけ NaN になる。

式 結果 止まった場所
parseInt("12", 2) 1 2 は 2 進の数字ではない
parseInt("102", 2) 2 10 まで読んだ
parseInt("2", 2) NaN 1 文字目で止まった
parseInt("FG", 16) 15 G で止まった
parseInt("1e3", 10) 1 e で止まった
parseInt("1_000", 10) 1 _ で止まった
parseInt("0x1f", 10) 0 x で止まった
parseInt("0b101", 2) 0 b で止まった
parseInt("ff.8", 16) 255 . で止まった

NaN だけを見るチェックは、1 文字目が正しければ素通りする。parseInt("FG", 16) の結果は 15 で、isNaN では検出できない。

接頭辞の扱いも非対称である。0x は基数 16 または省略のときに読み飛ばされるが、0b と 0o は特別扱いされない。基数を省略した parseInt("0b101") は 0、parseInt("0o17") も 0 だった。

文字列でない値と map

parseInt は、受け取った値を先に文字列へ変換する。そのため数値を渡すと、指数表記の文字列を読むことがある。

1
2
3
parseInt(0.0000005);  // 5(文字列にすると "5e-7")
parseInt(1e21); // 1(文字列にすると "1e+21")
parseInt(null, 36); // 1112745("null" を 36 進として読む)

Array.prototype.map にそのまま渡すと、配列の添字が基数になる。

1
2
["1", "2", "3"].map(parseInt);          // [1, NaN, NaN]
["10", "10", "10", "10"].map(parseInt); // [10, NaN, 2, 3]

2 番目の結果は、基数 0(推定で 10)、基数 1(範囲外で NaN)、基数 2、基数 3 で "10" を読んだものである。

同じ文字列を 4 通りで読む

JavaScript の 3 つと Python では、受け付ける文字列が違う。Python の int(s, 0) は、接頭辞から基数を決めるモードである。

入力 parseInt(s) Number(s) BigInt(s) int(s, 0)
"0x1f" 31 31 31n 31
"0b101" 0 5 5n 5
"0o17" 0 15 15n 15
"017" 17 17 17n ValueError
"-0x1f" -31 NaN SyntaxError -31
"1e3" 1 1000 SyntaxError ValueError
"" NaN 0 0n ValueError
" 12 " 12 12 12n 12
"1_000" 1 NaN SyntaxError 1000
"12"(全角) NaN NaN SyntaxError 12

気をつけたい点は 3 つある。

  • Number("") と BigInt("") は、空文字列をエラーにせず 0 にする。未入力の欄が 0 として処理される。
  • Number と BigInt は -0x1f のような符号付きの接頭辞を受け付けない。parseInt と int(s, 0) は受け付ける。
  • Python は全角数字も数字として読む。ドキュメントにも、0〜9 は任意の Unicode 10 進数字で表せると書かれている。int('12', 8) は 10 になり、アラビア文字の int('٣') は 3 になる。

Python の int は、途中で止まらない。int('12', 2) は ValueError になる。JavaScript から移植するときは、ここで動作が変わる。

BigInt には基数の引数がない

BigInt には parseInt のような基数の引数がない。2、8、16 進なら BigInt("0x" + s) と書けるが、前後の空白は受け付ける。

1
2
3
4
BigInt("0x" + "1f ");  // 31n(末尾の空白が通る)
BigInt("0x" + "1f\n"); // 31n(末尾の改行も通る)
BigInt("0x" + "-1"); // SyntaxError
BigInt("0x" + ""); // SyntaxError

負数は - を外してから変換する必要がある。3 進や 36 進は、この方法では読めない。

2^53 を超えると別の数になる

parseInt の戻り値は Number なので、Number.MAX_SAFE_INTEGER(9007199254740991)を超える値は丸められる。

1
2
3
parseInt("9007199254740993", 10);    // 9007199254740992
parseInt("ffffffffffffffff", 16); // 18446744073709552000
parseInt("ffffffffffffffff", 16) === 2 ** 64; // true

64 ビットの最大値を読んだはずが、1 大きい 2^64 になっている。JSON の大きな整数で起きる丸めと同じ原因で、JSON 整形で大きな整数が変わる話 や Snowflake ID のデコード でも扱った。大きな ID を扱うなら、文字列のまま BigInt にする。

負数の toString は 2 の補数にならない

MDN にあるとおり、Number.prototype.toString は基数 2 でも符号を保ち、2 の補数を返さない。

1
2
3
(-1).toString(2);    // "-1"
(-255).toString(16); // "-ff"
(-255).toString(16).padStart(6, "0"); // "000-ff"

C やバイナリファイルの世界で -1 は ff や ffffffff だが、JavaScript の toString からは出てこない。padStart で桁をそろえると、000-ff という数値として読めない文字列になる。

2 の補数がほしいなら、ビット幅を決めて変換する。32 ビットまでなら符号なし右シフト、それ以上なら BigInt.asUintN が使える。

1
2
3
4
5
(-1 >>> 0).toString(2);    // "11111111111111111111111111111111"
(-255 >>> 0).toString(16); // "ffffff01"
BigInt.asUintN(16, -255n).toString(16); // "ff01"
BigInt.asUintN(64, -1n).toString(16); // "ffffffffffffffff"
BigInt.asIntN(8, 0xffn); // -1n

>>> 0 は 32 ビットに切り詰めるので、2 ** 32 >>> 0 は 0 になる。サブネットマスクの計算でこの切り詰めに引っかかる話は シフト演算の記事 に書いた。

BigInt.asUintN も、指定した幅に入らない値をエラーにしない。下位のビットだけを残す。

1
2
3
BigInt.asUintN(8, -255n); // 1n
BigInt.asUintN(8, 256n); // 0n
BigInt.asIntN(8, 200n); // -56n

-255 は 8 ビットで表せないが、1 が返る。範囲の確認は自分で書く必要がある。

Python も bin(-5) は '-0b101'、hex(-255) は '-0xff' で、符号が付く。2 の補数はマスクで作る。to_bytes は範囲外なら OverflowError を出すので、幅の確認も兼ねられる。

1
2
3
f'{-255 & 0xffff:04x}'                     # 'ff01'
(-1).to_bytes(1, 'big', signed=True).hex() # 'ff'
(300).to_bytes(1, 'big') # OverflowError

Python には桁数の上限がある

Python の整数に上限はないが、文字列との変換には上限がある。Python 3.11.9 の既定は 4300 桁で、超えると ValueError になる。

1
2
int('9' * 5000)        # ValueError: Exceeds the limit (4300 digits) ...
int('f' * 5000, 16) # 通る

ドキュメントによると、制限の対象は計算量が線形でない変換である。基数が 2、4、8、16、32 の int(s, base) や、hex()、oct()、bin() は対象外になっている。実際に 16 進 5000 桁と 2 進 5000 桁の読み込みは通り、bin(10**5000) も動いた。一方、同じ値を str() で 10 進にすると ValueError になる。

実際に試す

前提: Node.js(確認は v24.11.1)。外部パッケージは不要である。文字列全体が指定した基数の整数のときだけ BigInt を返し、それ以外は例外にする。2 の補数への変換では範囲も確認する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// radix-strict.mjs(Node.js v24.11.1 で確認)
// 使い方: node radix-strict.mjs
const DIGITS = '0123456789abcdefghijklmnopqrstuvwxyz';

// 文字列全体が base 進の整数であるときだけ BigInt を返す。それ以外は例外
export function parseStrict(input, base) {
if (!Number.isInteger(base) || base < 2 || base > 36) {
throw new RangeError(`base は 2〜36: ${base}`);
}
const m = /^([+-]?)([0-9A-Za-z]+)$/.exec(input); // 先に ASCII だけに絞る
if (!m) throw new SyntaxError(`整数の形ではない: ${JSON.stringify(input)}`);
let value = 0n;
const b = BigInt(base);
for (const ch of m[2].toLowerCase()) {
const d = DIGITS.indexOf(ch);
if (d >= base) throw new SyntaxError(`${base} 進で使えない桁: ${ch}`);
value = value * b + BigInt(d);
}
return m[1] === '-' ? -value : value;
}

// 符号付き整数を bits ビットの 2 の補数(非負の BigInt)にする。入らない値は例外
export function toTwos(value, bits) {
const b = BigInt(bits);
const min = -(1n << (b - 1n));
const max = (1n << b) - 1n; // 符号なしとして入る上限まで許す
if (value < min || value > max) throw new RangeError(`${value} は ${bits} ビットに入らない`);
return BigInt.asUintN(bits, value);
}

// 2 の補数のビット列を符号付きに戻す
export function fromTwos(unsigned, bits) {
return BigInt.asIntN(bits, unsigned);
}

export function hexOf(unsigned, bits) {
return unsigned.toString(16).padStart(Math.ceil(bits / 4), '0');
}

const attempt = (f) => {
try { return String(f()); } catch (e) { return `${e.name}`; }
};

import { fileURLToPath } from 'node:url';
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const cases = [
['12', 2], ['0x1f', 10], ['0x1f', 16], ['1e3', 10], ['FG', 16], ['1_000', 10],
['-ff', 16], ['9007199254740993', 10], ['ffffffffffffffff', 16],
[String.fromCodePoint(0x212a), 36], [String.fromCodePoint(0x131), 36], ['', 10],
];
console.log('入力 / 基数 / parseInt / parseStrict');
for (const [s, b] of cases) {
const label = JSON.stringify(s).replace(/[^\x00-\x7f]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
console.log(`${label} / ${b} / ${parseInt(s, b)} / ${attempt(() => parseStrict(s, b))}`);
}
console.log('\n値 / ビット幅 / 2の補数(16進) / 戻した値');
for (const [v, bits] of [[-1n, 8], [-255n, 16], [-1n, 64], [200n, 8], [-129n, 8], [256n, 8]]) {
const r = attempt(() => hexOf(toTwos(v, bits), bits));
const back = attempt(() => fromTwos(toTwos(v, bits), bits));
console.log(`${v} / ${bits} / ${r} / ${back}`);
}
}

実行結果は次のとおりである。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ node radix-strict.mjs
入力 / 基数 / parseInt / parseStrict
"12" / 2 / 1 / SyntaxError
"0x1f" / 10 / 0 / SyntaxError
"0x1f" / 16 / 31 / SyntaxError
"1e3" / 10 / 1 / SyntaxError
"FG" / 16 / 15 / SyntaxError
"1_000" / 10 / 1 / SyntaxError
"-ff" / 16 / -255 / -255
"9007199254740993" / 10 / 9007199254740992 / 9007199254740993
"ffffffffffffffff" / 16 / 18446744073709552000 / 18446744073709551615
"\u212a" / 36 / NaN / SyntaxError
"\u0131" / 36 / NaN / SyntaxError
"" / 10 / NaN / SyntaxError

値 / ビット幅 / 2の補数(16進) / 戻した値
-1 / 8 / ff / -1
-255 / 16 / ff01 / -255
-1 / 64 / ffffffffffffffff / -1
200 / 8 / c8 / -56
-129 / 8 / RangeError / RangeError
256 / 8 / RangeError / RangeError

0x1f を基数 16 で読めないのは、接頭辞を許さない仕様にしたためである。受け付けたいなら、呼び出す側で取り除いてから渡す。

200 を 8 ビットにすると c8 になり、戻すと -56 になる。toTwos は符号なしとして入る値も通すので、c8 を符号付きで読むか符号なしで読むかは呼び出し側で決める。

正規表現で先に ASCII に絞っているのは、大文字と小文字の変換が ASCII の外から文字を持ち込むからである。ケルビン記号(U+212A)は toLowerCase() で k になり、トルコ語の点なし i(U+0131)は toUpperCase() で I になる。変換してから数字の表を引くと、見た目の違う文字が数字として通る。

ハシトシステムの進数変換では

進数変換 は、2、8、10、16 進と任意の基数(2〜36)を相互に変換する。入力は 1 文字ずつ検査して BigInt に積み上げるので、64 ビットを超える値も丸めない。2026-11-09 に本番の HTML から変換関数を取り出して動かすと、次のようになった。

  • 12 を 2 進として渡すと、parseInt と違ってエラーになる。FG の 16 進、1e3 の 10 進、1.5 も同様である。
  • 18446744073709551615 は丸められず、16 進で FFFFFFFFFFFFFFFF になる。
  • 1_000 と 1 000 は、区切りを読み飛ばして 1000 になる。結果欄の 4 桁区切りの表示を貼り付けても読める。
  • 16 進の欄に 0x1f を入れるとエラーになる。接頭辞は外して入れる。
  • 負数は -FF のように符号付きで表示する。2 の補数は計算しないので、ff01 のような表現がほしいときは上のスクリプトを使う。

一点、細かい例外がある。このツールは toUpperCase() してから数字の表を引くので、基数 19 以上では U+0131 の ı を I として受け付ける(基数 36 で 18 になった)。上で触れた変換の例で、普通の入力には影響しない。

サブネットの計算は サブネット計算、Snowflake ID の分解は Snowflake ID デコーダ で確かめられる。いずれもブラウザ内で処理され、入力はサーバーに送られない。

まとめ

  • parseInt は読めない文字で止まり、そこまでの値を返す。NaN の確認だけでは不正な入力を検出できない。
  • Number("") と BigInt("") は 0 になる。空欄は先に弾く。
  • 基数付きの BigInt 変換は標準にない。1 桁ずつ積み上げるか、2、8、16 進なら接頭辞を付ける。
  • toString は負数を -ff のように返す。2 の補数は >>> 0 か BigInt.asUintN で、ビット幅を決めて作る。
  • BigInt.asUintN は範囲外の値を黙って切り詰める。範囲の確認は別に書く。
  • Python の int は不正な桁で例外を出すが、全角数字を受け付け、10 進の 4300 桁を超えると ValueError になる。

結果は 2026-11-09 時点の上記の環境で得たものである。ランタイムの版によって変わる可能性があるので、手元でも上のスクリプトで確かめてほしい。

参考

  • MDN — parseInt()(無効な文字の扱い、基数の推定、精度)
  • MDN — Number.prototype.toString()(負数の扱い)
  • MDN — BigInt.asUintN()
  • Python ドキュメント — int()、整数と文字列の変換の長さ制限(Integer string conversion length limitation)

parseIntが基数2で12を1と読む理由 進数変換の打ち切りと負数
https://blog.hashito.biz/2026/11/09/parseint-radix-silent-truncate-negative-hex-twos-complement-bigint/
著者
hashito
作成日
2026年11月9日
著作権