Почему бы не использовать Double или Float для представления валюты? - PullRequest
854 голосов
/ 16 сентября 2010

Мне всегда говорили никогда представлять деньги с double или float типами, и на этот раз я задаю вам вопрос: почему?

Я уверен, что есть очень веская причина, я просто не знаю, что это такое.

Ответы [ 15 ]

891 голосов
/ 16 сентября 2010

Потому что числа с плавающей и двойной не могут точно представлять базовые 10, которые мы используем для денег. Эта проблема не только для Java, но и для любого языка программирования, который использует базовые типы с плавающей запятой 2.

В базе 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 * 1016. *, поэтому не имеет значения, что 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.

290 голосов
/ 16 сентября 2010

Из Bloch, J., Effective Java, 2nd ed, Item 48:

Типы float и double особенно плохо подходят для денежных расчетовпотому что невозможно представить 0.1 (или любую другую отрицательную степень десяти) как float или double точно.

Например, предположим, что у вас есть 1,03 доллара, а вы тратите 42c.Сколько у вас осталось денег?

System.out.println(1.03 - .42);

распечатывает 0.6100000000000001.

Правильный способ решения этой проблемы - использовать BigDecimal, int или longдля денежных расчетов.

Хотя BigDecimal имеет некоторые оговорки (см. принятый в настоящее время ответ).

69 голосов
/ 12 сентября 2012

Это не вопрос точности и не точности. Это вопрос удовлетворения ожиданий людей, которые используют базу 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 для внутренних вычислений вместо двойных, ответы всегда будут именно такими, какие ожидают люди, при условии отсутствия других ошибок в вашем коде.

48 голосов
/ 03 апреля 2013

Я обеспокоен некоторыми из этих ответов. Я думаю, что двойные и плавающие имеют место в финансовых расчетах. Конечно, при добавлении и вычитании недробных денежных сумм не будет потери точности при использовании целочисленных классов или классов BigDecimal. Но при выполнении более сложных операций вы часто получаете результаты, которые выходят за несколько или несколько десятичных разрядов, независимо от того, как вы храните числа. Вопрос в том, как вы представляете результат.

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

Проблема с двойными числами и, более того, с числами с плавающей запятой, заключается в том, что они используются для объединения больших и малых чисел. В Яве

System.out.println(1000000.0f + 1.2f - 1000000.0f);

результаты в

1.1875
36 голосов
/ 16 сентября 2010

Плавающие и двойные числа являются приблизительными. Если вы создадите 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.

Дело не в том, что ошибка не контролируется, если вы округлили: см. Эту статью Питера Лоури . Просто проще не округлять в первую очередь. Большинство приложений, которые обрабатывают деньги, не требуют большого количества математики, операции состоят из добавления вещей или распределения сумм по разным корзинам. Введение чисел с плавающей запятой и округление только усложняют вещи.

21 голосов
/ 20 января 2017

Я рискну быть опущенным, но я думаю, что непригодность чисел с плавающей запятой для валютных расчетов переоценена.Пока вы убедитесь, что вы правильно делаете центрирование и имеете достаточно значащих цифр для работы, чтобы противостоять несоответствию двоично-десятичного представления, объясненному zneak, проблем не будет.

Люди, вычисляющие с помощьювалюта в Excel всегда использовала числа с плавающей запятой двойной точности (в Excel нет типа валюты), и я еще не видел никого, кто бы жаловался на ошибки округления.

Конечно, вы должны оставаться в пределах разумного;Например, простой интернет-магазин, вероятно, никогда не столкнется с какой-либо проблемой с плавающей точкой двойной точности, но если вы делаете, например, учет или что-то еще, что требует добавления большого (неограниченного) количества чисел, вы не захотите касаться чисел с плавающей запятой десятифутовымполюс.

18 голосов
/ 26 октября 2012

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

Обычно, потому что тип double имеет точность меньше 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;
    }
}
15 голосов
/ 12 августа 2014

Результат числа с плавающей запятой не является точным, что делает его непригодным для любого финансового расчета, который требует точного результата, а не приближения.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
7 голосов
/ 07 августа 2016

Как уже говорилось ранее, «Представление денег в виде двойного числа или числа с плавающей запятой, вероятно, сначала будет хорошо выглядеть, поскольку программное обеспечение округляет крошечные ошибки, но по мере того, как вы будете выполнять дополнительные сложения, вычитания, умножения и деления на неточные числа, вы потеряете большеи больше точности при суммировании ошибок. Это делает поплавки и удвоения неадекватными для работы с деньгами, где требуется идеальная точность для кратных степеням 10 оснований. "

Наконец, в Java есть стандартный способ работыс валютой и деньгами!

JSR 354: API денег и валюты

JSR 354 предоставляет API для представления, транспортировки и выполнения комплексных вычислений с помощью Money иВалюта.Вы можете скачать его по этой ссылке:

JSR 354: Скачать API для денег и валюты

Спецификация состоит из следующих вещей:

  1. API для обработки, например, денежных сумм и валют
  2. API для поддержки взаимозаменяемых реализаций
  3. Фабрики для создания экземпляров классов реализации
  4. Функциональность для расчетов, преобразования и форматирования денежных сумм
  5. Java API для работы с деньгами и валютами, который планируется включить в Java 9.
  6. Все спецификации классов и интерфейсы находятся вjavax.money. * package.

Примеры 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 - Валюта и деньги

4 голосов
/ 05 января 2015

Если ваши вычисления включают в себя различные шаги, арифметика произвольной точности не покроет вас на 100%.

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

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

Эта ошибка будет накапливаться, и со временем ее будет уже не просто игнорировать. Это называется Ошибка распространения .

...