Мне всегда говорили никогда не изображать деньги double
и не float
печатать, и на этот раз я задаю вам вопрос: почему?
Я уверен, что есть очень веская причина, я просто не знаю, что это такое.
Мне всегда говорили никогда не изображать деньги double
и не float
печатать, и на этот раз я задаю вам вопрос: почему?
Я уверен, что есть очень веская причина, я просто не знаю, что это такое.
Ответы:
Потому что числа с плавающей запятой и двойные не могут точно представлять базовые 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:
float
Иdouble
типов особенно плохо подходит для денежных расчетов , так как невозможно представить 0,1 (или любую другую отрицательную степень десять) в видеfloat
или вdouble
точность.Например, предположим, у вас есть 1,03 доллара, а вы тратите 42c. Сколько денег у тебя осталось?
System.out.println(1.03 - .42);
распечатывает
0.6100000000000001
.Правильный путь для решения этой проблемы заключается в использовании
BigDecimal
,int
илиlong
для денежных расчетов.
Хотя 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. Но при выполнении более сложных операций вы часто получаете результаты, которые выходят за несколько или несколько десятичных разрядов, независимо от того, как вы храните числа. Вопрос в том, как вы представляете результат.
Если ваш результат находится на границе между округлением в большую сторону и округлением в меньшую сторону, и эта последняя копейка действительно имеет значение, вы, вероятно, должны сказать зрителю, что ответ находится почти посередине - отображая больше десятичных знаков.
Проблема с двойными числами, и особенно с числами с плавающей запятой, заключается в том, что они используются для объединения больших и малых чисел. В яве,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
результаты в
1.1875
Поплавки и двойники являются приблизительными. Если вы создадите BigDecimal и передадите float в конструктор, вы увидите, что фактически равно float:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
это, вероятно, не так, как вы хотите представить $ 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.
Precision 14
------------------------------------------------------
BigDecimalNoRound : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal : 56789.012345 / 1111111111 = 0.000051110111115611
Double : 56789.012345 / 1111111111 = 0.000051110111115611
Precision 15
------------------------------------------------------
BigDecimalNoRound : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal : 56789.012345 / 1111111111 = 0.0000511101111156110
Double : 56789.012345 / 1111111111 = 0.0000511101111156110
Precision 16
------------------------------------------------------
BigDecimalNoRound : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal : 56789.012345 / 1111111111 = 0.00005111011111561101
Double : 56789.012345 / 1111111111 = 0.00005111011111561101
Precision 17
------------------------------------------------------
BigDecimalNoRound : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal : 56789.012345 / 1111111111 = 0.000051110111115611011
Double : 56789.012345 / 1111111111 = 0.000051110111115611013
Precision 18
------------------------------------------------------
BigDecimalNoRound : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double : 56789.012345 / 1111111111 = 0.0000511101111156110125
Precision 19
------------------------------------------------------
BigDecimalNoRound : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double : 56789.012345 / 1111111111 = 0.00005111011111561101252
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;
public class Exercise {
public static void main(String[] args) throws IllegalArgumentException,
SecurityException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
String amount = "56789.012345";
String quantity = "1111111111";
int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
for (int i = 0; i < precisions.length; i++) {
int precision = precisions[i];
System.out.println(String.format("Precision %d", precision));
System.out.println("------------------------------------------------------");
execute("BigDecimalNoRound", amount, quantity, precision);
execute("DoubleNoRound", amount, quantity, precision);
execute("BigDecimal", amount, quantity, precision);
execute("Double", amount, quantity, precision);
System.out.println();
}
}
private static void execute(String test, String amount, String quantity,
int precision) throws IllegalArgumentException, SecurityException,
IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
String.class, int.class);
String price;
try {
price = (String) impl.invoke(null, amount, quantity, precision);
} catch (InvocationTargetException e) {
price = e.getTargetException().getMessage();
}
System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
quantity, price));
}
public static String divideUsingDoubleNoRound(String amount,
String quantity, int precision) {
// acceptance
double amount0 = Double.parseDouble(amount);
double quantity0 = Double.parseDouble(quantity);
//calculation
double price0 = amount0 / quantity0;
// presentation
String price = Double.toString(price0);
return price;
}
public static String divideUsingDouble(String amount, String quantity,
int precision) {
// acceptance
double amount0 = Double.parseDouble(amount);
double quantity0 = Double.parseDouble(quantity);
//calculation
double price0 = amount0 / quantity0;
// presentation
MathContext precision0 = new MathContext(precision);
String price = new BigDecimal(price0, precision0)
.toString();
return price;
}
public static String divideUsingBigDecimal(String amount, String quantity,
int precision) {
// acceptance
BigDecimal amount0 = new BigDecimal(amount);
BigDecimal quantity0 = new BigDecimal(quantity);
MathContext precision0 = new MathContext(precision);
//calculation
BigDecimal price0 = amount0.divide(quantity0, precision0);
// presentation
String price = price0.toString();
return price;
}
public static String divideUsingBigDecimalNoRound(String amount, String quantity,
int precision) {
// acceptance
BigDecimal amount0 = new BigDecimal(amount);
BigDecimal quantity0 = new BigDecimal(quantity);
//calculation
BigDecimal price0 = amount0.divide(quantity0);
// presentation
String price = price0.toString();
return price;
}
}
Результат числа с плавающей запятой не является точным, что делает его непригодным для любого финансового расчета, который требует точного результата, а не приближения. float и double предназначены для инженерных и научных расчетов и много раз не дают точного результата, а также результат вычисления с плавающей точкой может варьироваться от JVM до JVM. Посмотрите на приведенный ниже пример BigDecimal и двойного примитива, который используется для представления денежной стоимости. Совершенно ясно, что вычисление с плавающей запятой может быть неточным, и для финансовых расчетов следует использовать BigDecimal.
// floating point calculation
final double amount1 = 2.0;
final double amount2 = 1.1;
System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));
// Use BigDecimal for financial calculation
final BigDecimal amount3 = new BigDecimal("2.0");
final BigDecimal amount4 = new BigDecimal("1.1");
System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));
Вывод:
difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
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
Спецификация состоит из следующих вещей:
- API для обработки, например, денежных сумм и валют
- API для поддержки взаимозаменяемых реализаций
- Фабрики для создания экземпляров классов реализации
- Функциональность для расчетов, конвертации и форматирования денежных сумм
- Java API для работы с деньгами и валютами, который планируется включить в Java 9.
- Все спецификации классов и интерфейсы находятся в пакете javax.money. *.
Примеры JSR 354: деньги и валюта API:
Пример создания MonetaryAmount и его печати на консоли выглядит следующим образом:
MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));
При использовании эталонного API реализации необходимый код намного проще:
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));
API также поддерживает вычисления с MonetaryAmounts:
MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));
CurrencyUnit и MonetaryAmount
// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
MonetaryAmount имеет различные методы, которые позволяют получить доступ к назначенной валюте, числовой сумме, ее точности и многим другим:
MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();
int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5
// NumberValue extends java.lang.Number.
// So we assign numberValue to a variable of type Number
Number number = numberValue;
MonetaryAmounts можно округлить с помощью оператора округления:
CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35
При работе с коллекциями MonetaryAmounts доступны некоторые полезные вспомогательные методы для фильтрации, сортировки и группировки.
List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));
Пользовательские операции MonetaryAmount
// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
return Money.of(tenPercent, amount.getCurrency());
};
MonetaryAmount dollars = Money.of(12.34567, "USD");
// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567
Ресурсы:
Обработка денег и валюты на 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 или позже и не имеет никаких зависимостей.
Чтобы быть более точным, есть зависимость времени компиляции, но это не требуется.
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
Примеры использования Joda Money:
// create a monetary value
Money money = Money.parse("USD 23.87");
// add another amount with safe double conversion
CurrencyUnit usd = CurrencyUnit.of("USD");
money = money.plus(Money.of(usd, 12.43d));
// subtracts an amount in dollars
money = money.minusMajor(2);
// multiplies by 3.5 with rounding
money = money.multipliedBy(3.5d, RoundingMode.DOWN);
// compare two amounts
boolean bigAmount = money.isGreaterThan(dailyWage);
// convert to GBP using a supplied rate
BigDecimal conversionRate = ...; // obtained from code outside Joda-Money
Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);
// use a BigMoney for more complex calculations where scale matters
BigMoney moneyCalc = money.toBigMoney();
Документация: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html
Примеры реализации: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money
Некоторые примеры ... это работает (на самом деле не работает должным образом), практически на любом языке программирования ... Я пробовал с Delphi, VBScript, Visual Basic, JavaScript и теперь с Java / Android:
double total = 0.0;
// do 10 adds of 10 cents
for (int i = 0; i < 10; i++) {
total += 0.1; // adds 10 cents
}
Log.d("round problems?", "current total: " + total);
// looks like total equals to 1.0, don't?
// now, do reverse
for (int i = 0; i < 10; i++) {
total -= 0.1; // removes 10 cents
}
// looks like total equals to 0.0, don't?
Log.d("round problems?", "current total: " + total);
if (total == 0.0) {
Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
} else {
Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
}
ВЫВОД:
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 Интересно знать, что большинство брендов карманных научных калькуляторов работает с десятичной, а не с плавающей точкой. Так что никто не жалуется на ошибки преобразования числа с плавающей точкой.