2013-02-18 2 views
2

Я добавляю общие вычисления на стороне клиента на страницу моего заказа, так что дисконт объема покажет, когда пользователь делает выбор.Будет ли это надежным способом обработки ошибок округления при умножении чисел с плавающей запятой на Javascript?

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

Я знаю, что ошибки округления являются ожидаемым результатом при работе с числами с плавающей запятой. Например, 149,95 * 0,15 = 22,492499999999996 и 149,95 * 0,30 = 44,98499999999999. Первые раунды по желанию, последний - нет.

Я искал эту тему и нашел множество дискуссий, но ничего не удовлетворительно решает проблему.

Мой текущий расчет заключается в следующем:

discount = Math.round(price * factor * 100)/100; 

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

По существу:

discount = Math.round(Math.round(price * 100) * Math.round(factor * 100)/100)/100; 

Я думал о добавлении 0,0001 к числу до округления. Например:

discount = Math.round(price * factor * 100 + 0.0001)/100; 

Это работает для сценариев, которые я пробовал, но мне интересно о моей логике. Будет ли добавление 0,0001 всегда достаточным и не слишком большим, чтобы добиться желаемого результата округления?

Примечание: Для моих целей здесь меня интересует только один расчет за цену (так что не усугубляйте ошибки) и никогда не будет отображаться более двух знаков после запятой.

EDIT: Например, я хочу округлить результат 149.95 * 0.30 до двух знаков после запятой и получить 44.99. Однако, я получаю 44.98, потому что фактический результат 44.98499999999999 не 44.985. Ошибка не вводится / 100. Это происходит до этого.

Тест:

alert(149.95 * 0.30); // yields 44.98499999999999 

Таким образом:

alert(Math.round(149.95 * 0.30 * 100)/100); // yields 44.98 

44,98, как ожидается, с учетом фактического результата умножения, но не желателен, так как это не то, что пользователь будет ожидать (и отличается от результат PHP).

Решение: Я собираюсь преобразовать все в целые числа, чтобы выполнить мои вычисления. Как отмечается в принятом ответе, я могу несколько упростить мой первоначальный расчет. Моя идея добавить 0.0001 - это просто грязный хак. Лучше всего использовать правильный инструмент для работы.

+1

Просто не используйте поплавки на деньги вообще? – delnan

+0

Вы не указали проблему. Вы заявляете, что 44.984999 ... не округляется по желанию, но вы не указываете, к чему это округляется, к чему вы хотите объединить его или как вы показываете значение. Одна из возможностей заключается в том, что '/ 100' в вашей операции округления вводит ошибку округления, которая производит значение, которое вы не хотите, последующее преобразование этого значения для отображения показывает значение, которое вы не хотите. Там могут быть способы исправить это, но вы должны предоставить дополнительную информацию. –

+0

@delnan Что вы используете вместо этого? Я хватаю цены из HTML, которые отображаются пользователю в долларах и центах. Я могу * конвертировать * их в центы, что я и сделаю, но я должен начать с float. – toxalot

ответ

2

Не думаю, что добавление небольшого количества вам понравится, я думаю, что есть случаи, когда это слишком много. Также он должен быть правильно документирован, иначе можно было бы считать его неправильным.

работает в центах [...] потребовало бы мне конвертировать мои стартовые номера, вокруг них, умножать их, вокруг результата, а затем преобразовать его обратно:

discount = Math.round(Math.round(price * 100) * Math.round(factor * 100)/100)/100;

Я думаю, он должен работать, а затем только раунд. Тем не менее, вы должны сначала умножить результат, так что significant digits являются суммой двух сиг цифр от до, то есть 2 + 2 = 4 знака после запятой в вашем примере:

discount = Math.round(Math.round((price * factor) * 10000)/100)/100; 
+0

Конечно, я всегда тщательно документирую все, что заставляет идти? И я даже документирую ваше решение, потому что кто-то может прийти позже и подумать, что его можно упростить до типичного 'Math.round (price * factor * 100)/100'. – toxalot

+0

Необходимы ли дополнительные скобки? Или это будет хорошо: 'discount = Math.round (Math.round (price * factor * 10000)/100)/100;'? – toxalot

+0

Нет, они были просто для дополнительной подсветки. Выражение будет оцениваться слева направо без них. – Bergi

1

Добавление небольшого количества к вашим номерам не будет очень точным. Вы можете попробовать использовать библиотеку, чтобы получить лучшие результаты: https://github.com/jtobey/javascript-bignum.

