Преобразование строк в числа

При работе с интернационализированными приложениями числовые значения часто поступают в виде строк. Пользователь может вводить данные в формате своей локали:

  • 1,234.56 — формат для английской локали;
  • 1 234,56 — формат для французской;
  • 1.234,56 — формат для немецкой;
  • ١٢٣٤٫٥٦ — арабские цифры.

Стандартный parseFloat() в JavaScript не учитывает локализацию. Он не умеет корректно интерпретировать:

  • локализованные разделители дробной части;
  • группировку разрядов;
  • альтернативные наборы цифр;
  • символы валют;
  • формат процентов.

Библиотека Globalize решает эту проблему через механизм локализованного парсинга.


Подключение модулей для работы с числами

Для преобразования строк в числа необходимы:

  • ядро Globalize;
  • CLDR-данные;
  • модуль чисел.

Пример подключения:

<script src="cldr.js"></script>
<script src="cldr/event.js"></script>
<script src="cldr/supplemental.js"></script>

<script src="globalize.js"></script>
<script src="globalize/number.js"></script>

Загрузка CLDR-данных

Globalize использует данные Unicode CLDR. Без них парсинг невозможен.

Пример загрузки:

Globalize.load(
    {
        main: {
            en: {
                numbers: {
                    defaultNumberingSystem: "latn"
                }
            }
        }
    }
);

На практике обычно подключают JSON-файлы:

const enNumbers = require("cldr-data/main/en/numbers.json");
const likelySubtags = require("cldr-data/supplemental/likelySubtags.json");

Globalize.load(enNumbers);
Globalize.load(likelySubtags);

Создание экземпляра локали

После загрузки данных создаётся объект локали:

const globalize = new Globalize("en");

Либо:

Globalize.locale("en");

Метод parseNumber()

Основной инструмент преобразования строк в числа — parseNumber().

Синтаксис:

globalize.parseNumber(value);

Пример:

const globalize = new Globalize("en");

const result = globalize.parseNumber("1234.56");

console.log(result);

Результат:

1234.56

Парсинг с учётом локали

Главное преимущество Globalize — автоматическое понимание локальных форматов.

Английская локаль

const en = new Globalize("en");

console.log(
    en.parseNumber("1,234.56")
);

Результат:

1234.56

Немецкая локаль

В немецком формате:

  • запятая — дробный разделитель;
  • точка — разделитель тысяч.
const de = new Globalize("de");

console.log(
    de.parseNumber("1.234,56")
);

Результат:

1234.56

Французская локаль

const fr = new Globalize("fr");

console.log(
    fr.parseNumber("1 234,56")
);

Результат:

1234.56

Отличие от parseFloat()

Поведение parseFloat()

parseFloat("1.234,56");

Результат:

1.234

Парсинг прекращается после запятой.


Поведение Globalize

const de = new Globalize("de");

de.parseNumber("1.234,56");

Результат:

1234.56

Поддержка группировки разрядов

Globalize корректно распознаёт разделители тысяч.

const en = new Globalize("en");

console.log(
    en.parseNumber("10,000,000")
);

Результат:

10000000

Парсинг отрицательных чисел

const en = new Globalize("en");

console.log(
    en.parseNumber("-1,250.75")
);

Результат:

-1250.75

Преобразование целых чисел

const en = new Globalize("en");

console.log(
    en.parseNumber("5000")
);

Результат:

5000

Парсинг дробных чисел

const en = new Globalize("en");

console.log(
    en.parseNumber("98.75")
);

Результат:

98.75

Работа с арабскими цифрами

Globalize умеет распознавать альтернативные системы записи чисел.

Пример арабской локали:

const ar = new Globalize("ar");

console.log(
    ar.parseNumber("١٢٣٤٫٥٦")
);

Результат:

1234.56

Некорректные значения

Если строка не может быть интерпретирована как число, результатом будет NaN.

const en = new Globalize("en");

console.log(
    en.parseNumber("hello")
);

Результат:

NaN

Проверка:

const value = en.parseNumber("hello");

if (isNaN(value)) {
    console.log("Ошибка преобразования");
}

Парсинг процентов

Globalize поддерживает процентные форматы.

Создание парсера процентов

const en = new Globalize("en");

const parsePercent =
    en.numberParser({ style: "percent" });

console.log(
    parsePercent("50%")
);

Результат:

0.5

Парсинг локализованных процентов

const fr = new Globalize("fr");

const parsePercent =
    fr.numberParser({ style: "percent" });

console.log(
    parsePercent("25 %")
);

Результат:

0.25

Парсинг валют

Globalize умеет преобразовывать строки валют в числовые значения.

Создание валютного парсера

const en = new Globalize("en");

const parseCurrency =
    en.numberParser({
        style: "currency",
        currency: "USD"
    });

console.log(
    parseCurrency("$1,250.75")
);

Результат:

1250.75

Особенности валютного парсинга

Парсер учитывает:

  • символ валюты;
  • положение символа;
  • локальные разделители;
  • пробелы;
  • формат отрицательных сумм.

Пример для евро:

const de = new Globalize("de");

const parseCurrency =
    de.numberParser({
        style: "currency",
        currency: "EUR"
    });

console.log(
    parseCurrency("1.250,75 €")
);

Результат:

1250.75

Метод numberParser()

parseNumber() удобен для простых случаев, однако numberParser() предоставляет больше контроля.

Синтаксис:

const parser =
    globalize.numberParser(options);

После создания функцию можно использовать многократно.


Повторное использование парсера

const en = new Globalize("en");

const parser = en.numberParser();

console.log(parser("100"));
console.log(parser("200"));
console.log(parser("300"));

