Мне всегда говорили никогда не изображать деньги double
и не float
печатать, и на этот раз я задаю вам вопрос: почему?
Я уверен, что есть очень веская причина, я просто не знаю, что это такое.
floating-point
currency
Фрэн Фицпатрик
источник
источник
Ответы:
Потому что числа с плавающей запятой и двойные не могут точно представлять базовые 10 кратных, которые мы используем для денег. Эта проблема не только для Java, но и для любого языка программирования, который использует базовые типы с плавающей точкой.
В базе 10 вы можете написать 10,25 как 1025 * 10 -2 (целое число, умноженное на 10). Числа с плавающей точкой IEEE-754 различны, но очень простой способ думать о них - вместо этого умножить на степень два. Например, вы можете посмотреть на 164 * 2 -4 (целое число, умноженное на степень два), что также равно 10,25. Это не то, как числа представлены в памяти, но математические значения одинаковы.
Даже в базе 10 эта запись не может точно представлять самые простые дроби. Например, вы не можете представить 1/3: десятичное представление повторяется (0,3333 ...), поэтому нет конечного целого числа, которое вы могли бы умножить на степень 10, чтобы получить 1/3. Вы можете рассчитывать на длинную последовательность из 3 и небольшую экспоненту, такую как 333333333 * 10 -10 , но это не совсем точно: если вы умножите это на 3, вы не получите 1.
Тем не менее, для подсчета денег, по крайней мере, для стран, чьи деньги оцениваются в пределах порядка доллара США, обычно все, что вам нужно, это иметь возможность хранить кратные числа 10 -2 , так что это не имеет большого значения эта 1/3 не может быть представлена.
Проблема с плавающими и двойными числами заключается в том, что подавляющее большинство чисел, подобных деньгам, не имеют точного представления в виде целого числа, умноженного на степень 2. Фактически, единственные кратные 0,01 между 0 и 1 (что важно при работе с с деньгами, потому что они являются целочисленными центами), которые могут быть представлены в точности как двоичное число IEEE-754 с плавающей запятой, равное 0, 0,25, 0,5, 0,75 и 1. Все остальные отклонены на небольшое количество. По аналогии с примером 0.333333, если вы возьмете значение с плавающей запятой за 0.1 и умножите его на 10, вы не получите 1.
Представление денег как
double
илиfloat
, вероятно, будет хорошо выглядеть вначале, поскольку программное обеспечение округляет крошечные ошибки, но по мере того, как вы выполняете больше сложений, вычитаний, умножений и делений на неточные числа, ошибки будут составлять, и в результате вы получите значения, которые являются видимыми не точный. Это делает поплавки и удвоения неадекватными для работы с деньгами, где требуется идеальная точность для кратных степеням базовых 10.Решение, которое работает практически на любом языке, состоит в том, чтобы использовать вместо этого целые числа и считать центы. Например, 1025 будет 10,25 долларов. Несколько языков также имеют встроенные типы для работы с деньгами. Среди прочего, у Java есть
BigDecimal
класс, а у C # естьdecimal
тип.источник
1.0 / 10 * 10
может не совпадать с 1.0.От Блоха, Дж. Эффективная Ява, 2-е изд, пункт 48:
Хотя
BigDecimal
есть некоторые оговорки (см. В настоящее время принятый ответ).источник
long a = 104
и считаешь в центах, а не в долларах.BigDecimal
.Это не вопрос точности и не вопрос точности. Это вопрос удовлетворения ожиданий людей, которые используют базу 10 для расчетов вместо базы 2. Например, использование двойных чисел для финансовых расчетов не дает ответов, которые являются «неправильными» в математическом смысле, но может давать ответы, которые не то, что ожидается в финансовом смысле.
Даже если вы округлите свои результаты в последнюю минуту перед выводом, вы все равно можете иногда получить результат, используя удвоения, которые не соответствуют ожиданиям.
Используя калькулятор или вычисляя результаты вручную, 1.40 * 165 = 231 точно. Однако, внутренне используя double, в моей среде компилятора / операционной системы он сохраняется как двоичное число, близкое к 230.99999 ... поэтому, если вы усекаете число, вы получите 230 вместо 231. Вы можете подумать, что округление вместо усечения будет дали желаемый результат 231. Это правда, но округление всегда предполагает усечение. Какой бы метод округления вы ни использовали, существуют граничные условия, подобные этой, которые будут округляться, когда вы ожидаете, что она округлится. Они настолько редки, что их часто не обнаруживают при случайном тестировании или наблюдении. Возможно, вам придется написать некоторый код для поиска примеров, которые иллюстрируют результаты, которые ведут себя не так, как ожидалось.
Предположим, вы хотите что-то округлить до ближайшей копейки. Таким образом, вы берете свой окончательный результат, умножаете на 100, добавляете 0,5, усекаете, а затем делите результат на 100, чтобы вернуться к копейкам. Если вы сохранили внутренний номер 3,46499999 .... вместо 3,465, вы получите 3,46 вместо 3,47, когда округлите число до ближайшей копейки. Но ваши базовые 10 расчетов, возможно, указали, что ответ должен быть точно 3.465, что, очевидно, должно округляться до 3.47, а не до 3.46. Подобные вещи случаются иногда в реальной жизни, когда вы используете двойники для финансовых расчетов. Это редко, поэтому часто остается незамеченным как проблема, но это случается.
Если вы используете основание 10 для своих внутренних вычислений вместо двойных, ответы всегда будут именно такими, какие ожидают люди, если в вашем коде нет других ошибок.
источник
Math.round(0.49999999999999994)
возвращается 1?Я обеспокоен некоторыми из этих ответов. Я думаю, что двойные и плавающие имеют место в финансовых расчетах. Конечно, при добавлении и вычитании недробных денежных сумм не будет потери точности при использовании целочисленных классов или классов BigDecimal. Но при выполнении более сложных операций вы часто получаете результаты, которые выходят за несколько или несколько десятичных разрядов, независимо от того, как вы храните числа. Вопрос в том, как вы представляете результат.
Если ваш результат находится на границе между округлением в большую сторону и округлением в меньшую сторону, и эта последняя копейка действительно имеет значение, вы, вероятно, должны сказать зрителю, что ответ находится почти посередине - отображая больше десятичных знаков.
Проблема с двойными числами, и особенно с числами с плавающей запятой, заключается в том, что они используются для объединения больших и малых чисел. В яве,
результаты в
источник
Поплавки и двойники являются приблизительными. Если вы создадите BigDecimal и передадите float в конструктор, вы увидите, что фактически равно float:
это, вероятно, не так, как вы хотите представить $ 1,01.
Проблема в том, что в спецификации IEEE нет способа точно представить все дроби, некоторые из них заканчиваются как повторяющиеся дроби, так что вы получите ошибки аппроксимации. Поскольку бухгалтерам нравятся вещи, которые выходят именно за копейки, и клиенты будут раздражены, если они оплатят свой счет, а после обработки платежа они должны 0,01, и им начисляют комиссию или они не могут закрыть свой аккаунт, лучше использовать точные типы, такие как десятичные (в C #) или java.math.BigDecimal в Java.
Дело не в том, что ошибка не контролируется, если вы округлились : посмотрите эту статью Питера Лоури . Просто проще не округлять в первую очередь. Большинство приложений, которые обрабатывают деньги, не требуют много математики, операции состоят из добавления вещей или распределения сумм по разным корзинам. Введение с плавающей запятой и округление просто усложняют вещи.
источник
float
,double
ИBigDecimal
являются представляют собой точные значения. Преобразование кода в объект является неточным, как и другие операции. Сами типы не являются неточными.Я рискну быть опущенным, но я думаю, что непригодность чисел с плавающей запятой для валютных расчетов переоценена. До тех пор, пока вы убедитесь, что вы правильно делаете центрирование и имеете достаточно значащих цифр для работы, чтобы противостоять несовпадению двоично-десятичного представления, объясненному zneak, проблем не будет.
Люди, производящие вычисления в валюте в Excel, всегда использовали числа с плавающей запятой двойной точности (в Excel нет типа валюты), и я пока не видел, чтобы кто-то жаловался на ошибки округления.
Конечно, вы должны оставаться в пределах разумного; Например, простой интернет-магазин, вероятно, никогда не столкнется с какой-либо проблемой с плавающей точкой двойной точности, но если вы делаете, например, учет или что-то еще, что требует добавления большого (неограниченного) количества чисел, вы не захотите касаться чисел с плавающей запятой десятифутовым столб.
источник
Хотя верно, что тип с плавающей запятой может представлять только приблизительно десятичные данные, также верно, что если округлить числа до необходимой точности, прежде чем представлять их, получится правильный результат. Как правило.
Обычно потому, что двойной тип имеет точность менее 16 цифр. Если вам нужна лучшая точность, это не подходящий тип. Также приближения могут накапливаться.
Надо сказать, что даже если вы используете арифметику с фиксированной запятой, вам все равно придется округлять числа, если бы не BigInteger и BigDecimal, если вы получаете периодические десятичные числа. Так что здесь тоже есть приближение.
Например, COBOL, исторически используемый для финансовых расчетов, имеет максимальную точность 18 цифр. Так что часто происходит неявное округление.
В заключение, на мой взгляд, двойник не подходит в основном из-за точности в 16 цифр, что может быть недостаточно, а не потому, что он приблизительный.
Рассмотрим следующий вывод следующей программы. Это показывает, что после округления double дают тот же результат, что и BigDecimal с точностью до 16.
источник
Результат числа с плавающей запятой не является точным, что делает его непригодным для любого финансового расчета, который требует точного результата, а не приближения. float и double предназначены для инженерных и научных расчетов и много раз не дают точного результата, а также результат вычисления с плавающей точкой может варьироваться от JVM до JVM. Посмотрите на приведенный ниже пример BigDecimal и двойного примитива, который используется для представления денежной стоимости. Совершенно ясно, что вычисление с плавающей запятой может быть неточным, и для финансовых расчетов следует использовать BigDecimal.
Вывод:
источник
double
FP к центу не составит труда вычислить до 0,5 цента, равно как и десятичное FP. Если вычисления с плавающей запятой дают процентное значение, например, 123,499941 ¢, либо через двоичную или десятичную FP, проблема двойного округления остается той же - никакого преимущества в любом случае нет. Ваша предпосылка, кажется, предполагает, что математически точное значение и десятичная FP одинаковы - что даже десятичная FP не гарантирует. 0.5 / 7.0 * 7.0 - проблема для бинарного и дэйкмального FP. IAC, большинство из них будет спорным, так как я ожидаю, что следующая версия C предоставит десятичную FP.Как было сказано ранее: «Представление денег в виде двойного числа или числа с плавающей запятой, вероятно, сначала будет хорошо выглядеть, поскольку программное обеспечение округляет крошечные ошибки, но по мере того, как вы будете выполнять больше сложений, вычитаний, умножений и делений на неточных числах, вы потеряете все большую точность по мере того, как ошибки складываются. Это делает поплавки и двойные неадекватными для работы с деньгами, где требуется идеальная точность для кратных степеням базовых 10 ».
Наконец, у Java есть стандартный способ работы с валютой и деньгами!
JSR 354: API денег и валюты
JSR 354 предоставляет API для представления, транспортировки и выполнения комплексных расчетов с деньгами и валютой. Вы можете скачать его по этой ссылке:
Скачать JSR 354: деньги и валюта API
Спецификация состоит из следующих вещей:
Примеры JSR 354: деньги и валюта API:
Пример создания MonetaryAmount и его печати на консоли выглядит следующим образом:
При использовании эталонного API реализации необходимый код намного проще:
API также поддерживает вычисления с MonetaryAmounts:
CurrencyUnit и MonetaryAmount
MonetaryAmount имеет различные методы, которые позволяют получить доступ к назначенной валюте, числовой сумме, ее точности и многим другим:
MonetaryAmounts можно округлить с помощью оператора округления:
При работе с коллекциями MonetaryAmounts доступны некоторые полезные вспомогательные методы для фильтрации, сортировки и группировки.
Пользовательские операции MonetaryAmount
Ресурсы:
Обработка денег и валюты на Java с помощью JSR 354
Изучение API денег и валюты Java 9 (JSR 354)
Смотрите также: JSR 354 - Валюта и деньги
источник
Если ваши вычисления включают в себя различные этапы, арифметика произвольной точности не покроет вас на 100%.
Единственный надежный способ использовать идеальное представление результатов (использовать пользовательский тип данных Fraction, который будет группировать операции деления до последнего шага) и конвертировать только в десятичную запись на последнем шаге.
Произвольная точность не поможет, потому что всегда могут быть числа с таким количеством десятичных разрядов или некоторые результаты, такие как
0.6666666
... Никакое произвольное представление не будет охватывать последний пример. Таким образом, у вас будут небольшие ошибки на каждом этапе.Эти ошибки будут накапливаться, и в конечном итоге их будет не просто игнорировать. Это называется распространением ошибок .
источник
В большинстве ответов подчеркиваются причины, по которым нельзя использовать двойные числа для расчета денег и валюты. И я с ними полностью согласен.
Это не значит, что двойники никогда не могут быть использованы для этой цели.
Я работал над несколькими проектами с очень низкими требованиями к gc, и наличие объектов BigDecimal внесло большой вклад в эти накладные расходы.
Недостаток понимания о двойном представлении и недостаток опыта в обращении с точностью и точностью приводит к этому мудрому предположению.
Вы можете заставить его работать, если вы в состоянии справиться с требованиями к точности и точности вашего проекта, что должно быть сделано на основе того, с каким диапазоном двойных значений имеет дело одно.
Вы можете обратиться к методу FuzzyCompare в guava, чтобы получить больше идей. Параметр допуска является ключевым. Мы рассмотрели эту проблему для приложения для торговли ценными бумагами и провели исчерпывающее исследование того, какие допуски использовать для различных числовых значений в разных диапазонах.
Кроме того, могут возникнуть ситуации, когда вы захотите использовать двойные обертки в качестве ключа карты, при этом хеш-карта является реализацией. Это очень рискованно, потому что Double.equals и хэш-код, например, значения «0.5» и «0.6 - 0.1», вызовут большой беспорядок.
источник
Во многих ответах на этот вопрос обсуждаются IEEE и стандарты, касающиеся арифметики с плавающей точкой.
Исходя из не-компьютерных знаний (физика и инженерия), я склонен смотреть на проблемы с другой точки зрения. Для меня причина, почему я не использовал бы double или float в математических вычислениях, состоит в том, что я потерял бы слишком много информации.
Какие есть альтернативы? Их много (и еще много о которых я не знаю!).
BigDecimal в Java является родным для языка Java. Apfloat - это еще одна библиотека произвольной точности для Java.
Десятичный тип данных в C # - альтернатива Microsoft .NET для 28 значащих цифр.
SciPy (Scientific Python), вероятно, также может обрабатывать финансовые расчеты (я не пробовал, но подозреваю, что так).
GNU Multiple Precision Library (GMP) и библиотека GNU MFPR - это два бесплатных ресурса с открытым исходным кодом для C и C ++.
Есть также библиотеки точности чисел для JavaScript (!), И я думаю, что PHP может обрабатывать финансовые расчеты.
Есть также проприетарные (особенно, я думаю, для Fortran) и решения с открытым исходным кодом для многих компьютерных языков.
Я не программист по образованию. Однако я склоняюсь к BigDecimal в Java или к десятичному в C #. Я не пробовал другие решения, которые я перечислил, но они, вероятно, также очень хороши.
Мне нравится BigDecimal из-за методов, которые он поддерживает. Десятичная запятая в C # очень хороша, но у меня не было возможности работать с ней столько, сколько хотелось бы. В свободное время я делаю научные расчеты, которые меня интересуют, и BigDecimal, кажется, работает очень хорошо, потому что я могу установить точность своих чисел с плавающей запятой. Недостаток BigDecimal? Иногда это может быть медленным, особенно если вы используете метод деления.
Для скорости вы можете заглянуть в бесплатные и проприетарные библиотеки на C, C ++ и Fortran.
источник
Чтобы добавить к предыдущим ответам, есть также вариант реализации Joda-Money на Java, помимо BigDecimal, при решении проблемы, рассматриваемой в вопросе. Java модуль называется org.joda.money.
Это требует Java SE 8 или позже и не имеет никаких зависимостей.
Примеры использования Joda Money:
источник
Некоторые примеры ... это работает (на самом деле не работает должным образом), практически на любом языке программирования ... Я пробовал с Delphi, VBScript, Visual Basic, JavaScript и теперь с Java / Android:
ВЫВОД:
round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!
источник
Float - это двоичная форма Decimal с другим дизайном; это две разные вещи. Есть небольшие ошибки между двумя типами при преобразовании друг в друга. Кроме того, float предназначен для представления бесконечно большого количества значений для научных. Это означает, что он предназначен для потери точности до предельно малого и предельно большого числа с этим фиксированным числом байтов. Десятичное число не может представлять бесконечное количество значений, оно ограничивается только этим количеством десятичных цифр. Так что Float и Decimal предназначены для разных целей.
Есть несколько способов управления ошибкой для значения валюты:
Используйте длинное целое и вместо этого считать в центах.
Используйте двойную точность, сохраняйте ваши значащие цифры до 15, чтобы десятичное число могло быть точно смоделировано. Округлить до представления значений; Круглый часто при выполнении расчетов.
Используйте десятичную библиотеку, такую как Java BigDecimal, так что вам не нужно использовать double для имитации десятичной дроби.
PS Интересно знать, что большинство брендов карманных научных калькуляторов работает с десятичной, а не с плавающей точкой. Так что никто не жалуется на ошибки преобразования числа с плавающей точкой.
источник