JavaScript | 数値リテラルの記述方法

javascrpit JavaScript
スポンサーリンク

1. 丸め誤差とは何か?

JavaScript の Number 型は、内部的に 2進数の浮動小数点数(IEEE 754) で表されています。
つまり、10進数の「0.1」「0.2」などを 正確に 2進で表せません。

例:

0.1 + 0.2 === 0.3   // false 😨
0.1 + 0.2            // 0.30000000000000004
JavaScript

2. どんなときに起こるの?

代表的なパターン:

操作結果
0.1 + 0.20.30000000000000004
0.3 - 0.20.09999999999999998
0.1 * 30.30000000000000004
0.07 * 1007.000000000000001
1.005.toFixed(2)"1.00" 😱(四捨五入がズレる)

3. 対処法まとめ

✅ 方法①:toFixed()で表示を丸める(見た目だけ)

const result = 0.1 + 0.2;
console.log(result.toFixed(2)); // "0.30"
JavaScript

特徴

  • 人に見せる・UIに表示するには十分。
  • ただし戻り値は文字列なので、再計算には使えない。
Number((0.1 + 0.2).toFixed(2)); // => 0.3 (数値として再利用可能)
JavaScript

✅ 方法②:整数にスケーリングして計算する(金融計算の定番)

「円」や「ドル」など小数を扱うときは、小数を整数化 してから計算。

// 例:お金の計算(小数点第2位まで扱う)
const price1 = 0.1 * 100;  // 10
const price2 = 0.2 * 100;  // 20

const total = (price1 + price2) / 100; // 30 / 100 = 0.3
console.log(total); // 0.3 ✅
JavaScript

これで誤差を防げます。
多くの会計システムではこの方法が使われています。

✅ 方法③:許容誤差を使った比較(ε 比較)

「同じ数値か?」を判定したい場合は、少しの誤差を許容します。

const EPSILON = Number.EPSILON; // 2.220446049250313e-16

function nearlyEqual(a, b) {
  return Math.abs(a - b) < EPSILON * Math.max(1, Math.abs(a), Math.abs(b));
}

console.log(nearlyEqual(0.1 + 0.2, 0.3)); // true ✅
JavaScript

または、実務では誤差幅を自分で決めることもあります:

Math.abs(a - b) < 1e-10  // 小数第10位まで同じなら「同じ」とみなす
JavaScript

✅ 方法④:Math.round()で丸める(中間結果を整える)

途中計算で誤差が累積する場合、都度四捨五入してから次の処理をします。

function roundTo(num, digits) {
  const factor = 10 ** digits;
  return Math.round(num * factor) / factor;
}

roundTo(0.1 + 0.2, 2); // 0.3 ✅
JavaScript

✅ 方法⑤:toPrecision() で桁数制御

表示や精度チェックに便利。

(0.1 + 0.2).toPrecision(2); // "0.30"
(1 / 3).toPrecision(4);     // "0.3333"
JavaScript

✅ 方法⑥:ライブラリを使う(正確な小数計算)

丸め誤差のない任意精度計算をするには、ライブラリを使うのが最も確実です。

有名どころ:

  • decimal.js
  • big.js
  • math.js

例:decimal.js

const Decimal = require('decimal.js');

const a = new Decimal(0.1);
const b = new Decimal(0.2);
console.log(a.plus(b).toString()); // "0.3" ✅ 正確
JavaScript

これらは金融や科学計算に最適です。


4. よくある誤差の落とし穴と解決法まとめ表

ケースダメな結果対処法
0.1 + 0.20.30000000000000004.toFixed() または整数計算
0.07 * 1007.000000000000001Math.round(num * 100) / 100
1.005.toFixed(2)“1.00”Math.round(1.005 * 100) / 100
price1 + price2 === 0.3falseMath.abs(a - b) < 1e-10
大きな計算の累積誤差ずれが蓄積中間ごとに roundTo() で丸める

5. 実務でのおすすめ方針

目的おすすめ手法
画面表示.toFixed()
金額計算整数化(セント単位など)
小数点第N位で四捨五入Math.round(num * 10**N) / 10**N
数値比較Math.abs(a - b) < 1e-10
高精度が必要decimal.js or big.js

💡 おまけ:お金の四捨五入関数サンプル

// 小数第2位で四捨五入
function roundMoney(value) {
  return Math.round(value * 100) / 100;
}

console.log(roundMoney(1.005)); // 1.01 ✅
console.log(roundMoney(2.345)); // 2.35 ✅
JavaScript

まとめ

ポイント説明
JavaScript の Number は浮動小数点数→ 0.1, 0.2 などが正確に表せない
表示用には toFixed()ただし戻り値は文字列
計算用には整数化 or 四捨五入
比較には誤差を許容Math.abs(a - b) < 1e-10
高精度計算が必要ならライブラリdecimal.js など

「お金の計算」専用の安全な関数ライブラリ(税率計算や小数第2位の四捨五入込み)

セント単位で安全に計算する実用的なライブラリです。

