Нестрогий парсинг

Нестрогий парсинг — механизм обработки строковых данных, при котором допускаются отклонения от идеально ожидаемого формата. В библиотеке Globalize такой подход особенно важен при работе с пользовательским вводом: датами, числами, валютами и процентами, введёнными в локализованной форме.

Строгий парсинг требует полного соответствия шаблону. Нестрогий — допускает вариативность:

  • лишние пробелы;
  • альтернативные разделители;
  • неполное совпадение формата;
  • локальные особенности записи;
  • смешение регистра;
  • сокращённые формы.

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


Проблема строгого парсинга

При строгой проверке даже незначительное отклонение делает строку невалидной.

Пример:

const parseDate = Globalize.dateParser({
  skeleton: "yMd"
});

parseDate("12/31/2025");

Если текущая локаль ожидает формат:

31.12.2025

то строка "12/31/2025" может не распознаться.

Аналогично с числами:

const parseNumber = Globalize.numberParser();

parseNumber("1 000,50");

В одной локали пробел между разрядами допустим, в другой — нет.


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

В Globalize парсеры создаются фабричными функциями:

  • numberParser
  • dateParser
  • currencyParser
  • unitFormatter (косвенно участвует в разборе локализованных данных)

Каждый парсер:

  1. Использует данные CLDR.
  2. Учитывает активную локаль.
  3. Создаёт оптимизированную функцию разбора.

Пример:

const Globalize = require("globalize");

const parseNumber = Globalize.numberParser();

const result = parseNumber("10,5");

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

Globalize не предоставляет отдельный режим "lenient: true" как некоторые другие библиотеки, однако нестрогий разбор достигается несколькими способами:

  • использованием гибких skeleton-форматов;
  • предварительной нормализацией ввода;
  • применением пользовательских обёрток;
  • поддержкой локализованных вариантов записи;
  • использованием tolerant-подходов на уровне приложения.

Нестрогий парсинг чисел

Локализованные разделители

Разные локали используют разные символы:

Локаль Десятичный разделитель Разделитель тысяч
en . ,
ru , пробел
de , .
fr , пробел

Globalize автоматически учитывает это.

Globalize.locale("ru");

const parseNumber = Globalize.numberParser();

parseNumber("10,5");

Результат:

10.5

Обработка пробелов

Во многих локалях числа содержат разделители разрядов.

parseNumber("1 000 000");

Нестрогий подход предполагает предварительную очистку:

function normalizeNumber(value) {
  return value.replace(/\s+/g, "");
}

parseNumber(normalizeNumber("1 000 000"));

Поддержка разных разделителей

Пользователь может вводить:

10.5
10,5

Даже если локаль ожидает только один вариант.

Пример нормализации:

function tolerantNumber(value) {
  return value.replace(",", ".");
}

Комбинированный вариант:

function normalize(value) {
  return value
    .replace(/\s+/g, "")
    .replace(",", ".");
}

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

Стандартный режим

const parseCurrency = Globalize.currencyParser("USD");

parseCurrency("$10.50");

Нестрогий ввод

Проблемные варианты:

10.50$
USD 10.50
10,50 $

Globalize может не обработать такие значения напрямую, поэтому часто применяется промежуточная обработка:

function normalizeCurrency(value) {
  return value
    .replace("USD", "")
    .replace("$", "")
    .trim();
}

После очистки:

parseNumber(normalizeCurrency("USD 10.50"));

Нестрогий парсинг дат

Особенности локализованных дат

Разные страны используют разные схемы:

Страна Формат
США MM/DD/YYYY
Россия DD.MM.YYYY
Япония YYYY/MM/DD

Globalize учитывает локаль:

Globalize.locale("ru");

const parseDate = Globalize.dateParser({
  skeleton: "yMd"
});

Skeleton и гибкость форматов

Skeleton описывает структуру даты без жёсткой привязки к символам-разделителям.

Пример:

{
  skeleton: "yMd"
}

Поддерживаются варианты:

2025-12-31
31.12.2025
31/12/2025

в зависимости от локали.

Это уже делает парсинг более гибким.


Нормализация разделителей

Пользователь может использовать:

31-12-2025
31/12/2025
31.12.2025

Предобработка:

function normalizeDate(value) {
  return value.replace(/[-/]/g, ".");
}

Удаление лишних пробелов

function cleanDate(value) {
  return value.trim();
}

Комбинация:

function prepareDate(value) {
  return value
    .trim()
    .replace(/[-/]/g, ".");
}

Нестрогий парсинг времени

Проблема:

9:5
09:05
9.05

Решение:

function normalizeTime(value) {
  return value.replace(".", ":");
}

Пользовательские tolerant-парсеры

Обёртка над numberParser

Практический подход — создание безопасного универсального парсера.

function createLenientNumberParser(globalize) {
  const parser = globalize.numberParser();

  return function(value) {
    if (typeof value !== "string") {
      return NaN;
    }

    const normalized = value
      .trim()
      .replace(/\s+/g, "")
      .replace(",", ".");

    return parser(normalized);
  };
}

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

const parse = createLenientNumberParser(Globalize);

parse("1 000,50");

Безопасная обработка ошибок

function safeParse(parser, value) {
  try {
    return parser(value);
  } catch (e) {
    return null;
  }
}

