6

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

2 Answers2

5

Not sure if it works for absolutely all languages/locales:

const [main, decimal] = "32_012_012_012_012_312.09".split(".")
const mainNumber = BigInt(main.replaceAll('_', ''))
const decimalNumber = Number.parseFloat('0.' + decimal)

const start = mainNumber.toLocaleString('de-DE') // "32.012.012.012.012.312"
const end = decimalNumber.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }) // "0,09 €"

const result = start + end.substr(1) // "32.012.012.012.012.312,09 €"
Šimon Kocúrek
  • 1,591
  • 1
  • 12
  • 22
  • 1
    it's definitely a good start. a counterexample would be as simple as the `en-US` locale: `start = "32,012,012,012,012,312"`, `end = "€0.09"`, `result = "32,012,012,012,012,3120.09"`. so I guess this problem needs to be split to even more parts: 1. locale-format the number before the decimal point, 2. locale-format the decimal part, 3. currency-format the merged `string`. this still needs a library that handles big numbers in strings, it's just OK for it to specialize in currency formatting (like `currency.js`, but it doesn't handle big numbers as well) – Richard Trembecký Aug 24 '21 at 09:45
4

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.