JavaScriptで数値を最も近い0.05またはその他の増分に丸める方法

小さな硬貨がない国のために通貨や数値を0.05や0.10などの特定の増分に丸める方法を学ぶ

はじめに

カナダの顧客が合計$11.23の商品をショッピングカートに追加しましたが、最終的な請求額は$11.25と表示されています。これはカナダが2013年に1セント硬貨(ペニー)を廃止したため、現金取引は最も近い0.05単位に四捨五入されるからです。あなたのアプリケーションは、顧客が実際に支払う金額と一致する価格を表示する必要があります。

特定の単位に四捨五入することでこの問題を解決できます。数値を最も近いニッケル(0.05)、ダイム(0.10)、またはその他の任意の単位に四捨五入することができます。これは通貨のフォーマット、測定システム、および値が特定の単位に合わせる必要があるあらゆるシナリオに適用されます。

このガイドでは、JavaScriptを使用して数値をカスタム単位に四捨五入する方法を、手動実装と最新のIntl.NumberFormat APIの両方を含めて説明します。

特定の単位に四捨五入する理由

多くの国々は実用的な理由から小額硬貨を廃止しています。これらの硬貨の製造と取り扱いには額面価値以上のコストがかかります。これらの国々での現金取引は、利用可能な最も近い硬貨の単位に四捨五入されます。

現金支払いに0.05単位の四捨五入を使用している国々:

  • カナダ(2013年にペニーを廃止)
  • オーストラリア(1セントと2セント硬貨を廃止)
  • オランダ(最も近い0.05ユーロに四捨五入)
  • ベルギー(最も近い0.05ユーロに四捨五入)
  • アイルランド(最も近い0.05ユーロに四捨五入)
  • イタリア(最も近い0.05ユーロに四捨五入)
  • フィンランド(最も近い0.05ユーロに四捨五入)
  • スイス(最小硬貨は0.05 CHF)

0.10単位の四捨五入を使用している国々:

  • ニュージーランド(2006年に5セント硬貨を廃止)

アメリカ合衆国は2026年初頭までにペニーの製造を停止する計画であり、現金取引には0.05単位の四捨五入が必要になります。

現金の四捨五入は個々の商品ではなく、最終的な取引合計にのみ適用されます。電子決済は物理的な硬貨のやり取りがないため、正確な金額のままです。

通貨以外にも、以下のようなケースでカスタム単位が必要になる場合があります:

  • 特定の精度要件を持つ測定システム
  • 機器の精度に合わせて四捨五入される科学的計算
  • 特定の値にスナップするユーザーインターフェースコントロール
  • 心理的価格ポイントを使用する価格戦略

増分丸めの背後にある数学的概念

増分への丸めとは、その増分の最も近い倍数を見つけることを意味します。この計算式では、数値を増分で割り、最も近い整数に丸め、その後増分を掛け戻します。

roundedValue = Math.round(value / increment) * increment

例えば、11.23を最も近い0.05に丸める場合:

  1. 除算:11.23 / 0.05 = 224.6
  2. 丸め:Math.round(224.6) = 225
  3. 乗算:225 * 0.05 = 11.25

除算により増分を整数のステップに変換します。224.6を225に丸めることで最も近いステップを見つけます。乗算により元のスケールに戻します。

11.27を最も近い0.05に丸める場合:

  1. 除算:11.27 / 0.05 = 225.4
  2. 丸め:Math.round(225.4) = 225
  3. 乗算:225 * 0.05 = 11.25

11.23と11.27はどちらも11.25に丸められます。これは、それらの値が11.25の0.025以内にあるためです。中間点の11.25は11.25のままです。

増分丸めを手動で実装する

任意の数値を指定された増分に丸める関数を作成します:

function roundToIncrement(value, increment) {
  return Math.round(value / increment) * increment;
}

console.log(roundToIncrement(11.23, 0.05)); // 11.25
console.log(roundToIncrement(11.27, 0.05)); // 11.25
console.log(roundToIncrement(11.28, 0.05)); // 11.30
console.log(roundToIncrement(4.94, 0.10)); // 4.90
console.log(roundToIncrement(4.96, 0.10)); // 5.00

これは通貨だけでなく、あらゆる増分に対して機能します:

