1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

基数変換ツールを作る — BigInt で桁数無制限、そして「小数の循環」を検出する

1
Posted at

2〜36 進数を相互変換するツールを作った。「parseInt(s, 16) でいいのでは?」と思うかもしれないが、まともに作ると 2 つの落とし穴がある: (1) Number は 2^53 を超えると精度が壊れるので大きい数は BigInt が要る、(2) 小数部は、ある基数で割り切れても別の基数では循環する (10 進の 0.1 は 2 進で 0.0(0011) と無限に循環)。この循環をどう検出して表示するかが本題。完全ブラウザ完結・vanilla JS。

🌐 デモ: https://sen.ltd/portfolio/base-converter/
📦 GitHub: https://github.com/sen-ltd/base-converter

スクリーンショット

hinge 1: 整数部は BigInt で桁数無制限

parseInt("ff", 16) は確かに 255 を返す。でも大きい数だと壊れる:

Number.parseInt("ffffffffffffffff", 16)  // 18446744073709552000 (間違い!)
// 正しくは 18446744073709551615

Number は float64 なので 2^53 を超えると整数を正確に表せない。基数変換ツールとしては致命的。BigInt で実装する:

const DIGITS = "0123456789abcdefghijklmnopqrstuvwxyz";

// 文字列 → BigInt
export function parseIntPart(intStr, base) {
  const b = BigInt(base);
  let acc = 0n;
  for (const ch of intStr) {
    acc = acc * b + BigInt(digitValue(ch));
  }
  return acc;
}

// BigInt → 文字列
export function renderIntPart(value, base) {
  if (value === 0n) return "0";
  const b = BigInt(base);
  let out = "";
  let v = value;
  while (v > 0n) {
    out = DIGITS[Number(v % b)] + out;
    v = v / b;
  }
  return out;
}

これで桁数の上限が消える。テストで 200bit を確認:

test("200-bit number converts exactly", () => {
  const bin = "1" + "0".repeat(200);  // 2^200
  assert.equal(renderIntPart(parseIntPart(bin, 2), 10),
    "1606938044258990275541962092341162602522202993782792835301376");
});

v % b の結果を Number() で添字にしているが、b は最大 36 なので剰余は必ず 0〜35。ここだけは安全に Number 化できる。

hinge 2: 小数部の循環を検出する

ここが面白いところ。ある基数で「割り切れる」小数が、別の基数では「循環」する

  • 10 進の 0.1 = 2 進で 0.0001100110011... (0011 が無限循環)
  • 10 進の 0.5 = 2 進で 0.1 (ちょうど終わる)

なぜか? 0.1 = 1/10。分母 10 = 2×5 に 5 の因子があるが、2 進数は分母が 2 の冪のときしか割り切れない。だから 5 が残って循環する。

循環の検出: 剰余を覚えておく

小数部の基数変換は「変換先の基数を掛けて整数部を取り出す」の繰り返し。このとき剰余 (remainder) が再登場したら、そこから循環している。筆算の循環小数検出と同じ原理:

export function convertFraction(fracStr, fromBase, toBase, maxDigits = 64) {
  // 小数部を厳密に num/den (BigInt) として持つ
  const fb = BigInt(fromBase);
  let num = 0n, den = 1n;
  for (const ch of fracStr) {
    num = num * fb + BigInt(digitValue(ch));
    den = den * fb;
  }
  // 約分 (剰余の管理を正準にするため)
  const g = gcd(num, den);
  if (g > 0n) { num /= g; den /= g; }

  const tb = BigInt(toBase);
  const digits = [];
  const seen = new Map();      // 剰余 → 出現した桁位置
  let rem = num;
  let repeatStart = -1;

  while (rem !== 0n && digits.length < maxDigits) {
    if (seen.has(rem)) {       // この剰余は前に見た = 循環開始
      repeatStart = seen.get(rem);
      break;
    }
    seen.set(rem, digits.length);
    rem *= tb;
    digits.push(DIGITS[Number(rem / den)]);  // 次の桁
    rem %= den;
  }
  return { digits: digits.join(""), repeatStart };
}

ポイントは小数を num/den の分数として BigInt で厳密に持つこと。浮動小数点で計算すると誤差が入って循環検出が壊れる。BigInt の分数演算なら、剰余が正確に一致するので循環節を確実に見つけられる。

出力は循環節を括弧で囲む:

test("0.1 dec → repeating binary, wrapped in parens", () => {
  assert.deepEqual(convert("0.1", 10, 2), { value: "0.0(0011)", repeating: true });
});

0.0(0011)(0011) が無限に繰り返す部分。デモでは 10 進 0.1 が 2 進 0.0(0011)、8 進 0.0(6314)、16 進 0.1(9) と、基数ごとに違う循環をするのが一目で見える。

なぜ約分するのか

convertFraction の頭で gcd 約分しているのは、剰余の一致判定を正準化するため。約分しないと同じ値が 2/201/10 のように複数表現を持ち、剰余ベースの循環検出が正しく動かないことがある。最初に既約分数にしておけば、剰余は値に対して一意になる。

入力検証

基数 N では使える桁が 0 〜 (N-1 番目の文字) に限られる。2 進数に 2 があったら不正:

export function isValidForBase(str, base) {
  const body = str.startsWith("-") ? str.slice(1) : str;
  if (body === "" || body === ".") return false;
  let seenDot = false;
  for (const ch of body) {
    if (ch === ".") {
      if (seenDot) return false;  // 小数点は 1 個まで
      seenDot = true;
      continue;
    }
    const v = digitValue(ch);
    if (v < 0 || v >= base) return false;  // 基数外の桁
  }
  return true;
}

UI ではエラー時に「使える桁: 0123456789abcdef」のように具体的に出す。

設計

convert.js — parse/render (BigInt)、小数の基数変換 + 循環検出 (DOM-free, 38 tests)
app.js     — UI glue、主要基数の一覧テーブル

対応基数 2〜36 (0-9 + a-z)、負数・小数対応。convert.js は DOM 非依存なので 38 個のテストが全部 Node で走る。

試してみる

0.1 を入れて出力基数を 2 / 8 / 16 と変えてみてほしい。どの基数でも循環すること、循環節が違うことが見える。整数で 2^100 級の巨大な数を入れても精度が壊れないのも確認できる。

まとめ

  • 基数変換の整数部は BigInt で。Number は 2^53 超で精度が壊れる。v % base の添字化だけは base≤36 なので安全に Number()
  • 小数部はある基数で割り切れても別基数で循環する。分母に変換先基数の素因数以外が残ると循環。
  • 循環検出は剰余の再登場Map で記録。筆算の循環小数と同じ原理。
  • 小数を num/den の BigInt 分数で厳密に持てば、浮動小数点誤差なしで循環節を見つけられる。
  • 最初に gcd 約分して剰余を正準化すると検出が安定する。

これは SEN 合同会社の OSS ポートフォリオ #269 です。https://sen.ltd/portfolio/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?