Округление вверх или вниз, когда 0,5 - PullRequest
3 голосов
/ 11 марта 2019

У меня проблема с тем, как Javascript округляет числа при нажатии 0,5. Я пишу калькуляторы сборов и замечаю расхождение в 0,1с в результатах.

Проблема в том, что для них результат 21480.705, который мое приложение переводит в 21480.71, тогда как тариф говорит: 21480.70.

Это то, что я вижу с Javascript:

(21480.105).toFixed(2)
"21480.10"
(21480.205).toFixed(2)
"21480.21"
(21480.305).toFixed(2)
"21480.31"
(21480.405).toFixed(2)
"21480.40"
(21480.505).toFixed(2)
"21480.51"
(21480.605).toFixed(2)
"21480.60"
(21480.705).toFixed(2)
"21480.71"
(21480.805).toFixed(2)
"21480.81"
(21480.905).toFixed(2)
"21480.90"

Вопросы:

  • Что, черт возьми, происходит с этой ошибкой?
  • Какой самый быстрый и простой способ получить «округленный» результат (при достижении 0,5)?

Ответы [ 5 ]

1 голос
/ 11 марта 2019

Так как некоторые другие уже объяснили, причина «ошибочного» округления - это проблема точности с плавающей запятой. Вы можете исследовать это, используя toExponential() метод числа JavaScript.

(21480.905).toExponential(20)
#>"2.14809049999999988358e+4"
(21480.805).toExponential(20)
#>"2.14808050000000002910e+4"

Как вы можете видеть здесь 21480.905, получает двойное представление, которое немного меньше 21480.905, в то время как 21480.805 получает двойное представление, немного большее, чем исходное значение. Поскольку метод toFixed() работает с двойным представлением и не имеет представления о вашем первоначальном предполагаемом значении, он делает все, что может и должен, с информацией, которую он имеет.

Один из способов обойти это - сдвинуть десятичную точку на необходимое вам десятичное число путем умножения, затем использовать стандарт Math.round(), а затем снова сдвинуть десятичную точку обратно, либо делением, либо умножением на обратное значение. , Затем, наконец, мы вызываем метод toFixed(), чтобы убедиться, что выходное значение заполнено нулями.

var x1 = 21480.905;
var x2 = -21480.705;

function round_up(x,nd)
{
  var rup=Math.pow(10,nd);
  var rdwn=Math.pow(10,-nd); // Or you can just use 1/rup
  return (Math.round(x*rup)*rdwn).toFixed(nd)
}
function round_down(x,nd)
{
  var rup=Math.pow(10,nd);
  var rdwn=Math.pow(10,-nd); 
  return (Math.round(x*-rup)*-rdwn).toFixed(nd)
}

function round_tozero(x,nd)
{
   return x>0?round_down(x,nd):round_up(x,nd) 
}



console.log(x1,'up',round_up(x1,2));
console.log(x1,'down',round_down(x1,2));
console.log(x1,'to0',round_tozero(x1,2));

console.log(x2,'up',round_up(x2,2));
console.log(x2,'down',round_down(x2,2));
console.log(x2,'to0',round_tozero(x2,2));

Наконец: Обнаружение подобной проблемы - это обычно хорошее время, чтобы сесть и долго думать о том, действительно ли вы используете правильный тип данных для вашей проблемы. Так как ошибки с плавающей запятой могут накапливаться при итеративном вычислении, и поскольку люди иногда странно чувствительны в отношении денег, магически исчезающих / появляющихся в ЦП, возможно, вам было бы лучше хранить денежные счетчики в целых «центах» (или некоторых других хорошо продуманных структура), а не с плавающей точкой «доллар».

1 голос
/ 11 марта 2019

Это работает в большинстве случаев.(См. Примечание ниже.)

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

function round(value, decimals) {
  return Number(Math.round(value+'e'+decimals)+'e-'+decimals);
}

console.log(round(21480.105, 2).toFixed(2));

Найдено в http://www.jacklmoore.com/notes/rounding-in-javascript/

ПРИМЕЧАНИЕ: Как указал Марк Дикинсон, это не общее решение, поскольку оно возвращаетNaN в некоторых случаях, например, round(0.0000001, 2) и с большими входами.Поправки, чтобы сделать это более надежным, приветствуются.

0 голосов
/ 11 марта 2019

Что, черт возьми, происходит с этой ошибкой?

Пожалуйста, обратитесь к предостережению Mozilla Doc , в котором указана причина этих расхождений. «Числа с плавающей точкой не могут точно представлять все десятичные числа в двоичном формате, что может привести к неожиданным результатам ...»

Также, пожалуйста, укажите Не работает ли математика с плавающей запятой? (Спасибо Робби Корнелиссену за справку)

Какой самый быстрый и простой способ получить «округленный» результат (при достижении 0,5)?

Используйте библиотеку JS, например, accounting.js , чтобы округлить, отформатировать и представить валюту.

Например ...

function roundToNearestCent(rawValue) {
  return accounting.toFixed(rawValue, 2);
}

const roundedValue = roundToNearestCent(21480.105);
console.log(roundedValue);
<script src="https://combinatronics.com/openexchangerates/accounting.js/master/accounting.js"></script>

Также рассмотрите возможность проверки BigDecimal в JavaScript .

Надеюсь, это поможет!

0 голосов
/ 11 марта 2019

Почему -

Возможно, вы слышали, что в некоторых языках, таких как JavaScript, числа с дробной частью вызывают числа с плавающей запятой, а числа с плавающей запятой имеют дело с приближением числовых операций. Не точные расчеты, приближения. Потому что как именно вы рассчитываете вычислить и сохранить 1/3 или квадратный корень из 2 с точными вычислениями?

Если нет, то теперь вы слышали об этом.

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

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

Решение -

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

Используйте целые числа. Это точно. Добавьте дробную точку при преобразовании чисел в строку.

0 голосов
/ 11 марта 2019

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

function round(n, digits = 2) {
  // rounding to an integer is accurate in more cases, shift left by "digits" to get the number of digits behind the comma
  const str = "" + Math.round(n * 10 ** digits);

  return str
    .padStart(digits + 1, "0") // ensure there are enough digits, 0 -> 000 -> 0.00
    .slice(0, -digits) + "." + str.slice(-digits); // add a comma at "digits" counted from the end
}
...