Должны ли 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 и Марку Дикинсону за исправления)