Как округлять числа вверх, вниз или до ближайшего значения

Управляйте тем, как JavaScript округляет десятичные числа при форматировании с разными режимами округления

Введение

Когда вы форматируете числа для отображения, часто нужно округлять десятичные значения. Например, цена $2.567 превращается в $2.57. Измерение 3.891 метра может отображаться как 4 метра. То, как вы округляете эти числа, влияет на точность, ожидания пользователей и бизнес-логику.

В разных ситуациях нужны разные стратегии округления. Иногда нужно округлить вверх, чтобы не занижать стоимость товара. Иногда — округлить вниз, чтобы уложиться в бюджет. Чаще всего округляют до ближайшего значения для сохранения точности.

В JavaScript есть девять разных режимов округления через API Intl.NumberFormat. Эти режимы определяют, как округляются числа, если они попадают между двумя возможными значениями. В этом уроке разбираются три самых популярных режима округления и объясняется, когда использовать остальные.

Как JavaScript округляет числа по умолчанию

Если вы форматируете число без указания режима округления, JavaScript использует стратегию halfExpand. В этом режиме значения округляются до ближайшего возможного, а если число находится ровно посередине между двумя значениями, оно округляется в сторону от нуля.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0
});

console.log(formatter.format(2.4));
// Output: "2"

console.log(formatter.format(2.5));
// Output: "3"

console.log(formatter.format(2.6));
// Output: "3"

Значение 2.4 ближе к 2, чем к 3, поэтому округляется вниз до 2. Значение 2.6 ближе к 3, чем к 2, поэтому округляется вверх до 3. Значение 2.5 находится ровно посередине между 2 и 3, поэтому режим halfExpand округляет его в сторону от нуля — до 3.

Такое поведение по умолчанию соответствует тому, чему учат в школе и что ожидают в повседневных расчетах. Оно равномерно распределяет ошибки округления по множеству вычислений, поэтому подходит для общего форматирования чисел.

Что значит округление от нуля

Фраза «от нуля» описывает направление округления для чисел, которые находятся ровно посередине между двумя возможными значениями. Для положительных чисел округление от нуля — это округление вверх. Для отрицательных — округление вниз, к большему по модулю значению.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0
});

console.log(formatter.format(2.5));
// Output: "3"

console.log(formatter.format(-2.5));
// Output: "-3"

И 2.5, и -2.5 округляются от нуля. Для 2.5 это значит — к положительной бесконечности, получается 3. Для -2.5 — к отрицательной бесконечности, получается -3. В обоих случаях модуль увеличивается.

Округление вверх с помощью ceil

Режим округления ceil всегда округляет к положительной бесконечности. Для положительных чисел это округление вверх. Для отрицательных — к нулю.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "ceil"
});

console.log(formatter.format(2.1));
// Output: "3"

console.log(formatter.format(2.9));
// Output: "3"

console.log(formatter.format(-2.1));
// Output: "-2"

console.log(formatter.format(-2.9));
// Output: "-2"

Этот режим полезен, когда нужно быть уверенным, что число не окажется меньше необходимого. Например, если коробка вмещает 2.3 предмета, понадобится 3 коробки. Если задача занимает 1.1 дня, нужно планировать 2 дня.

const boxFormatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "ceil"
});

const itemsPerBox = 5;
const totalItems = 12;
const boxesNeeded = totalItems / itemsPerBox;

console.log(boxFormatter.format(boxesNeeded));
// Output: "3"

Вычисление даёт 2.4 коробки, но нельзя заказать часть коробки. Округление вверх гарантирует, что вместимости хватит.

Округление вниз с помощью floor

Режим округления floor всегда округляет к отрицательной бесконечности. Для положительных чисел это округление вниз. Для отрицательных — от нуля.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "floor"
});

console.log(formatter.format(2.1));
// Output: "2"

console.log(formatter.format(2.9));
// Output: "2"

console.log(formatter.format(-2.1));
// Output: "-3"

console.log(formatter.format(-2.9));
// Output: "-3"

Этот режим полезен, когда нужны консервативные оценки или важно не выйти за пределы. Например, если бюджет составляет $100.87, можно отобразить $100, чтобы случайно не потратить больше.

const budgetFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0,
  roundingMode: "floor"
});

const availableBudget = 100.87;

console.log(budgetFormatter.format(availableBudget));
// Output: "$100"

Округление вниз гарантирует, что отображаемая сумма всегда достижима при реальном бюджете.

Округление к ближайшему с halfExpand

Хотя halfExpand используется по умолчанию, вы можете явно указать его в коде, чтобы сделать намерение понятным. Этот режим округляет к ближайшему значению, а в случае середины — от нуля.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 1,
  roundingMode: "halfExpand"
});

