3

I am storing international currency amounts in my database as minor units to avoid floating-point arithmetic.

I am also storing the locale in my database to provide the context in order to display a formatted string representation of the amount.

For example, if I have two rows as follows:

529 US-en
529 ja-JP

I want to display on the front-end

$5.29
¥529

I have found the docs for Intl.NumberFormat() but it appears that you have to already have your numbers represented as major units only.

Is there a methodology that anyone has implemented for such a use-case WITHOUT knowing the ratio of minor units to major units for EVERY currency you wanted to support?

Basically is there a way I can just leverage the Intl.NumberFormat() function and have it consider the number I give it to be in minor units and then convert it to major accordingly?

Andrew Eames
  • 108
  • 1
  • 4
  • For anyone who doesn't know why this is an **excellent question**, read: https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency Too bad `Intl.NumberFormat()` doesn't have a `minorUnits: true` option. – Dem Pilafian Jun 20 '21 at 03:26

1 Answers1

0

I have found a solution to this. Two functions are required, one that will parse a currency string from input into minor units and the Dinero JS library to perform formatting and arithmetic on the minor units if so desired.

intl.parseToMinorUnits = function(string, currency) {
  const format = new Intl.NumberFormat(this.locale);
  const parts = format.formatToParts(12345.6);
  const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));
  const index = new Map(numerals.map((d, i) => [d, i]));
  
  const currencyFormat = new Intl.NumberFormat(this.locale, { style: 'currency', currency });
  const currencyParts = currencyFormat.formatToParts(12345.6);

  const _currency = new RegExp(`[${currencyParts.find(d => d.type === "currency").value}]`, "g");
  const _literal = new RegExp(`[${currencyParts.find(d => d.type === "literal") ?? '\s'}]`, "g");
  const _group = new RegExp(`[${parts.find(d => d.type === "group").value}]`, "g");
  const _decimal = new RegExp(`[${parts.find(d => d.type === "decimal").value}]`);
  const _numeral = new RegExp(`[${numerals.join("")}]`, "g");
  const _index = d => index.get(d);

  let parsed = string.trim()
    .replace(_currency, "")
    .replace(_literal, "")
    .replace(_group, "")
    .replace(_decimal, ".")
    .replace(_numeral, _index)

  const number = parsed ? +parsed : NaN

  if (isNaN(number)) {
    return 0
  }
  return +(String(parsed).replace(/\./g, ''))
}

intl.formatCurrency = function(amount, currency = 'USD') {
  const formatted = Dinero({ amount: amount || 0, currency })
    .setLocale(this.locale)
    .toFormat()
  return formatted
}

Andrew Eames
  • 108
  • 1
  • 4