console.log(roundToIncrement(17, 5)); // 15
console.log(roundToIncrement(23, 5)); // 25
console.log(roundToIncrement(2.7, 0.5)); // 2.5

JavaScriptの浮動小数点演算は小さな精度誤差を引き起こす可能性があります。精度が重要な通貨計算では、値を10の累乗で乗算して整数で処理します:

function roundToIncrementPrecise(value, increment) {
  // 増分の小数点以下の桁数を見つける
  const decimals = (increment.toString().split('.')[1] || '').length;
  const multiplier = Math.pow(10, decimals);

  // 整数に変換
  const valueInt = Math.round(value * multiplier);
  const incrementInt = Math.round(increment * multiplier);

  // 丸めて変換し直す
  return Math.round(valueInt / incrementInt) * incrementInt / multiplier;
}

console.log(roundToIncrementPrecise(11.23, 0.05)); // 11.25

Math.round()は半分上丸め(half-up rounding)を使用します。これは、2つの増分の間の正確に中間にある値が切り上げられることを意味します。一部のアプリケーションでは異なる丸めモードが必要です:

function roundToIncrementWithMode(value, increment, mode = 'halfUp') {
  const divided = value / increment;

  let rounded;
  switch (mode) {
    case 'up':
      rounded = Math.ceil(divided);
      break;
    case 'down':
      rounded = Math.floor(divided);
      break;
    case 'halfDown':
      rounded = divided % 1 === 0.5 ? Math.floor(divided) : Math.round(divided);
      break;
    case 'halfUp':
    default:
      rounded = Math.round(divided);
      break;
  }

  return rounded * increment;
}

console.log(roundToIncrementWithMode(11.225, 0.05, 'halfUp')); // 11.25
console.log(roundToIncrementWithMode(11.225, 0.05, 'halfDown')); // 11.20
console.log(roundToIncrementWithMode(11.23, 0.05, 'up')); // 11.25
console.log(roundToIncrementWithMode(11.23, 0.05, 'down')); // 11.20

roundingIncrementを使用したIntl.NumberFormat

最新のブラウザでは、Intl.NumberFormatroundingIncrementオプションがサポートされており、数値のフォーマット時に増分丸めを自動的に処理します。

カナダドルの5セント丸めの基本的な使用法:

const formatter = new Intl.NumberFormat('en-CA', {
  style: 'currency',
  currency: 'CAD',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 5
});

console.log(formatter.format(11.23)); // CA$11.25
console.log(formatter.format(11.27)); // CA$11.25
console.log(formatter.format(11.28)); // CA$11.30

roundingIncrementオプションは特定の値のみを受け付けます:

  • 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000

0.05単位の丸めには、maximumFractionDigits: 2roundingIncrement: 5を設定します。0.10単位の丸めには、maximumFractionDigits: 1roundingIncrement: 1を設定するか、maximumFractionDigits: 2roundingIncrement: 10を設定します。

スイスフランの0.05単位への丸め:

const chfFormatter = new Intl.NumberFormat('de-CH', {
  style: 'currency',
  currency: 'CHF',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 5
});

console.log(chfFormatter.format(8.47)); // CHF 8.45
console.log(chfFormatter.format(8.48)); // CHF 8.50

ニュージーランドドルの0.10単位への丸め:

const nzdFormatter = new Intl.NumberFormat('en-NZ', {
  style: 'currency',
  currency: 'NZD',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 10
});

console.log(nzdFormatter.format(15.94)); // $15.90
console.log(nzdFormatter.format(15.96)); // $16.00

roundingIncrementを異なる丸めモードと組み合わせることができます:

const formatterUp = new Intl.NumberFormat('en-CA', {
  style: 'currency',
  currency: 'CAD',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingIncrement: 5,
  roundingMode: 'ceil'
});

console.log(formatterUp.format(11.21)); // CA$11.25 (切り上げ)

利用可能な丸めモード:

  • ceil: 正の無限大に向かって丸める
  • floor: 負の無限大に向かって丸める
  • expand: ゼロから離れる方向に丸める
  • trunc: ゼロに向かって丸める
  • halfCeil: 半分を正の無限大に向かって丸める
  • halfFloor: 半分を負の無限大に向かって丸める
  • halfExpand: 半分をゼロから離れる方向に丸める(デフォルト)
  • halfTrunc: 半分をゼロに向かって丸める
  • halfEven: 半分を偶数に丸める(銀行家の丸め)