Проверка результата

Важно помнить: некоторые ошибки не вызывают исключение, а возвращают NaN.

const result = parseNumber("abc");

if (Number.isNaN(result)) {
  console.log("Некорректное число");
}

Работа с CLDR

Почему CLDR важен для нестрогого парсинга

Unicode Consortium поддерживает CLDR — набор локализационных данных, который используется Globalize.

CLDR содержит:

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

Благодаря этому Globalize способен корректно интерпретировать локализованные данные без ручного описания всех форматов.


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

const Globalize = require("globalize");

Globalize.load(
  require("cldr-data/main/ru/numbers.json"),
  require("cldr-data/main/ru/ca-gregorian.json"),
  require("cldr-data/supplemental/likelySubtags.json"),
  require("cldr-data/supplemental/numberingSystems.json")
);

Смешанные локали

Типичная проблема

Пользователь с русской локалью может вводить:

10.5

вместо:

10,5

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

Практический подход:

function universalNumberParser(globalize) {
  const parser = globalize.numberParser();

  return function(value) {
    const variants = [
      value,
      value.replace(",", "."),
      value.replace(".", ",")
    ];

    for (const variant of variants) {
      const result = parser(variant);

      if (!Number.isNaN(result)) {
        return result;
      }
    }

    return NaN;
  };
}

Нестрогий парсинг и UX

Почему строгий режим ухудшает интерфейс

Строгая проверка:

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

Tolerant-first подход

Современные интерфейсы:

  1. принимают максимально широкий ввод;
  2. нормализуют данные;
  3. приводят к локальному стандарту;
  4. только затем валидируют.

Автокоррекция ввода

Пример:

function smartNormalize(value) {
  return value
    .replace(/\s+/g, "")
    .replace(/[‐-‒–—]/g, "-")
    .replace(",", ".");
}

Ограничения нестрогого парсинга

Неоднозначные даты

Строка:

01/02/03

может означать:

  • 1 февраля 2003;
  • 2 января 2003;
  • 2001-02-03.

Полностью нестрогий режим здесь опасен.


Ложные совпадения

Слишком мягкая нормализация может привести к ошибкам:

"1,2,3"

После удаления запятых:

123

что уже меняет смысл данных.


Потеря локальной специфики

Чрезмерная универсальность иногда разрушает преимущества локализации.

Например:

1.234

может означать:

  • одну тысячу двести тридцать четыре;
  • 1.234.

Стратегии безопасного tolerant-парсинга

Многоэтапная обработка

Надёжная схема:

  1. Очистка строки.
  2. Нормализация символов.
  3. Попытка локального парсинга.
  4. Попытка альтернативных форматов.
  5. Финальная валидация.

Явная локаль

Globalize.locale("de");

Никогда не следует полагаться только на системные настройки браузера.


Логирование ошибок

function parseWithLog(parser, value) {
  const result = parser(value);

  if (Number.isNaN(result)) {
    console.warn("Ошибка парсинга:", value);
  }

  return result;
}

Интеграция с формами

Обработка input-полей

input.addEventListener("blur", () => {
  const value = input.value;

  const normalized = value
    .trim()
    .replace(/\s+/g, "")
    .replace(",", ".");

  const result = parseNumber(normalized);

  if (!Number.isNaN(result)) {
    input.value = result;
  }
});

Форматирование после парсинга

После успешного разбора значение обычно повторно форматируется:

const formatter = Globalize.numberFormatter({
  minimumFractionDigits: 2
});

input.value = formatter(result);

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

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

Неправильно:

function parse(value) {
  const parser = Globalize.numberParser();

  return parser(value);
}

Кэширование

Правильно:

const parser = Globalize.numberParser();

function parse(value) {
  return parser(value);
}

Практический tolerant-parser

Универсальный пример

function createUniversalParser(globalize) {
  const numberParser = globalize.numberParser();

  return function(value) {
    if (typeof value !== "string") {
      return NaN;
    }

    let normalized = value.trim();

    normalized = normalized.replace(/\s+/g, "");

    const variants = [
      normalized,
      normalized.replace(",", "."),
      normalized.replace(".", ",")
    ];

    for (const variant of variants) {
      try {
        const result = numberParser(variant);

        if (!Number.isNaN(result)) {
          return result;
        }
      } catch (e) {}
    }

    return NaN;
  };
}

Сравнение strict и lenient подходов

Особенность Strict Lenient
Точность высокая средняя
Гибкость низкая высокая
UX хуже лучше
Риск неоднозначности низкий высокий
Интернационализация ограниченная удобная
Устойчивость к ошибкам слабая высокая

Лучшие практики

Не удалять символы без анализа

Опасно:

value.replace(/[^\d]/g, "")

Использовать локальные правила

Globalize.locale("fr");

вместо ручной обработки форматов.


Разделять нормализацию и валидацию

Хорошая архитектура:

raw input
→ normalize
→ parse
→ validate
→ format

Проверять NaN

if (Number.isNaN(result)) {
  // ошибка
}

Избегать чрезмерной магии

Слишком агрессивный tolerant-парсинг делает поведение непредсказуемым.

Пользователь должен понимать:

  • какие данные допустимы;
  • как интерпретируется ввод;
  • почему возникла ошибка.