Какова наибольшая точность, которую вы можете потерять, если используете double для преобразования значения валюты в другую валюту и обратно?

Я знаю проблему двойников из-за вычислений по основанию 2:

var total: Double = 0.1
total *= 0.1
total /= 0.1
println(total) // 0.10000000000000002

Я понимаю, что BigDecimal следует использовать для расчетов, связанных с деньгами, но в моем случае небольшая потеря точности приемлема, потому что все расчеты валюты, которые мне нужно сделать, это конвертировать валюту C1 в валюту C2 и обратно в валюту C1. Единственное, что два значения C1 после этого преобразования должны совпадать, когда я показываю результат клиенту с точностью до 5 знаков после запятой.

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


person user1819676    schedule 04.09.2015    source источник
comment
вопрос не о java   -  person Andrew Tobilko    schedule 04.09.2015
comment
Вы можете потерять целочисленную точность, а также небольшую десятичную точность.   -  person Carlos Bribiescas    schedule 04.09.2015
comment
Правила с плавающей запятой часто зависят от языка, и в Java есть некоторые детали. Тег подходит.   -  person chrylis -cautiouslyoptimistic-    schedule 04.09.2015


Ответы (3)


Вы можете прочитать описание двойной точности с плавающей запятой:

11-битная ширина экспоненты позволяет представлять числа с десятичной экспонентой от 10^−308 до 10^308 с полной точностью 15–17 десятичных цифр.

Поскольку вы хотите 5 дробных цифр, это означает, что вы в порядке до макс. 10 целых цифр.

Или вы могли бы протестировать его. Иногда эмпирические данные более убедительны:

double value = 0.00009;
while (true) {
    String nextDown = String.format("%.5f", Math.nextDown(value));
    String nextUp   = String.format("%.5f", Math.nextUp(value));
    if (! nextDown.equals(nextUp))
        break; // loss of precision to 5 decimals
    System.out.printf("Good: %.5f%n", value);
    value = value * 10 + 0.00009;
}
System.out.printf("Bad: %.5f%n", value);

Вывод

Good: 0.00009
Good: 0.00099
Good: 0.00999
Good: 0.09999
Good: 0.99999
Good: 9.99999
Good: 99.99999
Good: 999.99999
Good: 9999.99999
Good: 99999.99999
Good: 999999.99999
Good: 9999999.99999
Good: 99999999.99999
Good: 999999999.99999
Good: 9999999999.99999
Bad: 99999999999.99997

Да, 10 хорошо, 11 плохо.

person Andreas    schedule 04.09.2015
comment
Хотя это верно, это только показывает, что у вас есть определение для представления этой точности. На самом деле вычисления между числами с плавающей запятой гораздо более подвержены неточностям. Например, System.out.println(.0009*999999999.99999); неверно. Вы можете проверить это здесь wolframalpha.com - person Carlos Bribiescas; 09.09.2015
comment
@CarlosBribiescas Ваш пример не теряет точности. Конечно, если ввести это на сайте, который вы указали, всплывающая подсказка покажет 09 в конце, но это будет 16 цифр. Когда вы нажимаете кнопку расчета (=), результат правильно отображается как оканчивающийся на 1 с точностью до 15 цифр. Итак, заявленные 15 разрядов точности не исчерпаны (пока). - person Andreas; 09.09.2015
comment
Но Java говорит, что ответ 899999.9999999909. (Скомпилируйте и запустите эту строку) Wolfram Alpha говорит, что это 899999.999999991. Кроме того, исходный пример, который говорит ОП, также имеет длину в пределах 15. - person Carlos Bribiescas; 09.09.2015
comment
Кроме того, проблема, которую вы описываете, когда подсказка инструмента отличается от фактического ответа, является демонстрацией проблемы. Подсказка инструмента генерируется на лету с двойной точностью. Когда вы нажимаете Enter, он фактически отправляет его на сервер для точной точности. - person Carlos Bribiescas; 09.09.2015
comment
@CarlosBribiescas Да, это хорошо известные ошибки использования математики с двойной точностью, и если вы округлите результат до 15 цифр, вы получите правильный ответ, по крайней мере, в этом случае. Однако вопрос был не общим, а конкретно о сумме из 5 дробных цифр, конвертированной в другую валюту и обратно, в какой момент результат перестанет быть точным до 5 дробных цифр. Мой ответ, что ОП должен быть хорошим до 10 целых цифр, остается в силе. - person Andreas; 09.09.2015

double как в binary64 будет поддерживать 15+ значимых десятичных разрядов при правильном округлении до/от Decimal.

Использование математики с фиксированным разрядом: с 15 значащими десятичными знаками и OP, требующим 5 знаков после запятой, остается 10 цифр слева от десятичной запятой.

x,xxx,xxx,xxx.xxxxx
person chux - Reinstate Monica    schedule 04.09.2015

Я бы порекомендовал никогда и нигде не хранить и не передавать сумму C2. Вместо этого сохраните или перенесите сумму C1 и обменный курс. Таким образом, вы можете вычислить и отобразить количество C2, когда вам это нужно, и у вас всегда будет точное исходное значение C1.

Если вам абсолютно необходимо сохранить C2, а затем пересчитать из него C1, все будет хорошо, если вы используете округление, а не усечение, так как вы рискуете, что пересчитанный результат будет, например. 0,0199999999999. (И, как указал @Andreas в своем ответе, существует ограничение на количество цифр, которые вы можете поддерживать таким образом.)

person Aasmund Eldhuset    schedule 04.09.2015
comment
Проблема в том, что C1 вводится пользователем, а нам нужно хранить все значения в C2. Поэтому, хотя мы и сохраняем ставки, переход из C1 в C2 и обратно в C1 неизбежен. - person user1819676; 04.09.2015
comment
Как насчет хранения C1? - person Aasmund Eldhuset; 04.09.2015