Mark Ku's Blog

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. TypeScript Jest tests for Banker's Rounding in Visual Studio Code

Bonus — Banker's Rounding in C# Backend

// 銀行家演算法進位
Math.Round(1.965,2); // 1.96

// 四捨五入
Math.Round(1.965,2, MidpointRounding.AwayFromZero,); // 1.97

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··490

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··333

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··264

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··221

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··215

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11