roundingIncrementの重要な制約:

  • minimumFractionDigitsmaximumFractionDigitsは同じ値でなければならない
  • 有効数字の丸めと組み合わせることはできない
  • roundingPriorityauto(デフォルト)の場合のみ機能する

ブラウザがroundingIncrementをサポートしているかどうかを確認する:

function supportsRoundingIncrement() {
  try {
    const formatter = new Intl.NumberFormat('en-US', {
      roundingIncrement: 5,
      minimumFractionDigits: 2,
      maximumFractionDigits: 2
    });
    const options = formatter.resolvedOptions();
    return options.roundingIncrement === 5;
  } catch (e) {
    return false;
  }
}

if (supportsRoundingIncrement()) {
  // roundingIncrementを使用したIntl.NumberFormatを使用
} else {
  // 手動丸めにフォールバック
}

実践的な実装例

ロケールに基づいて現金の丸め処理を行う通貨フォーマッターを作成します:

function createCashFormatter(locale, currency) {
  // 通貨ごとの現金丸め処理ルールを定義
  const cashRoundingRules = {
    'CAD': { increment: 5, digits: 2 },
    'AUD': { increment: 5, digits: 2 },
    'EUR': { increment: 5, digits: 2 }, // 一部のユーロ圏諸国
    'CHF': { increment: 5, digits: 2 },
    'NZD': { increment: 10, digits: 2 }
  };

  const rule = cashRoundingRules[currency];

  if (!rule) {
    // 特別な丸め処理は不要
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency
    });
  }

  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency,
    minimumFractionDigits: rule.digits,
    maximumFractionDigits: rule.digits,
    roundingIncrement: rule.increment
  });
}

const cadFormatter = createCashFormatter('en-CA', 'CAD');
console.log(cadFormatter.format(47.83)); // CA$47.85

const usdFormatter = createCashFormatter('en-US', 'USD');
console.log(usdFormatter.format(47.83)); // $47.83 (丸め処理なし)

現金と電子決済の両方を処理します:

function formatPaymentAmount(amount, currency, locale, paymentMethod) {
  if (paymentMethod === 'cash') {
    const formatter = createCashFormatter(locale, currency);
    return formatter.format(amount);
  } else {
    // 電子決済は正確な金額を使用
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency
    }).format(amount);
  }
}

console.log(formatPaymentAmount(11.23, 'CAD', 'en-CA', 'cash')); // CA$11.25
console.log(formatPaymentAmount(11.23, 'CAD', 'en-CA', 'card')); // CA$11.23

実際に請求する金額を計算します:

function calculateCashTotal(items, currency) {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);

  // 最終合計にのみ現金丸め処理を適用
  const cashRoundingRules = {
    'CAD': 0.05,
    'AUD': 0.05,
    'CHF': 0.05,
    'NZD': 0.10
  };

  const increment = cashRoundingRules[currency];

  if (!increment) {
    return subtotal;
  }

  return roundToIncrement(subtotal, increment);
}

function roundToIncrement(value, increment) {
  return Math.round(value / increment) * increment;
}

const items = [
  { price: 5.99 },
  { price: 3.49 },
  { price: 1.75 }
];

console.log(calculateCashTotal(items, 'CAD')); // 11.25
console.log(calculateCashTotal(items, 'USD')); // 11.23

通貨以外の値に対して増分丸め処理を使用します:

// 0.5度単位で丸めた温度表示
function formatTemperature(celsius) {
  const rounded = roundToIncrement(celsius, 0.5);
  return new Intl.NumberFormat('en-US', {
    style: 'unit',
    unit: 'celsius',
    minimumFractionDigits: 1,
    maximumFractionDigits: 1
  }).format(rounded);
}

console.log(formatTemperature(22.3)); // 22.5°C
console.log(formatTemperature(22.6)); // 22.5°C

// 増分にスナップされるスライダー値
function snapToGrid(value, gridSize) {
  return roundToIncrement(value, gridSize);
}