中には:

  • セント単位で計算する基礎関数(toCents / fromCents / roundToCents
  • 加減乗除(add, subtract, multiply, divide
  • 税抜 → 税額 → 税込(applyTaxExclusive)と、税込 → 税抜(applyTaxInclusive)の関数
  • Intl.NumberFormat を使った formatCurrency(表示用)
  • Node で動かせる簡易サンプル(ファイル末尾)
/**
 * safe-money-utils.js
 *
 * シンプルで安全な「お金」計算ユーティリティ(セント単位で内部計算)
 * - すべて内部は整数(最小通貨単位:セント)で計算して丸め誤差を防ぐ
 * - 小数第2位での四捨五入(銀行丸めや他の丸めルールが必要なら拡張可能)
 *
 * 使い方(例はファイル末尾にまとめてあります)
 */

// ---------- ヘルパー ----------

/**
 * 数値を安全にNumberに変換(null/undefined/空文字 -> 0)
 * @param {any} v
 * @returns {number}
 */
function toNumber(v) {
  if (v === null || v === undefined || v === "") return 0;
  if (typeof v === 'number') return v;
  const n = Number(v);
  return Number.isFinite(n) ? n : 0;
}

/**
 * 与えられた金額(Number)をセント(最小通貨単位:整数)に変換する
 * 四捨五入は「最も一般的な」方式(0.5切り上げ)を使用
 * @param {number|string} amount
 * @returns {number} cents
 */
function toCents(amount) {
  const n = toNumber(amount);
  // 小数第3位以降の誤差を防ぐため、十分大きい倍率で丸めたあと切替
  // ここでは安全のため 1e12 を使って浮動小数点の影響を抑える
  const scaled = Math.round(n * 100 * 1e8) / 1e8; // 中間での浮動誤差を低減
  return Math.round(scaled * 100);
}

/**
 * セントを元のNumber金額に戻す
 * @param {number} cents
 * @returns {number}
 */
function fromCents(cents) {
  return cents / 100;
}

/**
 * 小数第2位で四捨五入した数値(Number)を返す
 * @param {number} amount
 * @returns {number}
 */
function roundToCents(amount) {
  return fromCents(toCents(amount));
}

// ---------- 基本的な演算(セントで処理) ----------

/** 足し算 */
function add(...amounts) {
  const totalCents = amounts.reduce((acc, v) => acc + toCents(v), 0);
  return fromCents(totalCents);
}

/** 引き算 */
function subtract(a, b) {
  return fromCents(toCents(a) - toCents(b));
}

/** 掛け算(乗数は浮動小数点可) */
function multiply(amount, multiplier) {
  const cents = toCents(amount);
  const result = Math.round(cents * toNumber(multiplier));
  return fromCents(result);
}

/** 割り算(除数は非ゼロであること) */
function divide(amount, divisor) {
  if (Number(divisor) === 0) throw new Error('divide by zero');
  const cents = toCents(amount);
  const result = Math.round(cents / toNumber(divisor));
  return fromCents(result);
}

// ---------- 税率計算ユーティリティ ----------
/**
 * 税抜き価格に対して税額と税込金額を計算(税抜 -> 税額 -> 税込)
 * 税額はセント単位で四捨五入
 * @param {number|string} netAmount  税抜き金額(例: 1000 -> 1000.00)
 * @param {number|string} taxRatePercent  税率(%) 例: 10 または 8
 * @returns {{net: number, tax: number, gross: number}}
 */
function applyTaxExclusive(netAmount, taxRatePercent) {
  const netCents = toCents(netAmount);
  const rate = toNumber(taxRatePercent) / 100;
  // 税額 = net * rate を計算 -> セントで丸め
  const taxCents = Math.round(netCents * rate);
  const grossCents = netCents + taxCents;
  return {
    net: fromCents(netCents),
    tax: fromCents(taxCents),
    gross: fromCents(grossCents)
  };
}

/**
 * 税込価格(総額)から税額と税抜金額を逆算(内税 -> 税抜 / 税額)
 * @param {number|string} grossAmount
 * @param {number|string} taxRatePercent
 * @returns {{net: number, tax: number, gross: number}}
 */
function applyTaxInclusive(grossAmount, taxRatePercent) {
  const grossCents = toCents(grossAmount);
  const rate = toNumber(taxRatePercent) / 100;
  // net = gross / (1 + rate)
  const netCents = Math.round(grossCents / (1 + rate));
  const taxCents = grossCents - netCents;
  return {
    net: fromCents(netCents),
    tax: fromCents(taxCents),
    gross: fromCents(grossCents)
  };
}

// ---------- 表示ユーティリティ ----------
/**
 * 金額を通貨形式の文字列に整形(ロケールを指定可能)
 * @param {number|string} amount
 * @param {string} locale e.g. 'en-US', 'ja-JP'
 * @param {string} currency e.g. 'USD', 'JPY'
 */
function formatCurrency(amount, locale = 'ja-JP', currency = 'JPY') {
  const n = toNumber(roundToCents(amount));
  return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(n);
}

// ---------- エクスポート ----------
const SafeMoney = {
  toNumber,
  toCents,
  fromCents,
  roundToCents,
  add,
  subtract,
  multiply,
  divide,
  applyTaxExclusive,
  applyTaxInclusive,
  formatCurrency
};

// CommonJS / ESM の両方に対応する簡易エクスポート
if (typeof module !== 'undefined' && module.exports) {
  module.exports = SafeMoney;
}

if (typeof window !== 'undefined') {
  window.SafeMoney = SafeMoney;
}

export default SafeMoney;

// ---------- サンプル(手元で試す用) ----------
if (typeof require !== 'undefined' && require.main === module) {
  // Node で直接実行したときの簡易テスト
  console.log('=== SafeMoney サンプル ===');
  console.log('add(0.1, 0.2)           =>', SafeMoney.add(0.1, 0.2));
  console.log('roundToCents(1.005)     =>', SafeMoney.roundToCents(1.005));
  console.log('applyTaxExclusive(100, 10) =>', SafeMoney.applyTaxExclusive(100, 10));
  console.log('applyTaxInclusive(110, 10) =>', SafeMoney.applyTaxInclusive(110, 10));
  console.log('formatCurrency(1234.56, "ja-JP", "JPY") =>', SafeMoney.formatCurrency(1234.56, 'ja-JP', 'JPY'));
}
JavaScript
タイトルとURLをコピーしました