Должны ли IEEE (двоичные) правила округления использоваться для точных десятичных входных данных?

Скажи я набираю номер 1.20515 до 4 десятичных разрядов в IEEE-совместимых языках (C, Java и т. д.) с использованием правила округления до половины по умолчанию, результатом будет "1,2051", который не является четным.

Я думаю, что это связано с тем, что 1.20515 слегка смещен в сторону 1.2051 при хранении в двоичном коде, поэтому в двоичном пространстве даже нет связи.

Однако, если вход 1.20515 является точным в десятичных числах, не является ли это округление на самом деле неправильно?

Редактировать:

Что я действительно хочу знать, так это то, что я не хочу использовать точную десятичную арифметику (например, Java BigDecimal), будут ли эти правила двоичного округления вводить смещение в рабочий процесс: точное десятичное число в строке (максимум 6 dp) -> разбор по IEEE double -> округление с использованием правил IEEE до 4 dp

Изменить 2:

"Точный десятичный" ввод генерируется Java с использованием BigDecimal или же String это приходит непосредственно из базы данных. К сожалению, форматирование должно выполняться в JavaScript, в котором отсутствует достаточная поддержка для правильного округления (и я собираюсь реализовать некоторые из них).

1 ответ

Вы правы: 1.20515 не может быть представлен двоичным кодом IEEE754, поэтому двоичное десятичное преобразование -> округляется до ближайшего значения, равного 1.2051499999999999435118525070720352232456207275390625.

Стандарт IEEE754 на самом деле ничего не говорит о округлении двоичных значений до нецелых десятичных дробей (округление до ближайшего целого числа не страдает от этой проблемы), и поэтому любая такая функциональность соответствует стандарту языка (если он выбирает определить это). JavaScripttoFixed четко определяет его как точное математическое значение (т.е. 1.2051).

(ОБНОВЛЕНИЕ: фактически, стандарт IEEE754 определяет, как должны выполняться преобразования FP -> строк, см. Комментарий Стивена Кэнона ниже).

Однако, если вы хотите правильно округлить весь конвейер, вы можете вместо этого сделать

function roundeven(x) {
    return Math.sign(x)*((Math.abs(x) + 4.503599627370496e15) - 4.503599627370496e15);
}

roundeven(Math.round(parseFloat(s)*1e6)/1e2)/1e4;

который будет работать до тех пор, пока s имеет менее 16 цифр (т. е. абсолютное значение меньше 109).

Почему это так?

  • Math.round(parseFloat(s)*1e6) является точным: это потому, что binary64 может правильно выполнить обход до 15 десятичных цифр, и это, по сути, делает то же самое, масштабируя до целочисленного значения.
  • разделительный 1e2 будет включать некоторое округление (поскольку не все значения являются точно представимыми), но важно, что оно может (i) точно представлять значения с дробной половиной, и (ii) не округлять любые другие значения до дробной половины (так как у нас все еще есть менее 16 десятичных цифр).
  • roundeven осуществляет округление связей до четного целого числа. Эта реализация действительна для любого значения в вышеуказанном диапазоне.
  • окончательное деление снова будет включать некоторое округление, но значения будут ближайшими к правильным десятичным значениям, поэтому преобразование обратно в строку (при необходимости, скажем, через _.toFixed(2)) даст правильный результат.

(спасибо bill.cn и Марку Дикинсону за исправления)

Другие вопросы по тегам