console.log(snapToGrid(47, 5)); // 45
console.log(snapToGrid(53, 5)); // 55

重要な考慮事項とエッジケース

現金の丸め処理は、個々の明細項目ではなく、取引の最終合計にのみ適用してください。各項目を個別に丸めると、合計を丸めた場合と異なる結果になります:

const items = [1.23, 1.23, 1.23];

// 誤り:各項目を丸める
const wrongTotal = items
  .map(price => roundToIncrement(price, 0.05))
  .reduce((sum, price) => sum + price, 0);
console.log(wrongTotal); // 3.75

// 正しい:合計を丸める
const correctTotal = roundToIncrement(
  items.reduce((sum, price) => sum + price, 0),
  0.05
);
console.log(correctTotal); // 3.70

丸められた金額をユーザーに表示しますが、データベースには正確な金額を保存してください。これにより会計の精度が保たれ、丸めのルールを変更しても情報が失われません:

const transaction = {
  items: [
    { id: 1, price: 5.99 },
    { id: 2, price: 3.49 },
    { id: 3, price: 1.75 }
  ],
  subtotal: 11.23, // 正確な値を保存
  currency: 'CAD',
  paymentMethod: 'cash'
};

// 表示金額を計算
const displayAmount = roundToIncrement(transaction.subtotal, 0.05); // 11.25

同じ通貨を使用する異なる国では、現金の丸めルールが異なる場合があります。ユーロは現金の丸めを行う国と行わない国の両方で使用されています。通貨だけでなく、特定の国の規制を確認してください。

負の金額は絶対値に対して丸めを行い、その後負の符号を適用します:

console.log(roundToIncrement(-11.23, 0.05)); // -11.25
console.log(roundToIncrement(-11.22, 0.05)); // -11.20

非常に大きな数値は浮動小数点演算で精度が失われる可能性があります。大きな金額を扱う金融アプリケーションでは、最小通貨単位での整数演算を検討してください:

function roundToIncrementCents(cents, incrementCents) {
  return Math.round(cents / incrementCents) * incrementCents;
}

// セント単位で処理
const amountCents = 1123; // $11.23
const roundedCents = roundToIncrementCents(amountCents, 5); // 1125
const dollars = roundedCents / 100; // 11.25

丸め実装をエッジケースでテストしてください:

// 0.05丸めのテストケース
const testCases = [
  { input: 0.00, expected: 0.00 },
  { input: 0.01, expected: 0.00 },
  { input: 0.02, expected: 0.00 },
  { input: 0.025, expected: 0.05 },
  { input: 0.03, expected: 0.05 },
  { input: 0.99, expected: 1.00 },
  { input: -0.03, expected: -0.05 },
  { input: 11.225, expected: 11.25 }
];

testCases.forEach(({ input, expected }) => {
  const result = roundToIncrement(input, 0.05);
  console.log(`${input} -> ${result} (expected ${expected})`);
});

Intl.NumberFormatroundingIncrementオプションは最新のブラウザでサポートされています。Firefoxはバージョン93でサポートを追加しました。古いブラウザをサポートするアプリケーションでは、フォールバックを実装してください:

function formatCurrency(amount, locale, currency, increment) {
  try {
    const formatter = new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: currency,
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
      roundingIncrement: increment
    });

    // roundingIncrementが実際に適用されたかチェック
    if (formatter.resolvedOptions().roundingIncrement === increment) {
      return formatter.format(amount);
    }
  } catch (e) {
    // roundingIncrementがサポートされていない
  }

  // フォールバック:手動での丸め
  const rounded = roundToIncrement(amount, increment / 100);
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency: currency
  }).format(rounded);
}

丸め動作をユーザー向けインターフェースで明確に文書化してください。ユーザーは表示される金額が項目の合計と異なる理由を理解する必要があります:

<div class="cart-summary">
  <div class="line-item">小計: $11.23</div>
  <div class="line-item total">合計 (現金): $11.25</div>
  <div class="note">現金支払いは最も近い$0.05に丸められます</div>
</div>

異なる法域では、丸めの恩恵を受ける側(顧客または販売者)やエッジケースの処理方法に関する規制が異なります。特定のユースケースについては、現地の規制を確認してください。