11

For formatting a number according to locale, there is a standard JavaScript API: Intl.NumberFormat

But for the reverse action, parsing a string to a number I cannot find any standard API supporting locales:

Is there really no JavaScript standard API to parse a string to a number according to a locale?

And if not: are there any market established, open source libraries to do so?

Min-Soo Pipefeet
  • 1,478
  • 1
  • 10
  • 26
  • afaik, no. what else beside converting a comma decimal point to a period would need done? – dandavis Mar 26 '19 at 19:37
  • 1
    Not standard, but you might look at https://github.com/Brightspace/intl – p.s.w.g Mar 26 '19 at 19:40
  • 1
    @dandavis Group separator, e.g. Maybe other things, I don't know because I don't know every locale on earth. That's why I'd like to have a standardized and properly maintained localization functionality. – Min-Soo Pipefeet Mar 26 '19 at 19:55
  • well JS has never been able to parse grouped digits or leading char like `$`, which is why i asked... – dandavis Mar 26 '19 at 20:24
  • See [this related post](https://stackoverflow.com/q/25645163/8583692). – Mahozad Dec 24 '21 at 15:20

6 Answers6

13

The NPM package d2l-intl provides a locale-sensitive parser.

const { NumberFormat, NumberParse } = require('d2l-intl');
const formatter = new NumberFormat('es');
const parser = new NumberParse('es');
const number = 1234.5;
console.log(formatter.format(number));                 // 1.234,5
console.log(parser.parse(formatter.format(1234.5)));   // 1234.5

Unfortunately, that library only comes with support for a handful of locales out of the box. It also uses parseInt which only supports Western Arabic numerals, so for locales that use different numeral systems, you're going to have to get more clever. Here's one solution I found by Mike Bostock. I don't want to take credit for it, but I've reproduced it here for posterity (with some slight tweaks based on my own preferences):

class NumberParser {
  constructor(locale) {
    const format = new Intl.NumberFormat(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]));
    this._group = new RegExp(`[${parts.find(d => d.type === "group").value}]`, "g");
    this._decimal = new RegExp(`[${parts.find(d => d.type === "decimal").value}]`);
    this._numeral = new RegExp(`[${numerals.join("")}]`, "g");
    this._index = d => index.get(d);
  }
  parse(string) {
    return (string = string.trim()
      .replace(this._group, "")
      .replace(this._decimal, ".")
      .replace(this._numeral, this._index)) ? +string : NaN;
  }
}

const formatter = new Intl.NumberFormat('ar-EG');
const parser = new NumberParser('ar-EG');
console.log(formatter.format(1234.5));               // ١٬٢٣٤٫٥
console.log(parser.parse(formatter.format(1234.5))); // 1234.5
p.s.w.g
  • 141,205
  • 29
  • 278
  • 318
2

"A JavaScript library for internationalization and localization that leverage the official Unicode CLDR JSON data. The library works both for the browser and as a Node.js module."

https://github.com/globalizejs/globalize

Andrea
  • 107
  • 1
  • 10
2

Try Mike Bostock's coercion of the Intl.NumberFormat tool into a parser.

Credit: https://observablehq.com/@mbostock/localized-number-parsing

ES6:

class NumberParser {
  constructor(locale) {
    const parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
    const numerals = [...new Intl.NumberFormat(locale, {useGrouping: false}).format(9876543210)].reverse();
    const index = new Map(numerals.map((d, i) => [d, i]));
    this._group = new RegExp(`[${parts.find(d => d.type === "group").value}]`, "g");
    this._decimal = new RegExp(`[${parts.find(d => d.type === "decimal").value}]`);
    this._numeral = new RegExp(`[${numerals.join("")}]`, "g");
    this._index = d => index.get(d);
  }
  parse(string) {
    return (string = string.trim()
      .replace(this._group, "")
      .replace(this._decimal, ".")
      .replace(this._numeral, this._index)) ? +string : NaN;
  }
}

ES5:

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

    function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }

    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

    var NumberParser = (function () {
        function NumberParser(locale) {
            _classCallCheck(this, NumberParser);

            var parts = new Intl.NumberFormat(locale).formatToParts(12345.6);
            var numerals = [].concat(_toConsumableArray(new Intl.NumberFormat(locale, { useGrouping: false }).format(9876543210))).reverse();
            var index = new Map(numerals.map(function (d, i) {
            return [d, i];
            }));
            this._group = new RegExp("[" + parts.find(function (d) {
            return d.type === "group";
            }).value + "]", "g");
            this._decimal = new RegExp("[" + parts.find(function (d) {
            return d.type === "decimal";
            }).value + "]");
            this._numeral = new RegExp("[" + numerals.join("") + "]", "g");
            this._index = function (d) {
            return index.get(d);
            };
        }

        _createClass(NumberParser, [{
            key: "parse",
            value: function parse(string) {
            return (string = string.trim().replace(this._group, "").replace(this._decimal, ".").replace(this._numeral, this._index)) ? +string : NaN;
            }
        }]);

        return NumberParser;
    })();
Jim Morrison
  • 1,961
  • 1
  • 18
  • 25
1

A solution just for thought:

const decimalDigits = /\p{Decimal_Number}/u;

function replaceDecimalDigits(string) {
  return string.replace(new RegExp(decimalDigits.source, 'ug'), function (digit) {
    // The next code is using the property that decimal digits in unicode go one after another, the ranges also do not follow each other, except ranges in 0x1D7CE-0x1D799:
    let value = -1;
    do {
      value += 1;
      digit = String.fromCodePoint(digit.codePointAt(0) - 1);
    } while (decimalDigits.test(digit));
    return value % 10;
  }).replace(/[٫,\.]/g, '.').replace(/[٬]/g, '');
};

console.log(replaceDecimalDigits('١٬٢٣٤٫٥'));

May be, the cache can be generated as well...

4esn0k
  • 8,765
  • 7
  • 30
  • 40
1

for de-DE I parse it like this:

function parseNumber(number: string): number {
  number = number
    .split('.').join('_')
    .split(',').join('.')
    .split('_').join(',')
  return parseFloat(number);
}
Paul Coch
  • 11
  • 1
0

This library tries to handle all locales. It retrieves string which contains a number and it tries to "guess" which culture is the number from and converts it to number.

https://www.npmjs.com/package/number-parsing

Use it in a following way:

var parser = require("number-parsing");
var a = parser("123'123.99USD"); // will return 123123.99
var b = parser("1234"); // will return 1234
var c = parser("123 123,777") // will return 123123.777
// and so on
michal.jakubeczy
  • 6,418
  • 1
  • 47
  • 54