The Problem
After our e-commerce frontend captures an order, it gets pushed to SAP. We frequently saw decimal-rounding discrepancies because our frontend's rounding differed from SAP's.
In fact, most programming languages and banks use this rounding mode (IEEE 754).
Why Banker's Rounding Exists
Banker's rounding mainly aims to ensure precision and correctness in monetary calculations. It's a stricter rounding rule designed to minimize cumulative bias. It's commonly used in financial systems and banking, where any tiny error can compound. Banker's rounding keeps results consistent across different computing environments.
Mnemonic
Round down on 4 or below, round up on 6 or above; for 5, look at what follows: non-zero rounds up, zero looks at the digit before — even rounds down, odd rounds up.
Frontend Code
export const numberFormat = (
num: number,
decimal: number = 2, // 小數幾位
isZeroPadding: boolean = false, // 缺項補零
isNeedThousandComma: boolean = false, // 千分位
roundType = RoundType.EvenRound
) => {
try {
let result;
if (num === undefined) {
return num;
}
if (isNaN(num)) {
return num.toString();
}
let newNumber;
const sign = Math.sign(num);
// 進位
switch (roundType) {
// 四捨五入
case RoundType.Round:
newNumber = (Math.round(Math.abs(num) * Math.pow(10, decimal)) / Math.pow(10, decimal)) * sign;
break;
// 無條件進位
case RoundType.Celi:
newNumber = Math.ceil(num * Math.pow(10, decimal)) / Math.pow(10, decimal);
break;
// 無條件捨去
case RoundType.Floor:
newNumber = parseFloat(num.toFixed(decimal));
break;
case RoundType.EvenRound:
newNumber = evenRound(num, decimal);
break;
default:
newNumber = num;
}
// add Comma
let newNumberStr = newNumber.toString();
if (isZeroPadding) {
if (newNumberStr.indexOf('.') === -1) {
newNumberStr += '.';
}
const numberArray = newNumberStr.split('.');
let x1 = numberArray[0];
if (isNeedThousandComma) {
const rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
}
let x2 = numberArray.length > 1 ? numberArray[1] : '';
// 缺項補零
while (x2.length < decimal) {
x2 += '0';
}
if (decimal > 0) {
x2 = '.' + x2;
}
result = x1 + x2;
return result;
}
return newNumberStr;
} catch (e) {
return num.toString();
}
};
function evenRound(num: number, decimalPlaces: number) {
const d = decimalPlaces || 0;
const m = Math.pow(10, d);
const n = +(d ? num * m : num).toFixed(8); // Avoid rounding errors
const i = Math.floor(n),
f = n - i;
const e = 1e-8; // Allow for rounding errors in f
const r = f > 0.5 - e && f < 0.5 + e ? (i % 2 === 0 ? i : i + 1) : Math.round(n);
return d ? r / m : r;
}
export const moneyFormat = (price: number) => {
return `$${numberFormat(price, 2, true, true)}`;
};
Frontend Unit Tests (Jest)
import { numberFormat, RoundType } from '@/lib/format';
test('event-round-1', () => {
const result = parseFloat(numberFormat(1.964, 2, false, false, RoundType.EvenRound));
const answer = 1.96;
expect(result).toBe(answer);
});
test('event-round-2', () => {
const result = parseFloat(numberFormat(1.9651, 2, false, false, RoundType.EvenRound));
const answer = 1.97;
expect(result).toBe(answer);
});
test('event-round-3', () => {
const result = parseFloat(numberFormat(1.965, 2, false, false, RoundType.EvenRound));
const answer = 1.96;
expect(result).toBe(answer);
});
test('event-round-4', () => {
const result = parseFloat(numberFormat(1.935, 2, false, false, RoundType.EvenRound));
const answer = 1.94;
expect(result).toBe(answer);
});
test('event-round-5', () => {
const result = parseFloat(numberFormat(1.966, 2, false, false, RoundType.EvenRound));
const answer = 1.97;
expect(result).toBe(answer);
});
Automated Testing (requires Jest plugin in VS Code)
Very convenient — for core business code, two clicks tells you whether a method got broken.

Bonus — Banker's Rounding in C# Backend
// 銀行家演算法進位
Math.Round(1.965,2); // 1.96
// 四捨五入
Math.Round(1.965,2, MidpointRounding.AwayFromZero,); // 1.97





























Comments