console.log(formatter.format(2.14));
// Output: "2.1"

console.log(formatter.format(2.15));
// Output: "2.2"

console.log(formatter.format(2.16));
// Output: "2.2"

Значение 2.14 ближе к 2.1, поэтому оно округляется до 2.1. Значение 2.16 ближе к 2.2, поэтому оно округляется до 2.2. Значение 2.15 находится ровно посередине, поэтому оно округляется от нуля до 2.2.

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

Комбинирование режимов округления с дробными знаками

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

const ceilFormatter = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingMode: "ceil"
});

console.log(ceilFormatter.format(10.001));
// Output: "$10.01"

console.log(ceilFormatter.format(10.999));
// Output: "$11.00"

При двух знаках после запятой 10.001 округляется либо до 10.00, либо до 10.01. Режим ceil округляет вверх, давая 10.01. Значение 10.999 округляется вверх до 11.00.

Округление к нулю с trunc

Режим округления trunc округляет к нулю, то есть просто убирает дробную часть числа. Для положительных чисел это округление вниз, для отрицательных — вверх.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "trunc"
});

console.log(formatter.format(2.1));
// Output: "2"

console.log(formatter.format(2.9));
// Output: "2"

console.log(formatter.format(-2.1));
// Output: "-2"

console.log(formatter.format(-2.9));
// Output: "-2"

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

Округление от нуля с expand

Режим округления expand округляет от нуля. Для положительных чисел это округление вверх. Для отрицательных — округление вниз, увеличивая модуль числа.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "expand"
});

console.log(formatter.format(2.1));
// Output: "3"

console.log(formatter.format(2.9));
// Output: "3"

console.log(formatter.format(-2.1));
// Output: "-3"

console.log(formatter.format(-2.9));
// Output: "-3"

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

Как работают режимы округления на середине

Пять режимов, начинающихся с half, всегда округляют до ближайшего значения, но по-разному обрабатывают случаи, когда число ровно посередине. Эти режимы позволяют точно управлять поведением при равных значениях между двумя возможными результатами.

Режим halfCeil округляет значения, находящиеся посередине, в сторону положительной бесконечности.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "halfCeil"
});

console.log(formatter.format(2.5));
// Output: "3"

console.log(formatter.format(-2.5));
// Output: "-2"

Режим halfFloor округляет значения, находящиеся посередине, в сторону отрицательной бесконечности.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "halfFloor"
});

console.log(formatter.format(2.5));
// Output: "2"

console.log(formatter.format(-2.5));
// Output: "-3"

Режим halfTrunc округляет значения, находящиеся посередине, к нулю.

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "halfTrunc"
});

console.log(formatter.format(2.5));
// Output: "2"

console.log(formatter.format(-2.5));
// Output: "-2"

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

const formatter = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "halfEven"
});

console.log(formatter.format(2.5));
// Output: "2"

console.log(formatter.format(3.5));
// Output: "4"

console.log(formatter.format(4.5));
// Output: "4"

Значение 2.5 округляется до 2, потому что 2 — чётное. Значение 3.5 округляется до 4, потому что 4 — чётное. Значение 4.5 также округляется до 4, потому что 4 — чётное.

Как выбрать режим округления для вашего приложения

Выбор режима округления зависит от бизнес-требований и характера отображаемых данных. В разных ситуациях нужны разные стратегии.

Используйте ceil, когда нужно обеспечить достаточность. Для планирования ёмкости, подсчёта запасов и оценки времени часто требуется округление вверх, чтобы гарантировать наличие необходимых ресурсов.

Используйте floor, когда нужно оставаться в пределах лимитов. Для отображения бюджета, отслеживания квот и расчёта скидок часто требуется округление в меньшую сторону, чтобы не превысить доступные ресурсы.

Используйте halfExpand для обычного отображения, когда важна точность, но сверхвысокая точность не критична. Это значение по умолчанию не просто так — оно отлично подходит для большинства задач форматирования чисел.

Используйте halfEven для финансовых расчётов, когда нужно минимизировать накопление ошибок округления. Этот режим гарантирует, что при большом количестве вычислений ошибки округления не будут постоянно смещаться в одну сторону.

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

Использование режимов округления с форматированием валюты

Режимы округления естественно сочетаются с форматированием валюты. У разных компаний свои правила округления денежных значений, и опция roundingMode позволяет реализовать эти правила.

const retailPrice = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingMode: "ceil"
});

const wholesalePrice = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  roundingMode: "floor"
});

const calculatedPrice = 19.874;

console.log(retailPrice.format(calculatedPrice));
// Output: "$19.88"

console.log(wholesalePrice.format(calculatedPrice));
// Output: "$19.87"

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