Преимущество предварительно созданного парсера

Создание парсера требует подготовки шаблонов и анализа CLDR-данных. При интенсивной обработке выгоднее создавать парсер один раз.

Плохо:

for (const value of values) {
    globalize.parseNumber(value);
}

Лучше:

const parser =
    globalize.numberParser();

for (const value of values) {
    parser(value);
}

Опции numberParser()

style

Определяет формат данных.

Поддерживаемые значения:

  • "decimal"
  • "percent"
  • "currency"

currency

Используется при style: "currency".

const parser = globalize.numberParser({
    style: "currency",
    currency: "USD"
});

Обработка пользовательского ввода

Типичный сценарий:

const userInput =
    inputElement.value;

const parser =
    globalize.numberParser();

const number =
    parser(userInput);

if (isNaN(number)) {
    showError();
}

Очистка строк перед парсингом

Хотя Globalize умеет обрабатывать локализованные форматы, иногда требуется предварительная очистка:

const value =
    input.trim();

const number =
    parser(value);

Парсинг данных из API

Некоторые API возвращают локализованные строки:

{
    "price": "1.234,56 €"
}

Обработка:

const parser =
    de.numberParser({
        style: "currency",
        currency: "EUR"
    });

const price =
    parser(data.price);

Валидация числового ввода

Globalize удобно использовать совместно с пользовательской валидацией.

function validatePrice(value) {

    const result =
        parser(value);

    return !isNaN(result);
}

Парсинг в формах

form.addEventListener("submit", event => {

    const value =
        amountInput.value;

    const amount =
        parser(value);

    if (isNaN(amount)) {

        event.preventDefault();

        alert("Некорректное число");
    }
});

Работа с пробелами

В некоторых локалях используются неразрывные пробелы.

Например:

"1 234,56"

Globalize корректно интерпретирует такие значения.


Ограничения parseNumber()

Метод не предназначен для:

  • математических выражений;
  • экспоненциальных форм;
  • нестандартных пользовательских шаблонов;
  • вычислений.

Пример:

globalize.parseNumber("5 + 5");

Результат:

NaN

Сравнение parseNumber() и numberParser()

Возможность parseNumber() numberParser()
Простое использование Да Да
Настройка параметров Нет Да
Повторное использование Нет Да
Производительность Ниже Выше
Поддержка валют Ограниченная Полная
Поддержка процентов Ограниченная Полная

Типичные ошибки

Использование без CLDR

const globalize =
    new Globalize("en");

globalize.parseNumber("100");

Ошибка:

E_MISSING_CLDR

Неверная локаль

const de = new Globalize("de");

de.parseNumber("1,234.56");

Результат будет некорректным, поскольку строка не соответствует немецкому формату.


Игнорирование NaN

const value =
    parser(userInput);

save(value);

Без проверки можно сохранить некорректное значение.


Практический пример

Полноценный парсер цен

Globalize.locale("fr");

const globalize =
    new Globalize("fr");

const parsePrice =
    globalize.numberParser({
        style: "currency",
        currency: "EUR"
    });

function convertPrice(text) {

    const result =
        parsePrice(text);

    if (isNaN(result)) {
        throw new Error(
            "Некорректная цена"
        );
    }

    return result;
}

console.log(
    convertPrice("1 250,50 €")
);

Результат:

1250.5

Производительность

При большом количестве преобразований рекомендуется:

  1. Загружать только нужные локали.
  2. Кэшировать парсеры.
  3. Не создавать Globalize внутри циклов.
  4. Использовать предварительно созданные функции.

Пример кэширования:

const parsers = {};

function getParser(locale) {

    if (!parsers[locale]) {

        parsers[locale] =
            new Globalize(locale)
                .numberParser();
    }

    return parsers[locale];
}

Интеграция с Intl

Globalize дополняет возможности встроенного Intl.

Intl.NumberFormat умеет форматировать числа:

const formatter =
    new Intl.NumberFormat("de-DE");

console.log(
    formatter.format(1234.56)
);

Результат:

1.234,56

Однако встроенного механизма обратного преобразования строк в числа у Intl нет. Именно эту задачу решает Globalize.


Поддержка различных форматов

Globalize корректно работает с:

  • десятичными числами;
  • процентами;
  • валютами;
  • локализованными разделителями;
  • различными системами цифр;
  • отрицательными значениями;
  • форматами CLDR.

Архитектура парсинга в Globalize

Внутри библиотеки процесс состоит из нескольких этапов:

  1. Анализ CLDR-данных.
  2. Построение регулярных выражений.
  3. Определение символов локали.
  4. Замена локализованных символов.
  5. Преобразование в JavaScript Number.

Схематично:

Строка
   ↓
Анализ локали
   ↓
Нормализация
   ↓
Удаление группировки
   ↓
Замена дробного разделителя
   ↓
Number

Использование parserFactory

Внутри Globalize применяется механизм фабрики парсеров.

const parser =
    globalize.numberParser();

Функция создаётся заранее и затем выполняет быстрый разбор строк без повторного анализа CLDR.


Поддержка scientific notation

Стандартный парсер ориентирован на локализованные пользовательские числа, а не на инженерную запись.

Пример:

globalize.parseNumber("1e3");

Поведение зависит от локали и конфигурации, поэтому scientific notation обычно обрабатывают отдельно.


Безопасность преобразования

При обработке пользовательского ввода важно:

  • проверять NaN;
  • ограничивать диапазоны;
  • валидировать валюты;
  • исключать неожиданные символы.

Пример:

const value =
    parser(input);

if (isNaN(value)) {
    throw new Error("Ошибка");
}

if (value < 0) {
    throw new Error("Отрицательное число");
}