# 整数除算はどこで丸める？BigIntで検算するSolidityの計算順序

整数の割り算では、掛ける順序と丸める単位で結果が変わります。BigIntのゼロ方向への丸め、切り上げ、Solidityの演算幅、個別計算と合計計算の違いを実行例で整理します。

正規URL: https://3mikan.com/archives/6073

著者: みかん
公開: 2026-09-06T00:00:00.000Z
更新: 2026-09-06T00:00:00.000Z

手数料や持分の計算式を、電卓では合っているのにコードでは再現できない。そんなときは小数点の表示を直す前に、**途中の割り算で端数を捨てていないか**を確認します。

実数として等しい式でも、整数演算では結果が変わります。この記事ではNode.jsのBigIntで小さな反例を実行し、Solidityの整数型へ移すときに残る上限・符号・中間積の違いまで整理します。アカウント、ブラウザ、RPC、デプロイは使いません。Solidityの挙動は一次仕様を参照し、掲載した結果はNode.jsでの実行結果と区別します。

掲載例はNode.js 22以降で実行できます。各節のコードを`example.mjs`へ保存し、同じ節で使う関数定義と呼び出しを合わせてから`node example.mjs`を実行してください。再利用できる関数は[integer-rounding.mjs](/examples/offline-web3/integer-rounding.mjs)から取得できます。

## 同じ式に見えても、割る位置で結果が変わる

次のコードは、そのままNode.jsで実行できます。

```js
console.log((1n * 3n) / 2n); // 1n
console.log((1n / 2n) * 3n); // 0n
```

最初の式は3を2で割り、整数の1を残します。後の式は最初に1を2で割って0にするため、その後で3を掛けても1には戻りません。

これはBigIntの精度が足りない問題ではありません。整数の除算が、ゼロ方向へ丸めた整数を返すためです。小数部分を保持する処理ではないので、後から掛け算をしても、途中で捨てた情報は復元しません。[^bigint]

```text
掛けてから割る：1 × 3 = 3 → 3 ÷ 2 = 1
割ってから掛ける：1 ÷ 2 = 0 → 0 × 3 = 0
```

実装では、元の式だけでなく、どの演算がどの単位の整数を返すかを書き出します。数量を最小単位の整数に直す方法は、[ERC-20数量計算の記事](/archives/6070)で扱っています。単位変換と、その後の比率計算の丸めは別の仕様です。

## 「切り捨て」は負数まで含めると曖昧になる

非負の値では、ゼロ方向への丸めと、数学でいう床関数による切り下げが一致します。しかし、負数では一致しません。

```js
console.log(-5n / 2n);        // -2n
console.log(Math.floor(-5 / 2)); // -3
console.log(-5n % 2n);        // -1n
```

`-2.5`から小数部分を取り除いてゼロへ近づけると`-2`です。負の無限大へ向けて切り下げると`-3`です。「端数切り捨て」とだけ書くと、どちらを求める仕様なのか分かりません。[^bigint]

Solidityの型付き整数の除算もゼロ方向へ丸めます。一方、`5 / 2`のような数値リテラル同士の式では、型付き整数の実行時演算とは異なり、有理数としての精度を保持することがあります。Solidityを確認する際は、`int256(-5) / int256(2)`のように型を明示した例と、リテラルだけの式を混同しないようにします。[^solidity]

以下の参照実装では、符号付きの丸め問題を混ぜないために、入力を非負の整数に限定します。

## 切り上げは、商と余りを見て決める

非負の`a × b ÷ denominator`を考えます。商を`q`、余りを`r`とすると、次の関係を満たします。

```text
a × b = q × denominator + r
0 ≤ r < denominator
```

切り下げなら`q`、切り上げなら余りがあるときだけ`q + 1`です。この規則をそのままコードにすると、割り切れる値やゼロを余分に1増やすことも防げます。

```js
function mulDivUnsigned(a, b, denominator, rounding = 'down') {
  const max = (1n << 256n) - 1n;
  for (const value of [a, b, denominator]) {
    if (typeof value !== 'bigint') throw new TypeError('expected bigint');
    if (value < 0n || value > max) throw new RangeError('outside uint256');
  }
  if (denominator === 0n) throw new RangeError('division by zero');
  if (rounding !== 'down' && rounding !== 'up') {
    throw new TypeError('rounding must be down or up');
  }
  const product = a * b;
  const quotient = product / denominator;
  const remainder = product % denominator;
  const result = quotient + (rounding === 'up' && remainder !== 0n ? 1n : 0n);
  if (result > max) throw new RangeError('result exceeds uint256');
  return result;
}

console.log(mulDivUnsigned(5n, 3n, 2n));       // 7n
console.log(mulDivUnsigned(5n, 3n, 2n, 'up')); // 8n
console.log(mulDivUnsigned(6n, 3n, 2n, 'up')); // 9n
console.log(mulDivUnsigned(0n, 3n, 2n, 'up')); // 0n
```