+0

Это кажется излишним, чтобы рассчитать несколько скидок. Это больше кода, чем мое приложение PHP. – toxalot

+0

Да, это может быть излишним; зависит от того, насколько важно для ваших расчетов быть точным. Если вы делаете дисконтирование, даже работа со значениями пенни может быть неправильной, если фактор включает доли процента. Может быть, достаточно хорошо. –

1

ответ BERĢI показывает решение. Этот ответ показывает математическую демонстрацию, что это правильно. При этом он также устанавливает некоторую оценку того, насколько допустима погрешность ввода.

Ваша задача заключается в следующем:

  • У вас есть число с плавающей точкой, X, который уже содержит ошибки округления. Например, он предназначен для представления 149,95, но фактически содержит 149,94999999999998863131622783839702606201171875.
  • Вы хотите умножить это число с плавающей запятой x на значение скидки d.
  • Вы хотите знать результат умножения на ближайший пенни, выполняемый так, как будто идеальная математика использовалась без ошибок.

Предположим, мы добавим еще два предположения:

  • х всегда представляет собой некоторое точное количество центов. То есть, это число, которое имеет точное число сотых, например 149.95.
  • Ошибка в x небольшая, меньше, чем, скажем, .00004.
  • Соотношение дисконтирования d представляет собой процентное число (то есть также точное число сотых, например 0,25 для 25%) и находится в интервале [0%, 100%].
  • Ошибка d является крошечной, всегда результатом правильного преобразования десятичной цифры с двумя цифрами после десятичной точки в двоичную с плавающей запятой двойной точности (64 бит).

Учитывать значение x*d*10000. В идеале это будет целое число, так как x и d каждый из них идеально кратно .01, поэтому умножение идеального произведения x и d на 10 000 дает целое число. Так как ошибки в x и d малы, то округление x*d*10000 до целого будет производить это идеальное целое число. Например, вместо идеала x и d мы имеем x и d плюс небольшие ошибки, x + e0 и d + e1, и вычисляем (x + e0) • (d + e1) • 10000 = (x • d + х • e1 + д • е0 + е0 • е1) • 10000. Мы предположили, что e1 является крошечным, поэтому доминирующей ошибкой является d • e0 • 10000. Предположим, что e0, ошибка в x, меньше чем .00004, а d не более 1 (100%), поэтому d • e0 • 10000 меньше .4. Эта ошибка, плюс крошечные ошибки от e1, недостаточна для изменения округления x*d*10000 от идеального целого до некоторого другого целого. (Это связано с тем, что ошибка должна быть не менее 0,5 для изменения того, как результат должен быть целым числом раундов. Например, 3 плюс ошибка .5 будет округлять до 4, но 3 плюс .49999 не будут.)

Таким образом, Math.round(x*d*10000) производит желаемое целое число. Тогда Math.round(x*d*10000)/100 - это приблизительное значение x*d*100, точность которого составляет гораздо меньше, чем один цент, поэтому округляя его, Math.round(Math.round(x*d*10000)/100) производит точно количество желаемых центов. Наконец, деление на 100 (для получения нескольких долларов, с сотых, вместо целого числа центов, как целого) создает новую ошибку округления, но ошибка настолько мала, что когда результирующее значение правильно преобразуется в десятичное число с двумя десятичными знаками, отображается правильное значение. (Если дальнейшая арифметика выполняется с этим значением, это может остаться недействительным.)

Из вышеизложенного видно, что если ошибка в x растет до .00005, этот расчет может завершиться неудачей. Предположим, что стоимость ордера может вырасти до 100 000 долларов. Ошибка с плавающей запятой при представлении значения около 100 000 не более 100 000 • 2 -53. Если бы кто-то заказал сто тысяч предметов с этой ошибкой (они не могли, поскольку предметы имели бы меньшие индивидуальные цены, чем 100 000 долларов, поэтому их ошибки будут меньше), а цены были индивидуально добавлены, выполняя сто тысяч (минус один) дополнения добавляющие сто тысяч новых ошибок, то есть почти две сотни тысяч ошибок не более 100000 • 2 -53, так что суммарная погрешность составляет не более 2 • 10 • 10 • 2 -53, который примерно .00000222. Поэтому это решение должно работать для нормальных заказов.

Обратите внимание, что решение требует повторного рассмотрения, если скидка не является целым процентом. Например, если скидка указана как «одна треть» вместо 33%, то x*d*10000 не должно быть целым числом.

Смежные вопросы