💰

「銀行丸め」を知っているか?~JavaScript編~

2021/08/05に公開
6

経緯

フロントエンドエンジニアとして、自社サービスのキャッシュレスPOSレジ(タブレットアプリ)をReact Nativeで開発しています。
先日、値引按分額を算出する際にデバッグしていたところ「1円がどうしても合わない!!!」と悩んでいました。
調べたところ、「銀行丸め」で計算すればOKとのこと。
※「偶数丸め」とも呼ぶそうです。

銀行丸め とは何か

こちらの記事に詳しく書かれていました。
抜粋すると下記の通り。四捨五入の特殊版です。

「端数が0.5より小さいなら切り捨て、端数が0.5より大きいならば切り上げる。端数がちょうど0.5なら切り捨てと切り上げのうち結果が偶数となる方へ丸める。」

前提

下記のような商品A~Cを会計処理するとする。

★ 問題ない会計パターン

[
 商品A:{
  price:750,
 },
 商品B:{
  price:300,
 },
 商品C{
  price:1180,
 },
 subTotal:2230, //小計
 discount:0, //クーポン値引額
 total:2230, //最終支払額
]

しかし、上記のケースで値引クーポン(会計から400円値引する)がある場合はどうだろうか?
仕様により、「商品ごとに値引額を按分し、先に算出しないといけない」とする。

★ 1円ずれる会計パターン

400円を商品A~Cで按分(値引額 * 商品代金 / 小計)し、itemDiscountに代入
[
 商品A:{
  price:300,
  itemDiscount:54, //按分した単品の値引額
 },
 商品B:{
  price:750,
  itemDiscount:135, //按分した単品の値引額
 },
 商品C:{
  price:1180,
  itemDiscount:212, //按分した単品の値引額
 },
 subTotal:2230,//小計
 discount:401,//クーポン値引額(54+135+212=401)
 total:1829,//最終支払額、正しくは1830
]

discount:401、、、だと!?

※「100円の商品が3つ」で「値引総額100円」の場合は別の問題になります!
そちらはコメント参照

ちなみにitemDiscountの算出方法は
値引額 * 商品代金 / 小計 の四捨五入
つまり商品Aなら、
 400 * 300 / 2230 = 53.811....  → 四捨五入し「54円」

しかし、商品Bでは
 400 * 750/ 2230 = 134.5  → 四捨五入し「135円」となるが、 少数点第一位が.5なので 
「銀行丸め」により「134円」としないと1円オーバーしてしまう!
商品B:{
 price:750,
 itemDiscount:135, //1円オーバーの原因
},

答え

const calcItemDiscountWithCoupon = (orderData) => {
 const apportionmentPrice = 値引額 * 商品代金 / 小計
 //按分後、少数点以下が'.5'の場合に銀行丸めを行う
 if (apportionmentPrice.indexOf('.5') !== -1) {
  //少数点以下を切捨て2で割りきれるか確認
  const floorApportionmentPrice = Math.floor(apportionmentPrice)
  if(floorApportionmentPrice % 2 === 0){
   return floorApportionmentPrice //偶数ならそのまま返す
  } else{
   return Math.ceil(apportionmentPrice)//奇数ならapportionmentPriceを切り上げ
  }
 }
}

これによって、商品Bの値引き額は「134円」になった!!!

この後に「単品でのディスカウントや消費税(8%・10%)」の分岐を踏まえて、
「内税算出(表示義務化が2021年4月施行)」するために必要な工程の一つでしたが、
基本的には上記で「銀行丸め」しました。

Discussion

YutaUraYutaUra

補足ですが、こういった丸め処理のことを偶数丸め(偶数方向への丸めめ)などともいうそうです!

cisdurcisdur

銀行丸めは誤差を小さくするのに役立ちますが、「1円がどうしても合わない!!!」ということが問題である場合、あまり本質的な解決にならないのではないでしょうか。

例として、「100円の商品が3つ」で「値引総額100円」の場合を考えてみてください。各商品の値引按分額は、通常の四捨五入でも銀行丸めでも33円ですので、値引き額の合計は99円になってしまいます。

つえつえ

ご指摘ありがとうございます。確かにその通りでした。
コードを見直したところ、「銀行丸め」をした上で最終的な差を、最後の商品のitemDiscountで調整していました。
なので、下記のようになるかと思います。

const totalDiscount = []  
totalDiscount.push(会計データ.商品.map(v => {
 return v. itemDiscount
}))
if(totalDiscount !== ディスカウントすべき額) {
 最後の商品(商品C)のitemDiscountに過不足(ディスカウントすべき額 - totalDiscount)を足す処理
}

[
 商品A:{
  price:100,
  itemDiscount:33, //按分した単品の値引額
 },
 商品B:{
  price:100,
  itemDiscount:33, //按分した単品の値引額
 },
 商品C:{
  price:100,
  itemDiscount:34, //按分した単品の値引額(別個で計算が必要)
 },
 subTotal:300,//小計
 discount:100,//クーポン値引額(ディスカウントすべき額)
 total:200,//最終支払額
]

みなさま、「銀行丸め」だけで解決できるわけではござまいせん!

KageShironKageShiron

文字列にして.indexOf('.5') !== -1 を見るということかと思いますが、銀行丸めだと.51の時は近い方に丸めるのではないでしょうか。
2.5→2
2.51→3

つえつえ

.5 じゃないものは 普通にmath.round() 四捨五入の処理で大丈夫です。