- JavaScript
JavaScriptの小数計算誤差と丸め|toFixedの落とし穴と正しい対処法
JavaScriptの小数計算誤差とは、Number型が数値を2進数の浮動小数点(IEEE 754 倍精度)で持つため、0.1 のような10進数の小数をぴったり表せずに生じるわずかなズレのことです。 0.1 + 0.2 が 0.30000000000000004 になるのも、(1.005).toFixed(2) が "1.00" になるのも、同じ原因から起きています。
この記事では、誤差が出る代表例を Node.js 22.22(V8 12.4)で実際に実行した結果 で一覧にし、なぜ起きるかを図で短く整理したうえで、「整数で持つ」「EPSILONで比べる」「表示だけ丸める」「decimal.jsを使う」のどれを選べばよいかの判断表 と、負の数でも正しく動く四捨五入・切り捨て・切り上げ・偶数丸めのコピペ関数 を載せます。最後に、金額計算で迷わないための推奨パターンもまとめます。
小数計算誤差と対処法の早見表
まずはこの表で「何が起きるか」と「どう直すか」をつかめます。 実行結果はすべて Node.js 22.22.2 で確認したものです。
| やりたいこと・起きること | そのまま書いた結果 | おすすめの書き方 | 結果 |
|---|---|---|---|
| 0.1 + 0.2 を足す | 0.30000000000000004 |
最小単位の整数で計算 (10 + 20) / 100 |
0.3 |
| 0.1 + 0.2 === 0.3 を比べる | false |
相対誤差で比較 nearlyEqual() |
true |
| 1.005 を小数2桁に四捨五入 | (1.005).toFixed(2) → "1.00" |
roundHalfUp(1.005, 2)(本文の関数) |
1.01 |
| 1.005 を小数2桁に四捨五入 | Math.round(1.005 * 100) / 100 → 1 |
同上、または new Decimal("1.005").toFixed(2) |
1.01 |
| 1.15 × 100 | 114.99999999999999 |
new Big("1.15").times(100) |
115 |
| 90円の3割引を切り捨て | Math.floor(90 * 0.7) → 62 |
Math.floor(90 * 7 / 10) |
63 |
| -2.5 を四捨五入 | Math.round(-2.5) → -2 |
roundHalfUp(-2.5) |
-3 |
| 偶数丸め(最近接偶数への丸め) | 組み込み関数なし | roundHalfEven() / Intl.NumberFormat の halfEven |
2.5 → 2 |
| 表示だけ桁をそろえる | toFixed は誤差の影響を受ける |
Intl.NumberFormat の maximumFractionDigits + roundingMode |
"1.01" |
| 2^53 を超える整数 | 2 ** 53 + 1 → 9007199254740992 |
BigInt:2n ** 53n + 1n |
9007199254740993n |
| JSONの大きなID | JSON.parse で末尾が変わる |
IDは文字列で受け取る | 元のまま |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptの小数計算誤差と丸め|toFixedの落とし穴と正しい対処法」
誤差が出る例の一覧(Node.js 22 で実行)
誤差は「足し算」だけでなく、掛け算・丸め・大きな整数でも起きます。 次のコードをそのまま実行すると、代表的なパターンを一度に確認できます。
// err.mjs 実行: node err.mjs (Node.js 22.22.2)
const cases = {
"0.1 + 0.2": 0.1 + 0.2,
"0.1 + 0.2 === 0.3": 0.1 + 0.2 === 0.3,
"0.3 - 0.1": 0.3 - 0.1,
"1.15 * 100": 1.15 * 100,
"0.07 * 100": 0.07 * 100,
"1.1 * 3": 1.1 * 3,
"(1.005).toFixed(2)": (1.005).toFixed(2),
"(1.255).toFixed(2)": (1.255).toFixed(2),
"(1.45).toFixed(1)": (1.45).toFixed(1),
"(2.5).toFixed(0)": (2.5).toFixed(0),
"Math.round(1.005 * 100) / 100": Math.round(1.005 * 100) / 100,
"2 ** 53": 2 ** 53,
"2 ** 53 + 1": 2 ** 53 + 1,
"Number.MAX_SAFE_INTEGER": Number.MAX_SAFE_INTEGER,
"Number.MAX_SAFE_INTEGER + 2": Number.MAX_SAFE_INTEGER + 2,
"Number.isSafeInteger(2 ** 53)": Number.isSafeInteger(2 ** 53),
"9007199254740993": 9007199254740993,
"Number.EPSILON": Number.EPSILON,
};
for (const [k, v] of Object.entries(cases)) console.log(k.padEnd(34), "→", v);
実行結果:
0.1 + 0.2 → 0.30000000000000004
0.1 + 0.2 === 0.3 → false
0.3 - 0.1 → 0.19999999999999998
1.15 * 100 → 114.99999999999999
0.07 * 100 → 7.000000000000001
1.1 * 3 → 3.3000000000000003
(1.005).toFixed(2) → 1.00
(1.255).toFixed(2) → 1.25
(1.45).toFixed(1) → 1.4
(2.5).toFixed(0) → 3
Math.round(1.005 * 100) / 100 → 1
2 ** 53 → 9007199254740992
2 ** 53 + 1 → 9007199254740992
Number.MAX_SAFE_INTEGER → 9007199254740991
Number.MAX_SAFE_INTEGER + 2 → 9007199254740992
Number.isSafeInteger(2 ** 53) → false
9007199254740993 → 9007199254740992
Number.EPSILON → 2.220446049250313e-16
ポイントは3つです。
| 種類 | 例 | 何が起きているか |
|---|---|---|
| 演算の誤差 | 0.1 + 0.2、1.15 * 100 |
入力の時点でわずかにズレた値どうしを計算するので、ズレが表に出る |
| 丸めの誤差 | (1.005).toFixed(2) → "1.00" |
1.005 は内部で 1.00499999… なので「5未満」と判定される |
| 整数の精度 | 2 ** 53 + 1 |
2^53(約9千兆)を超えると、隣り合う整数を区別できない |
割引・税計算でも起きる例
掛け算のあとに Math.floor で切り捨てると、1円ずれることがあります。 1〜5000円を総当たりで確かめると、Math.floor(price * 0.7) と Math.floor(price * 7 / 10) の結果が違う価格が 70件 ありました(最初の例は 90円)。
// 1〜5000円で「3割引の切り捨て」が食い違う価格を数える
let bad = [];
for (let p = 1; p <= 5000; p++) {
if (Math.floor(p * 0.7) !== Math.floor(p * 7 / 10)) bad.push(p);
}
console.log(bad.length, bad.slice(0, 5), 90 * 0.7);
// 実行結果: 70 [ 90, 170, 180, 330, 340 ] 62.99999999999999
p * 7 / 10 のように 「整数どうしを掛けてから、最後に1回だけ割る」 と、割り算の結果は正しく丸められた値になるため、この範囲では食い違いが出ませんでした。なお、同じ方法で 0.9・0.8・1.05 倍を調べたところ 1〜5000 の範囲では食い違いは出ず、1.15 倍では 97 件ありました。「たまたま出ない率」もあるので、テストで見つけにくいのが特徴です。
なぜ誤差が起きるのか(2進数の浮動小数点を図で)
10進数の 0.1 は、2進数では「0.0001100110011…」と無限に続く小数になります。 Number 型は仮数部 52 ビット(実質 53 ビット)しか持てないので、途中で切って一番近い値に丸めて保存します。
flowchart LR
A["ソースコードの 0.1"] --> B["2進数に変換<br/>0.000110011001100…(無限に続く)"]
B --> C["53ビットで丸めて保存<br/>実際の値 0.1000000000000000055511…"]
C --> D["計算(0.1 + 0.2)"]
D --> E["結果も53ビットに丸める<br/>0.3000000000000000444…"]
E --> F["表示:同じ値になる最短の10進数<br/>0.30000000000000004"]
toPrecision(21) で、保存されている値を21桁まで表示すると、ズレが見えます。
console.log((0.1).toPrecision(21), (1.005).toPrecision(21), (1.15).toPrecision(21), (0.07).toPrecision(21), (1.45).toPrecision(21));
// 実行結果: 0.100000000000000005551 1.00499999999999989342 1.14999999999999991118 0.0700000000000000066613 1.44999999999999995559
| ソースの値 | 実際に保存されている値(21桁) | 起きること |
|---|---|---|
1.005 |
1.00499999999999989342 | 小数2桁の四捨五入で「切り捨て側」になる |
1.15 |
1.14999999999999991118 | ×100 が 114.999… になる |
0.07 |
0.0700000000000000066613 | ×100 が 7.000…001 になる |
1.45 |
1.44999999999999995559 | toFixed(1) が "1.4" になる |
console.log(0.1) が 0.1 と表示されるのは、「その値と区別できる最短の10進数」を表示しているからです。 保存値は最初からズレていて、計算を重ねるとズレが表示の桁まで上がってきます。
2^53 を超える整数
整数も、Number.MAX_SAFE_INTEGER(9007199254740991)までしか正確に表せません。 それより大きいと、隣の整数との間隔が 2 以上になります。データベースの 64bit ID や、ナノ秒のタイムスタンプを Number で受けると末尾が変わるのはこのためです。
console.log(JSON.parse('{"id": 9007199254740993}').id);
// 実行結果: 9007199254740992
大きなIDは、APIの段階で文字列として受け渡すのが確実です。
対処法の判断表:どれを選べばよいか
目的によって正解が変わります。 「計算を正確にしたい」のか「表示を整えたい」のか「比較したい」のかで選びます。
| 方法 | 向いている用途 | 長所 | 注意点 |
|---|---|---|---|
| 最小単位の整数で持つ(円・セント・グラム) | 金額、数量、ポイント | 依存なし・速い・誤差なし | 割り算・率の計算で丸め位置を決める必要がある |
Number.EPSILON を使った比較 |
計算結果が「ほぼ等しいか」の判定 | 1行で書ける | 大きな値では相対誤差にする必要がある |
Math.round(x * 10 ** n) / 10 ** n |
手早く表示用に丸める | 短い | 1.005 → 1 のように境界で外れる |
| 指数表記シフトの丸め関数(本文) | 依存なしで小数n桁の丸め | 1.005 → 1.01 になる | 入力自体が誤差を含む計算結果だと限界あり |
toFixed(n) |
文字列として桁をそろえる | 末尾0を補う("1.500") |
保存値で判定するので 1.005 → "1.00" |
toPrecision(n) |
有効数字でそろえる | 桁数の大小に強い | 大きな値は指数表記になる |
Intl.NumberFormat(roundingMode) |
画面表示・桁区切り・通貨記号 | 9種類の丸め方を指定できる | 戻り値は文字列。計算には使わない |
BigInt |
2^53 超の整数、整数どうしの計算 | 桁数の制限なし | 小数は扱えない。Number と混ぜられない |
| decimal.js / big.js | 小数の計算そのものを正確にしたい | 10進数で計算・丸めモード豊富 | 依存が増える。文字列で渡す必要がある |
迷ったら「データは整数(最小単位)で持つ」「表示は Intl.NumberFormat」「率や割り算が多い計算は decimal.js」の3つを基本にすると整理しやすくなります。
比較するなら Number.EPSILON(相対誤差で)
Number.EPSILON(約 2.22e-16)は「1 と、1 の次に大きい Number との差」です。 0付近の値ならそのまま閾値に使えますが、値が大きくなると誤差も大きくなるため、値の大きさに合わせて閾値を広げます。
// eps.mjs
const nearlyEqual = (a, b, eps = Number.EPSILON) =>
Math.abs(a - b) < eps * Math.max(1, Math.abs(a), Math.abs(b));
console.log("nearlyEqual(0.1+0.2, 0.3):", nearlyEqual(0.1 + 0.2, 0.3));
console.log("Math.abs(0.1+0.2-0.3) < Number.EPSILON:", Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);
console.log("1000.1+1000.2 vs 2000.3 単純EPSILON:",
Math.abs(1000.1 + 1000.2 - 2000.3) < Number.EPSILON, " 相対:", nearlyEqual(1000.1 + 1000.2, 2000.3));
実行結果:
nearlyEqual(0.1+0.2, 0.3): true
Math.abs(0.1+0.2-0.3) < Number.EPSILON: true
1000.1+1000.2 vs 2000.3 単純EPSILON: false 相対: true
単純に < Number.EPSILON と書くと、1000 程度の値で false になります。 計算を何回も重ねる場合は、eps を Number.EPSILON * 10 などに広げて使います。
四捨五入・切り捨て・切り上げ・偶数丸めのコピペ関数
x * 10 ** n の掛け算で誤差が入るのを避けるため、「指数表記の文字列で桁をずらす」方法を使います。 1.005 を "1.005e2" として Number() に渡すと、10進数の 100.5 として読み直されるので、境界の判定が見た目どおりになります。
// round.mjs 依存なし(Node.js 22 / ブラウザ共通)
// 指数表記で桁をずらす(x * 10**n の掛け算誤差を避ける)
function shift(x, n) {
const [m, e = "0"] = String(x).split("e");
return Number(`${m}e${Number(e) + n}`);
}
// 四捨五入(0から遠い方へ:負の数も -1.5 → -2)
export function roundHalfUp(x, digits = 0) {
const s = Math.sign(x);
return s * shift(Math.round(shift(Math.abs(x), digits)), -digits);
}
// 切り捨て(0方向)
export function truncate(x, digits = 0) {
return shift(Math.trunc(shift(x, digits)), -digits);
}
// 切り下げ(-∞方向)
export function floor(x, digits = 0) {
return shift(Math.floor(shift(x, digits)), -digits);
}
// 切り上げ(+∞方向)
export function ceil(x, digits = 0) {
return shift(Math.ceil(shift(x, digits)), -digits);
}
// 偶数丸め(ちょうど半分なら偶数側へ)
export function roundHalfEven(x, digits = 0) {
const v = shift(x, digits);
const r = Math.round(v);
const isHalf = Math.abs(v % 1) === 0.5;
const res = isHalf ? (r % 2 === 0 ? r : r - 1) : r;
return shift(res, -digits);
}
確認用のコードと実行結果です(小数2桁)。
// test_round.mjs
import { roundHalfUp, truncate, floor, ceil, roundHalfEven } from "./round.mjs";
const xs = [1.005, 1.255, 1.15, 2.345, -1.005, -2.345, 1e-7, 123.456e5];
console.log("x".padEnd(12), "Math.round(x*100)/100", "roundHalfUp", "truncate", "floor", "ceil", "halfEven");
for (const x of xs) console.log(String(x).padEnd(12), Math.round(x*100)/100,
roundHalfUp(x,2), truncate(x,2), floor(x,2), ceil(x,2), roundHalfEven(x,2));
x Math.round(x*100)/100 roundHalfUp truncate floor ceil halfEven
1.005 1 1.01 1 1 1.01 1
1.255 1.25 1.26 1.25 1.25 1.26 1.26
1.15 1.15 1.15 1.15 1.15 1.15 1.15
2.345 2.35 2.35 2.34 2.34 2.35 2.34
-1.005 -1 -1.01 -1 -1.01 -1 -1
-2.345 -2.35 -2.35 -2.34 -2.35 -2.34 -2.34
1e-7 0 0 0 0 0.01 0
12345600 12345600 12345600 12345600 12345600 12345600 12345600
Math.round(x*100)/100 は 1.005 と 1.255 で外れますが、roundHalfUp は見た目どおり 1.01・1.26 になります。 1e-7 のような指数表記の入力も、shift が指数部を足し引きするので正しく扱えます。
負の数での挙動(Math.round(-2.5) は -2)
Math.round は「ちょうど半分なら +∞ 方向」に丸めるため、Math.round(-2.5) は -2 になります。 日本で一般的な「四捨五入」(0から遠い方へ)とは負の数で結果が変わります。
| x | Math.round | roundHalfUp | roundHalfEven | Math.trunc | Math.floor | Math.ceil |
|---|---|---|---|---|---|---|
| 2.5 | 3 | 3 | 2 | 2 | 2 | 3 |
| 3.5 | 4 | 4 | 4 | 3 | 3 | 4 |
| -2.5 | -2 | -3 | -2 | -2 | -3 | -2 |
| -3.5 | -3 | -4 | -4 | -3 | -4 | -3 |
| 0.5 | 1 | 1 | 0 | 0 | 0 | 1 |
| -0.5 | -0 | -1 | 0 | -0 | -1 | -0 |
| 1.5 | 2 | 2 | 2 | 1 | 1 | 2 |
(Node.js 22.22.2 の実行結果。-0 は console.log の表示どおり)
| 呼び方 | 方向 | JavaScriptの関数 |
|---|---|---|
| 切り捨て(0方向) | 正は小さく、負は大きく | Math.trunc / truncate() |
| 切り下げ(床関数) | 常に小さい方へ | Math.floor / floor() |
| 切り上げ(天井関数) | 常に大きい方へ | Math.ceil / ceil() |
| 四捨五入(0から遠い方) | 半分は絶対値が大きい方 | roundHalfUp() |
| 半分は+∞方向 | 半分は大きい方 | Math.round |
| 偶数丸め | 半分は偶数側 | roundHalfEven() |
負の数を「切り捨て」と言うときは、Math.trunc(0方向)か Math.floor(-∞方向)かを仕様で決めておく と、あとで迷いません。
丸め関数の限界
この関数は「入力した数値が、見た目どおりの10進数」であることを前提にしています。 たとえば 1.15 * 100 のように、すでに誤差を含んだ計算結果(114.99999999999999)を渡すと、その値として丸めます。計算の途中で誤差が入るなら、計算自体を整数か decimal.js で行うのが確実です。
toFixed・toPrecision・Intl.NumberFormat の違い
toFixed は「保存値」で判定し、文字列を返す
toFixed は内部の正確な値(1.00499999…)を基準に丸めるので、(1.005).toFixed(2) は "1.00" になります。 仕様どおりの動作で、半分ちょうどのときは0から遠い方へ丸めます。
console.log((-2.5).toFixed(0), (-1.005).toFixed(2), typeof (1.5).toFixed(0), (1e21).toFixed(2),
(1.5).toFixed(3), (0.5).toFixed(0), (8.345).toFixed(2), (8.335).toFixed(2));
try { (1).toFixed(101); } catch (e) { console.log(e.name + ": " + e.message); }
-3 -1.00 string 1e+21 1.500 1 8.35 8.34
RangeError: toFixed() digits argument must be between 0 and 100
| toFixed の性質 | 実例 |
|---|---|
| 戻り値は文字列 | typeof (1.5).toFixed(0) → "string" |
| 末尾の0を補う | (1.5).toFixed(3) → "1.500" |
| 半分ちょうどは0から遠い方へ | (-2.5).toFixed(0) → "-3" |
| 保存値で判定するので境界は入力しだい | 8.345 → "8.35"、8.335 → "8.34" |
| 1e21 以上は指数表記 | (1e21).toFixed(2) → "1e+21" |
| 桁数は 0〜100 | 101 で RangeError |
toFixed の結果を Number() に戻して再計算すると、また誤差が入ります。 表示の直前に1回だけ使います。
toPrecision は有効数字でそろえる
console.log((123.456).toPrecision(4), (0.000123456).toPrecision(2), (123456).toPrecision(2));
// 実行結果: 123.5 0.00012 1.2e+5
測定値やグラフのラベルのように「桁数より有効数字」が大事な表示に向いています。 整数部が桁数より長いと指数表記になる点に注意します。
Intl.NumberFormat の roundingMode(Node 22 で利用可)
Intl.NumberFormat は roundingMode で9種類の丸め方を選べます(ECMA-402 の Intl.NumberFormat v3 で追加。Node.js 22 で動作を確認)。桁区切りや通貨記号も同時に付けられるので、画面表示はこれにまとめるのがすっきりします。
// intl_round.mjs
const modes = ["halfExpand", "halfEven", "halfTrunc", "trunc", "floor", "ceil", "expand", "halfCeil", "halfFloor"];
for (const m of modes) {
const nf = new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 2, roundingMode: m });
console.log(m.padEnd(11), [1.005, 1.255, 2.345, -2.345, 0.125].map(v => nf.format(v)).join(" "));
}
const nf0 = new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 0, roundingMode: "halfEven" });
console.log("halfEven 0桁:", [0.5, 1.5, 2.5, -2.5].map(v => nf0.format(v)).join(" "));
console.log("文字列を渡す:", new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 2 }).format("1.005"));
console.log("useGrouping false:", new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 2, useGrouping: false }).format("12345.675"));
実行結果(入力:1.005 / 1.255 / 2.345 / -2.345 / 0.125):
halfExpand 1.01 1.26 2.35 -2.35 0.13
halfEven 1 1.26 2.34 -2.34 0.12
halfTrunc 1 1.25 2.34 -2.34 0.12
trunc 1 1.25 2.34 -2.34 0.12
floor 1 1.25 2.34 -2.35 0.12
ceil 1.01 1.26 2.35 -2.34 0.13
expand 1.01 1.26 2.35 -2.35 0.13
halfCeil 1.01 1.26 2.35 -2.34 0.13
halfFloor 1 1.25 2.34 -2.35 0.12
halfEven 0桁: 0 2 2 -2
文字列を渡す: 1.01
useGrouping false: 12345.68
| roundingMode | 意味 | 日本語での呼び方の目安 |
|---|---|---|
halfExpand(既定) |
半分は0から遠い方へ | 四捨五入 |
halfEven |
半分は偶数側へ | 偶数丸め(最近接偶数への丸め) |
halfTrunc |
半分は0方向へ | 五捨六入に近い |
trunc |
0方向へ | 切り捨て |
floor |
-∞方向へ | 切り下げ |
ceil |
+∞方向へ | 切り上げ(正の数) |
expand |
0から遠い方へ | 絶対値の切り上げ |
halfCeil |
半分は+∞方向へ | Math.round と同じ方向 |
halfFloor |
半分は-∞方向へ | ― |
Node.js 22 では format(1.005) が "1.01" になり、toFixed と結果が異なりました。 さらに確実にしたい場合は、format("1.005") のように 文字列で渡す と10進数としてそのまま丸められます。末尾の0をそろえたいときは minimumFractionDigits: 2 を併用します。通貨や桁区切りの細かい書式は、既存記事「JavaScript Intlの使い方」でまとめています。
BigInt:2^53 を超える整数を正確に扱う
BigInt は桁数の制限がない整数型で、数値の末尾に n を付けて書きます。 整数専用なので、小数の誤差の対処には使えませんが、大きなIDや「最小単位の整数」で桁が足りないときに役立ちます。
console.log("2n**53n+1n:", 2n ** 53n + 1n, " 7n/2n:", 7n / 2n, " -7n/2n:", -7n / 2n);
try { console.log(1n + 1); } catch (e) { console.log(e.constructor.name + ": " + e.message); }
try { console.log(JSON.stringify({ id: 1n })); } catch (e) { console.log(e.constructor.name + ": " + e.message); }
2n**53n+1n: 9007199254740993n 7n/2n: 3n -7n/2n: -3n
TypeError: Cannot mix BigInt and other types, use explicit conversions
TypeError: Do not know how to serialize a BigInt
| BigInt の性質 | 対応 |
|---|---|
割り算は0方向に切り捨て(-7n / 2n → -3n) |
四捨五入が必要なら余りを見て自分で調整 |
Number と混ぜると TypeError |
BigInt(x) / Number(x) で明示的に変換 |
JSON.stringify できない |
toString() で文字列にしてから送る |
Math.* が使えない |
演算子(+ - * / % **)で書く |
decimal.js / big.js:小数を10進数のまま計算する
計算そのものを10進数で行いたいときは、任意精度の10進数ライブラリを使います。 ここでは decimal.js 10.6.0 と big.js 7.0.1 を npm で入れて確認しました。
npm i decimal.js big.js
// lib.mjs
import Decimal from "decimal.js";
import Big from "big.js";
console.log("Decimal 0.1+0.2:", new Decimal("0.1").plus("0.2").toString());
console.log("Decimal 1.005 toFixed(2):", new Decimal("1.005").toFixed(2),
" toDP HALF_EVEN:", new Decimal("1.005").toDecimalPlaces(2, Decimal.ROUND_HALF_EVEN).toString());
console.log("Decimal(1.005)数値から:", new Decimal(1.005).toString());
console.log("Decimal -2.5 round(HALF_UP):", new Decimal("-2.5").toDecimalPlaces(0, Decimal.ROUND_HALF_UP).toString());
console.log("Decimal 1/3:", new Decimal(1).div(3).toString());
console.log("Big 0.1+0.2:", new Big("0.1").plus("0.2").toString(),
" Big 1.005 round(2):", new Big("1.005").round(2).toString(),
" round half-even:", new Big("1.005").round(2, Big.roundHalfEven).toString());
console.log("Big 1.15*100:", new Big("1.15").times(100).toString());
Decimal 0.1+0.2: 0.3
Decimal 1.005 toFixed(2): 1.01 toDP HALF_EVEN: 1
Decimal(1.005)数値から: 1.005
Decimal -2.5 round(HALF_UP): -3
Decimal 1/3: 0.33333333333333333333
Big 0.1+0.2: 0.3 Big 1.005 round(2): 1.01 round half-even: 1
Big 1.15*100: 115
| ライブラリ | 特徴 | 向いている用途 |
|---|---|---|
| big.js | 小さく、API が少ない | 四則演算と丸めだけで足りる計算 |
| decimal.js | 精度・丸めモードを細かく設定、三角関数なども対応 | 率の計算、割り算、統計 |
値は文字列で渡すのが基本です。 new Decimal(1.005) は最短表記の "1.005" として扱われましたが、計算結果の Number(誤差を含む値)を渡すとその誤差ごと取り込みます。入力値はフォームや API の文字列のまま渡すと安心です。1/3 のような割り切れない計算は、decimal.js の既定精度(有効数字20桁)で丸められます。
金額計算の推奨策
金額は「最小単位の整数で持ち、率は整数比で計算し、丸めは最後に1回、表示は Intl.NumberFormat」が基本です。
// money.mjs
// 金額は最小単位(円・セント)の整数で持つ
const items = [
{ name: "ノート", unitPrice: 1999, qty: 3 }, // 19.99 ドルをセントで
{ name: "ペン", unitPrice: 110, qty: 7 }, // 1.10 ドル
];
const subtotal = items.reduce((sum, i) => sum + i.unitPrice * i.qty, 0);
// 税率は「百分率の整数」で持ち、整数演算 → 最後に1回だけ丸める
const taxRatePercent = 10;
const tax = Math.floor((subtotal * taxRatePercent) / 100);
const total = subtotal + tax;
const fmt = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
console.log({ subtotal, tax, total });
console.log("表示:", fmt.format(subtotal / 100), fmt.format(tax / 100), fmt.format(total / 100));
console.log("Number.isSafeInteger(total):", Number.isSafeInteger(total));
{ subtotal: 6767, tax: 676, total: 7443 }
表示: $67.67 $6.76 $74.43
Number.isSafeInteger(total): true
金額計算のチェックリスト
| チェック項目 | 推奨 |
|---|---|
| 金額の持ち方 | 最小単位(円・セント)の整数 |
| 率(税率・割引率) | 10(%)や 7/10 のような整数比で持つ |
| 丸めのタイミング | 明細ごとか合計か、仕様で決めて1か所で丸める |
| 丸めの方向 | 切り捨て・四捨五入・切り上げを仕様で明記 |
| 負の金額(返金など) | Math.trunc と Math.floor のどちらか決める |
| 大きな合計 | Number.isSafeInteger() で確認、超えるなら BigInt |
| 割り算・按分が多い | decimal.js / big.js を使う |
| 表示 | Intl.NumberFormat(文字列)で、計算には戻さない |
| サーバー・DBとの受け渡し | 文字列か整数で送る(DB側は DECIMAL 型など) |
同じ金額をフロントエンドとサーバーの両方で計算する場合は、丸めの方向と位置を両方で同じにしておく と、表示と保存値がそろいます。Java 側の扱いは既存記事「Java BigDecimalの使い方」で解説しています。
よくある質問
Q. toFixed を使えば誤差は解決しますか?
A. 表示の桁をそろえる用途には使えますが、(1.005).toFixed(2) が "1.00" になるように、保存値の誤差は残ります。四捨五入の結果を見た目どおりにしたいなら、本文の roundHalfUp か、Intl.NumberFormat に文字列を渡す方法、decimal.js を使います。
Q. Math.round(x * 100) / 100 は使ってもよいですか?
A. 大半の値では期待どおりですが、1.005 や 1.255 のような境界の値で外れます。境界が問題にならない用途(グラフ表示など)なら手軽な選択肢です。金額や採点など境界が大事な計算では、指数表記シフトの関数かライブラリを使うと確実です。
Q. TypeScript なら誤差は起きませんか?
A. TypeScript の number も実行時は JavaScript の Number なので、同じ誤差が起きます。型で区別したい場合は、金額を type Cents = number & { __brand: "Cents" } のようなブランド型にして「整数で持つ」ことを型で表す方法があります。
Q. 偶数丸め(最近接偶数への丸め)はいつ使いますか?
A. 半分ちょうどを常に切り上げると、大量に合計したときに上方向へ偏ります。統計や集計で偏りを小さくしたいときに偶数丸めを使います。表示だけなら Intl.NumberFormat の roundingMode: "halfEven"、計算なら decimal.js の ROUND_HALF_EVEN が手軽です。
まとめ
- 誤差の原因は、10進数の小数を2進数の53ビットで近似していること(0.1 は 0.1000000000000000055511… として保存)
(1.005).toFixed(2)が"1.00"、Math.round(-2.5)が-2になるのは仕様どおりの動作- 比較は相対誤差の
nearlyEqual()で。単純な< Number.EPSILONは大きな値で外れる - 丸めは指数表記シフトのコピペ関数で、負の数・偶数丸めまで見た目どおりにできる
- 表示は
Intl.NumberFormatのroundingMode(Node 22 で9種類を確認)、文字列で渡すとより確実 - 金額は最小単位の整数+整数比の率+最後に1回丸める。割り算が多ければ decimal.js / big.js
- 2^53 を超える整数は BigInt、APIのIDは文字列で受け渡す
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptの小数計算誤差と丸め|toFixedの落とし穴と正しい対処法」
株式会社RJCでは、数値計算の正確な扱い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。