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 | |
Array.prototype.map にそのまま渡すと、配列の添字が基数になる。
1 | |
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 | |
負数は - を外してから変換する必要がある。3 進や 36 進は、この方法では読めない。
2^53 を超えると別の数になる
parseInt の戻り値は Number なので、Number.MAX_SAFE_INTEGER(9007199254740991)を超える値は丸められる。
1 | |
64 ビットの最大値を読んだはずが、1 大きい 2^64 になっている。JSON の大きな整数で起きる丸めと同じ原因で、JSON 整形で大きな整数が変わる話 や Snowflake ID のデコード でも扱った。大きな ID を扱うなら、文字列のまま BigInt にする。
負数の toString は 2 の補数にならない
MDN にあるとおり、Number.prototype.toString は基数 2 でも符号を保ち、2 の補数を返さない。
1 | |
C やバイナリファイルの世界で -1 は ff や ffffffff だが、JavaScript の toString からは出てこない。padStart で桁をそろえると、000-ff という数値として読めない文字列になる。
2 の補数がほしいなら、ビット幅を決めて変換する。32 ビットまでなら符号なし右シフト、それ以上なら BigInt.asUintN が使える。
1 | |
>>> 0 は 32 ビットに切り詰めるので、2 ** 32 >>> 0 は 0 になる。サブネットマスクの計算でこの切り詰めに引っかかる話は シフト演算の記事 に書いた。
BigInt.asUintN も、指定した幅に入らない値をエラーにしない。下位のビットだけを残す。
1 | |
-255 は 8 ビットで表せないが、1 が返る。範囲の確認は自分で書く必要がある。
Python も bin(-5) は '-0b101'、hex(-255) は '-0xff' で、符号が付く。2 の補数はマスクで作る。to_bytes は範囲外なら OverflowError を出すので、幅の確認も兼ねられる。
1 | |
Python には桁数の上限がある
Python の整数に上限はないが、文字列との変換には上限がある。Python 3.11.9 の既定は 4300 桁で、超えると ValueError になる。
1 | |
ドキュメントによると、制限の対象は計算量が線形でない変換である。基数が 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 | |
実行結果は次のとおりである。
1 | |
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)