javascriptnumber-formatting

Currency formatting using Intl.NumberFormat without currency symbol


I am trying to use Intl as a default currency formatter and it is almost perfect.

Using the example from a Intl.NumberFormat() constructor:

const number = 123456.789;

console.log(new Intl.NumberFormat('de-DE', { style: 'currency',
currency: 'EUR' }).format(number)); // expected output: "123.456,79 €"
 
// the Japanese yen doesn't use a minor unit 
console.log(new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY'
}).format(number)); // expected output: "¥123,457"

This is almost perfect but I would actually like to drop the symbol from the output. So I would expect to see:

// expected output: "123.456,79"
// expected output: "123,457"

I find it bizarre that I spend over an hour looking for a solution and only found some sort of replace/trim usage.

Why there is not an option to format the number with all the Intl power but only dropping the currency symbol?!?

I hope I missed it, tbh.


Solution

  • One simple way to do achieve what you want is to use String#replace() to remove the currency from the string. To make this easier, you can set currencyDisplay to "code" which will use the ISO currency code - the same one passed in to currency:

    const number = 123456.789;
    
    console.log(new Intl.NumberFormat('de-DE', { 
        style: 'currency',
        currency: 'EUR', 
        currencyDisplay: "code" 
      })
      .format(number)
      .replace("EUR", "")
      .trim()
    ); // 123.456,79
     
    // the Japanese yen doesn't use a minor unit 
    console.log(new Intl.NumberFormat('ja-JP', { 
        style: 'currency', 
       currency: 'JPY', 
        currencyDisplay: "code" 
      })
      .format(number)
      .replace("JPY", "")
      .trim()
    ); // 123,457

    This can be extracted into a function:

    const number = 123456.789;
    
    console.log(format('de-DE', 'EUR', number)); // 123.456,79
    console.log(format('ja-JP', 'JPY', number)); // 123,457
    
    function format (locale, currency, number) {
      return new Intl.NumberFormat(locale, { 
        style: 'currency', 
        currency, 
        currencyDisplay: "code" 
      })
      .format(number)
      .replace(currency, "")
      .trim();
    }


    An alternative that allows you more control is to use Intl.NumberFormat#formatToParts() which formats the number but gives you tokens that you can programmatically consume and manipulate. For example, using the method with locale = "de-DE" and currency = "EUR" you get the following output:

    [
      {
        "type": "integer",
        "value": "123"
      },
      {
        "type": "group",
        "value": "."
      },
      {
        "type": "integer",
        "value": "456"
      },
      {
        "type": "decimal",
        "value": ","
      },
      {
        "type": "fraction",
        "value": "79"
      },
      {
        "type": "literal",
        "value": " "
      },
      {
        "type": "currency",
        "value": "EUR"
      }
    ]
    

    Which means that you can easily filter out "type": "currency" and then combine the rest into a string. For example:

    const number = 123456.789;
    
    console.log(format('de-DE', 'EUR', number)); // 123.456,79
    console.log(format('ja-JP', 'JPY', number)); // 123,457
    
    function format (locale, currency, number) {
      return new Intl.NumberFormat(locale, { 
        style: 'currency',
        currency, 
        currencyDisplay: "code",
      })
      .formatToParts(number)
      .filter(x => x.type !== "currency")
      .filter(x => x.type !== "literal" || x.value.trim().length !== 0)
      .map(x => x.value)
      .join("")
    }

    NOTE: the exclusion here: .filter(x => x.type !== "literal" || x.value.trim().length !== 0) handles whitespace characters within the number. That might come up when using the option currencySign: 'accounting' in the formatter. In some locales this will use parentheses for negative numbers which would leave a space inside if just the currency is removed:

    const number = -123456.789;
    
    const parts = new Intl.NumberFormat('ja-JP', { 
        style: 'currency',
        currency: 'JPY', 
        currencySign: "accounting",
        currencyDisplay: "code",
      })
      .formatToParts(number);
      
    console.log(parts); 
    
    /* output:
    [
      { type: "literal" , value: "("    },
      { type: "currency", value: "JPY"  },
      { type: "literal" , value: " "    },
      { type: "integer" , value: "123"  },
      { type: "group"   , value: ","    },
      { type: "integer" , value: "457"  },
      { type: "literal" , value: ")"    }
    ]
    */
    .as-console-wrapper { max-height: 100% !important; }

    Thus negative numbers are handled correctly:

    const number = -123456.789;
    
    console.log(format('de-DE', 'EUR', number)); // 123.456,79
    console.log(format('ja-JP', 'JPY', number)); // 123,457
    
    function format (locale, currency, number) {
      return new Intl.NumberFormat(locale, { 
        style: 'currency',
        currency, 
        currencyDisplay: "code",
        currencySign: "accounting",
      })
      .formatToParts(number)
      .filter(x => x.type !== "currency")
      .filter(x => x.type !== "literal" || x.value.trim().length !== 0)
      .map(x => x.value)
      .join("")
    }

    Thanks to Chris Peckham for pointing out potential pitfalls when using the accounting currency sign option.