Точность и precision

Работа с числами в JavaScript неизбежно сталкивается с особенностями IEEE-754 представления, где дробные значения часто теряют ожидаемую точность. В контексте валидации данных это приводит к необходимости явно ограничивать и контролировать количество знаков после запятой, особенно в финансовых, научных и аналитических данных. В Joi для этих целей используется механизм ограничения точности через метод precision.


Модель представления чисел и проблема точности

Числа с плавающей точкой в JavaScript не являются точными в математическом смысле. Например:

0.1 + 0.2 === 0.3 // false

Результат выражения оказывается false из-за особенностей двоичного представления дробей. Валидационные схемы должны учитывать подобные погрешности, особенно при проверке входных данных, где требуется фиксированное количество знаков после запятой.


Метод precision в Joi

В библиотеке Joi метод precision применяется к числовым схемам и задаёт максимально допустимое количество знаков после десятичной точки.

Сигнатура:

Joi.number().precision(limit)

где limit — целое число, определяющее максимальную точность.


Базовое поведение precision

При использовании precision Joi проверяет количество знаков после запятой и отклоняет значения, превышающие установленный лимит.

const schema = Joi.number().precision(2);

schema.validate(10.12); // корректно
schema.validate(10.123); // ошибка валидации

В данном случае значение 10.123 содержит три знака после запятой и нарушает ограничение.


Валидация и сообщение об ошибке

При нарушении ограничения точности Joi возвращает структурированное сообщение об ошибке:

{
  message: '"value" must have no more than 2 decimal places',
  path: [],
  type: 'number.precision',
  context: {
    limit: 2,
    value: 10.123
  }
}

Тип ошибки number.precision позволяет однозначно идентифицировать причину отклонения значения.


Влияние преобразования типов

Joi может автоматически приводить строки к числам при включённой опции преобразования:

Joi.number().precision(2).validate("10.10");

Строка "10.10" будет преобразована в число 10.1, после чего проверка точности выполнится уже над числовым значением. Это важно учитывать при работе с пользовательским вводом, где данные часто приходят в строковом формате.


Особенности работы с целыми числами

Целые числа не нарушают ограничение точности:

const schema = Joi.number().precision(3);

schema.validate(5); // допустимо
schema.validate(100); // допустимо

Внутренне такие значения не имеют дробной части, поэтому ограничение считается соблюдённым независимо от параметра precision.


Ограничение и поведение при округлении

Joi не выполняет автоматическое округление значений при превышении точности. Значение либо проходит валидацию, либо отклоняется.

Joi.number().precision(2).validate(10.129);

Результат — ошибка, а не преобразование в 10.13. Это принципиальное поведение, позволяющее избежать скрытых модификаций входных данных.


Комбинация с другими числовыми ограничениями

Метод precision часто используется вместе с другими числовыми правилами:

const schema = Joi.number()
  .min(0)
  .max(100)
  .precision(2);

Такая схема ограничивает диапазон значений и одновременно фиксирует формат представления дробной части.


Взаимодействие с safe integers

JavaScript ограничивает безопасные целые числа диапазоном Number.MAX_SAFE_INTEGER. Ограничение точности не влияет на целую часть, но может сочетаться с проверкой:

Joi.number()
  .integer()
  .max(Number.MAX_SAFE_INTEGER);

Если одновременно применяется precision, правило integer фактически делает дробную часть недопустимой, и precision становится избыточным.


Влияние NaN и Infinity

Значения NaN, Infinity и -Infinity не проходят числовую валидацию независимо от настройки точности:

Joi.number().precision(2).validate(NaN);       // ошибка
Joi.number().precision(2).validate(Infinity);  // ошибка

Метод precision применяется только к конечным числовым значениям с фиксированной десятичной частью.


Особенности работы с отрицательными числами

Ограничение точности одинаково применяется к отрицательным значениям:

Joi.number().precision(2).validate(-10.12); // допустимо
Joi.number().precision(2).validate(-10.123); // ошибка

Знак числа не влияет на подсчёт количества знаков после запятой.


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

Числа могут поступать в разных формах записи:

  • обычная десятичная форма
  • экспоненциальная форма

Пример:

Joi.number().precision(2).validate(1e-2);

Значение 1e-2 интерпретируется как 0.01, и точность считается корректной. Однако при экспоненциальной записи внутреннее преобразование может влиять на итоговое количество значащих цифр, что требует осторожности при работе с научными данными.


Строгий режим и precision

В строгом режиме Joi отключает автоматические преобразования типов:

const schema = Joi.number().precision(2).strict();

В этом случае строковые значения даже корректного числового вида не приводятся к числу, а сразу отклоняются:

schema.validate("10.10"); // ошибка
schema.validate(10.10);   // корректно

Это усиливает контроль над точностью входных данных.


Внутренняя логика проверки

Проверка precision основана на анализе десятичного представления числа. Joi определяет количество цифр после точки путём преобразования числа в строку и анализа дробной части.

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

10.1 // может быть представлено как 10.0999999999999

Для таких случаев Joi нормализует значение перед проверкой, минимизируя влияние машинного представления числа.


Ограничения подхода precision

Несмотря на удобство, метод имеет ряд ограничений:

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

В задачах, где критична абсолютная точность (например, валютные операции), часто применяются подходы с использованием целых чисел (копейки вместо рублей) или decimal-библиотек.


Сочетание с кастомной валидацией

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

Joi.number().custom((value, helpers) => {
  const decimalPart = value.toString().split('.')[1];
  if (decimalPart && decimalPart.length > 2) {
    return helpers.error('number.precision');
  }
  return value;
});

Такой подход позволяет учитывать бизнес-правила, выходящие за рамки стандартного precision.


Поведение при сериализации данных

При последующей сериализации валидированных данных точность может изменяться из-за особенностей JSON.stringify и внутреннего представления числа:

JSON.stringify(10.1); // "10.1"
JSON.stringify(10.10); // "10.1"

Таким образом, precision влияет только на этап валидации, но не фиксирует формат хранения данных.


Итоговые свойства механизма precision в Joi

  • ограничивает количество знаков после запятой
  • не выполняет округление значений
  • работает на этапе валидации входных данных
  • совместим с числовыми ограничениями диапазона
  • зависит от особенностей представления чисел в JavaScript
  • может комбинироваться со строгим режимом и кастомными правилами

Механизм точности в Joi представляет собой инструмент контроля формата числовых данных, ориентированный на проверку структуры значения, а не на математическую нормализацию вычислений.