Search code examples
javascriptlocalenumber-formattingbigdecimalcurrency-formatting

How to format a big number (represented by string) in JavaScript with currency and with respect to the locale?


I'm working on a banking app front-end written in TypeScript.

From the back-end, I receive:

  • an amount with 2 decimal places (e.g. account balance) in string. This can potentially be a large value - over 15 digits before the decimal point.
    • this is formatted as: "32012012012012312.09"
  • a currency code string (e.g. "USD")

I'd like to format the amount with thousand separators, a currency symbol, and NBSP/NNBSP handling, all with respect to provided locale. What's the easiest way?

Are there potential solutions to partial problems? E.g. 1. formatting the number and 2. formatting with currency symbol.

What I tried:

  • Intl.NumberFormat - I like it, but it takes a number | bigint (although it doesn't crash with strings), but it stops being precise at large decimal numbers - e.g. it formats 32_012_012_012_012_312.09 as 32,012,012,012,012,310.00 - I believe this is a JavaScript limitation regarding numbers, that's why I'm looking for something handling strings
  • accounting-js - formatMoney function with both string and number - same result and missing functionality of passing the locale in
  • looking for other libraries and threads on this topic - I looked a bit into big/bignumber/decimal.js and numeral.js, haven't tried them, but I believe they are about handling big numbers, not formatting or even locale-based formatting

Solution

  • Using Šimon Kocúrek's advice to format the BigInt part separately and bringing it a step further to still utilize the currency formatting of Intl.NumberFormat in a safer way:

    const locale = 'de-DE'
    const currency = 'EUR'
    const amount = "321321321321321321.357" // parseFloat(c) gives 321321321321321340
    
    // in the comments I also give an example for '321321321321321321.998' because of rounding issue
    const [mainString, decimalString] = amount.split(".") // ['321321321321321321', '.357' | '998']
    
    const decimalFormat = new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
    const decimalFullString = `0.${decimalString}` // '0.357' | '0.998'
    const decimalFullNumber = Number.parseFloat(decimalFullString) // 0.357 | 0.998
    const decimalFullFinal = decimalFormat.format(decimalFullNumber) // '0,36' | '1,00'
    const decimalFinal = decimalFullFinal.slice(1) // ',36' | ',00'
    
    const mainFormat = new Intl.NumberFormat(locale, { minimumFractionDigits: 0 })
    let mainBigInt = BigInt(mainString) // 321321321321321321n
    if (decimalFullFinal[0] === "1") mainBigInt += BigInt(1) // 321321321321321321n | 321321321321321322n
    const mainFinal = mainFormat.format(mainBigInt) // '321.321.321.321.321.321' | '321.321.321.321.321.322'
    
    const amountFinal = `${mainFinal}${decimalFinal}` // '321.321.321.321.321.321,36' | '321.321.321.321.321.322,00'
    
    const currencyFormat = new Intl.NumberFormat(locale, { style: "currency", currency, maximumFractionDigits: 0 })
    const template = currencyFormat.format(0) // '€0'
    const result = template.replace("0", amountFinal) // '€321.321.321.321.321.321,36' | '€321.321.321.321.321.322,00'
    

    Running with:

    • locale = 'fr' produces '321 321 321 321 321 321,36 €' (with NNBSP between trios)
    • locale = 'de' produces '321.321.321.321.321.321,36 €'
    • locale = 'en' produces '€321,321,321,321,321,321.36'

    This also handles string-numbers with more than 2 decimal digits.