これはBigIntを使う**検算用の参照実装**です。入力と最終結果をuint256の範囲へ制限していますが、中間積まで256ビットに制限してはいません。

切り上げのために`(product + denominator - 1) / denominator`と書く方法も、非負かつ正の除数という条件では数学的に成立します。ただし、固定幅整数へ移すと、その足し算でも上限を超える可能性があります。参照実装では商と余りを使い、条件を読み取りやすくしています。

## 「掛け算を先にする」だけではSolidity側の解決にならない

最大値を`M = 2^256 - 1`とすると、`M × 2 ÷ 2`の最終結果は`M`です。しかし、中間の`M × 2`はuint256の上限を超えます。

```js
const max = (1n << 256n) - 1n;
console.log(mulDivUnsigned(max, 2n, 2n) === max); // true
console.log(max * 2n > max);                    // true
```

BigIntはこの中間積を保持できます。Solidityの通常の`uint256`乗算ではオーバーフローを検査するため、中間結果が範囲を外れると処理が取り消されます。`unchecked`に変えても、元の精度を維持するのではなく、オーバーフロー時の扱いが変わるだけです。[^solidity]

OpenZeppelinには、中間精度を保持して乗除算する`Math.mulDiv`が用意されています。採用時には使用バージョン、丸め方向、除数ゼロ、最終結果が範囲を超える場合の挙動を確認します。[^math]

この記事のBigInt関数は、そのSolidity実装をコンパイル・実行して検証したものではありません。任意精度で求めた期待値と、コントラクトの実装結果を別々に照合するための基準です。

## 個別計算の合計と、合計してからの計算は別の結果になる

1単位ずつの入力が10件あり、それぞれに`3 / 2`を掛けて切り下げるとします。

```text
個別に計算：floor(1 × 3 ÷ 2) × 10 = 10
合計後に計算：floor(10 × 3 ÷ 2) = 15
```

差の5単位は、計算機の不具合ではありません。10回の除算で端数を捨てるか、最後の1回だけで捨てるかの違いです。

どちらを採用すべきかは、計算の用途によります。請求単位が1件ごとなのか、期間内の合計なのか、余りを次回へ持ち越すのか。その仕様を先に決めます。「精度が高そうだから合計してから計算する」と置き換えると、1件ごとの上限や契約上の丸め規則を変えてしまうことがあります。

小さな入力を何度も処理する場合は、1回だけの誤差ではなく、繰り返しによる差もテストへ含めます。持分の丸めが状態変化へ影響する例は、[状態保持型ファジングの比較記事](/archives/6053)にもつながります。

## テストでは期待値だけでなく、不等式も確認する

非負の`a`・`b`と正の除数について、切り下げ結果`lo`が次の関係を満たすかを確認します。結果と除数を掛け戻せば元の積を超えず、結果を1増やして掛ければ元の積を超える、という関係です。

```text
lo × denominator ≤ a × b < (lo + 1) × denominator
```

切り上げと切り下げの差は、割り切れる場合に0、それ以外に1です。これなら、特定の数値を並べたテストだけに依存しません。

そのうえで、0、1、割り切れる値、1単位の余り、uint256の最大値、最大値を超える最終結果、除数ゼロ、負数、Numberの誤入力を個別に確認します。全組合せテストも有限の入力範囲の検証であり、すべての業務要件の正しさを証明するものではありません。

## まとめ

整数演算では、**単位、符号、丸め方向、丸める時点、中間積と最終結果の上限**をセットで決めます。

先に掛ければ途中の端数損失を減らせますが、固定幅整数のオーバーフローは別に残ります。BigIntを検算の正本にしながら、Solidity側では型と演算幅を明示し、ゼロ・境界値・繰り返し処理を確認するのが出発点です。

[^bigint]: [ECMAScript：BigIntの除算・剰余](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-bigint-divide)。2026-09-05確認。
[^solidity]: [Solidity：整数型・除算・checked arithmetic](https://docs.soliditylang.org/en/v0.8.36/types.html#division)。2026-09-05確認。記事内のSolidity説明は仕様参照であり、本検証ではコンパイル・EVM実行をしていません。
[^math]: [OpenZeppelin Contracts 5.x：Math.mulDiv](https://docs.openzeppelin.com/contracts/5.x/api/utils#Math-mulDiv-uint256-uint256-uint256-)。2026-09-05確認。

## 確認した一次情報

- [ECMAScript：BigIntの除算・剰余](<https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-bigint-divide>): 確認日 2026-09-05
- [Solidity：整数型・除算・checked arithmetic](<https://docs.soliditylang.org/en/v0.8.36/types.html#division>): 確認日 2026-09-05
- [OpenZeppelin Contracts 5.x：Math.mulDiv](<https://docs.openzeppelin.com/contracts/5.x/api/utils#Math-mulDiv-uint256-uint256-uint256->): 確認日 2026-09-05
