Почему бы не использовать Double или Float для представления валюты?

939

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

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

Фрэн Фицпатрик
источник
4
Посмотрите этот ТАК вопрос: Ошибки округления?
Джефф Огата
80
Просто чтобы быть ясно, они не должны использоваться для чего-то, что требует точности - не только валюта.
Джефф
152
Они не должны использоваться для чего-либо, что требует точности . Но 53 значащих бита double (~ 16 десятичных цифр) обычно достаточно хороши для вещей, которые просто требуют точности .
Ден04
21
@jeff Ваш комментарий полностью искажает, для чего полезна двоичная с плавающей точкой и для чего она не годится. Прочитайте ответ ниже, и, пожалуйста, удалите свой вводящий в заблуждение комментарий.
Паскаль Куок

Ответы:

985

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

zneak
источник
3
@Fran Вы получите ошибки округления, и в некоторых случаях, когда используется большое количество валюты, вычисления процентных ставок могут быть сильно
отклонены
5
... большинство базовых 10 фракций, то есть. Например, 0.1 не имеет точного двоичного представления с плавающей точкой. Таким образом, 1.0 / 10 * 10может не совпадать с 1.0.
Крис Джестер-Янг
6
@ linuxuser27 Я думаю, что Фрэн пыталась быть смешной. В любом случае, ответ zneak - лучшее, что я видел, даже лучше, чем классическая версия Блоха.
Исаак Рабинович
5
Конечно, если вы знаете точность, вы всегда можете округлить результат и, таким образом, избежать всей проблемы. Это намного быстрее и проще, чем использование BigDecimal. Другой альтернативой является использование фиксированной точности int или long.
Питер Лори
2
@ zneak вы знаете, что рациональные числа являются подмножеством действительных чисел, верно? Действительные числа IEEE-754 являются действительными числами. Они просто оказались также быть рациональным.
Тим Сегин
315

От Блоха, Дж. Эффективная Ява, 2-е изд, пункт 48:

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

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

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

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

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

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

кендырь
источник
6
Меня немного смущает рекомендация использовать int или long для денежных расчетов. Как вы представляете 1.03 как int или long? Я пробовал "long a = 1.04;" и "длинный а = 104/100;" но безрезультатно.
Питер
49
@ Питер, ты используешь long a = 104и считаешь в центах, а не в долларах.
zneak
@zneak Как насчет того, когда нужно применить процент, например, сложный процент или аналогичный?
trusktr
3
@trusktr, я бы выбрал десятичный тип вашей платформы. На Java это так BigDecimal.
zneak
13
@maaartinus ... а ты не думаешь, что использование double для таких вещей подвержено ошибкам? Я видел этот вопрос поплавка округления ударил реальные системы трудно . Даже в банковском деле. Пожалуйста , не рекомендуется, или если вы делаете, предусмотрено , что в качестве отдельного ответа (так что мы можем downvote его: P)
EIS
75

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

Рэнди Д Оксентенко
источник
2
Связанный, интересный: В моей консоли chrome js: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Кертис Яллоп
16
Этот ответ вводит в заблуждение. 1.40 * 165 = 231. Любое число, отличное от точно 231 , неверно в математическом смысле (и во всех других смыслах).
Кару
2
@Karu Я думаю, именно поэтому Рэнди говорит, что поплавки плохие ... Моя консоль Chrome JS показывает 230.99999999999997 в результате. То есть не так, что точка сделано в ответ.
trusktr
6
@Karu: Imho ответ не математически неправильный. Просто есть 2 вопроса, один из которых отвечает, но это не вопрос. Вопрос, на который ваш компилятор отвечает: 1.39999999 * 164.99999999 и т. Д., Математически правильное значение равно 230.99999 .... Очевидно, это не тот вопрос, который был задан в первую очередь ....
Маркус
2
@CurtisYallop, потому что закрывающее двойное значение до 0,49999999999999999 равно 0,5. Почему Math.round(0.49999999999999994)возвращается 1?
phuclv
53

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

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

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

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

результаты в

1.1875
Роб Скала
источник
16
ЭТО!!!! Я искал все ответы, чтобы найти этот СООТВЕТСТВУЮЩИЙ ФАКТ !!! В обычных вычислениях никого не волнует, если вы имеете долю от доли процента, но здесь с большими числами легко теряются несколько долларов за транзакцию!
Falco
22
А теперь представьте, что кто-то получает ежедневный доход в размере 0,01% на свои 1 миллион долларов - он не получал бы ничего каждый день - и через год он не получил 1000 долларов, ЭТО БУДЕТ ВАЖНО
Falco
6
Проблема не в точности, но это число не говорит вам, что оно становится неточным. Целое число может содержать до 10 цифр, а число с плавающей запятой может содержать до 6, не становясь неточным (при соответствующем сокращении). Это разрешает это, в то время как целое число получает переполнение, а язык наподобие java предупредит вас или не допустит. Когда вы используете двойное число, вы можете ввести до 16 цифр, что достаточно для многих случаев использования.
Сиги
39

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

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

Натан Хьюз
источник
5
float, doubleИ BigDecimalявляются представляют собой точные значения. Преобразование кода в объект является неточным, как и другие операции. Сами типы не являются неточными.
chux - Восстановить Монику
1
@chux: перечитывая это, я думаю, у вас есть точка зрения, что моя формулировка может быть улучшена. Я отредактирую это и перефразирую.
Натан Хьюз
28

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

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

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

эсциталопрам
источник
3
Это на самом деле довольно приличный ответ. В большинстве случаев их вполне можно использовать.
Вахид Амири
2
Следует отметить, что большинство инвестиционных банков используют двойные, как и большинство программ на С ++. Некоторые используют долго, но у этого есть своя собственная проблема отслеживания масштаба.
Питер Лори
20

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

Обычно потому, что двойной тип имеет точность менее 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;
    }
}
user1593165
источник
15

Результат числа с плавающей запятой не является точным, что делает его непригодным для любого финансового расчета, который требует точного результата, а не приближения. 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
Дхирадж Арора
источник
3
Давайте попробуем что-то иное, чем простое сложение / вычитание и целочисленное умножение. Если бы код вычислял месячную ставку ссуды в 7%, оба типа не смогли бы обеспечить точное значение и потребовали бы округления до ближайшего 0,01. Округление до самой низкой денежной единицы является частью расчета денег. Использование десятичных типов позволяет избежать этого с добавлением / вычитанием, но не более того.
chux - Восстановить Монику
@ chux-ReinstateMonica: если предполагается, что проценты составляются ежемесячно, рассчитывайте проценты каждый месяц, складывая дневной баланс, умножая его на 7 (процентная ставка), и делите, округляя до ближайшей копейки, на количество дней в год. Никаких округлений, кроме одного раза в месяц на самом последнем этапе.
суперкат
@supercat Мой комментарий подчеркивает, что использование двоичной FP с наименьшей денежной единицей или десятичной FP приводит к одинаковым проблемам округления - как в вашем комментарии с «и делить, округляя до ближайшей копейки». Использование базовых 2 или базовых 10 FP не дает преимущества в любом случае в вашем сценарии.
chux - Восстановить Монику
@ chux-ReinstateMonica: В приведенном выше сценарии, если математика решает, что процент должен быть точно равен некоторому числу пол-центов, правильная финансовая программа должна округляться точно указанным образом. Если вычисления с плавающей запятой дают процентное значение, например, $ 1,23499941, но математически точное значение до округления должно было составлять 1,235 доллара, а округление определяется как «ближайший четный», использование таких вычислений с плавающей запятой не приведет к быть на 0,000059 долл. США, а точнее на 0,01 долл. США, что для целей бухгалтерского учета является просто ошибочным.
суперкат
@supercat Использование двоичного doubleFP к центу не составит труда вычислить до 0,5 цента, равно как и десятичное FP. Если вычисления с плавающей запятой дают процентное значение, например, 123,499941 ¢, либо через двоичную или десятичную FP, проблема двойного округления остается той же - никакого преимущества в любом случае нет. Ваша предпосылка, кажется, предполагает, что математически точное значение и десятичная FP одинаковы - что даже десятичная FP не гарантирует. 0.5 / 7.0 * 7.0 - проблема для бинарного и дэйкмального FP. IAC, большинство из них будет спорным, так как я ожидаю, что следующая версия C предоставит десятичную FP.
chux - Восстановить Монику
11

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

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

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

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

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

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

  1. API для обработки, например, денежных сумм и валют
  2. API для поддержки взаимозаменяемых реализаций
  3. Фабрики для создания экземпляров классов реализации
  4. Функциональность для расчетов, конвертации и форматирования денежных сумм
  5. Java API для работы с деньгами и валютами, который планируется включить в Java 9.
  6. Все спецификации классов и интерфейсы находятся в пакете 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 - Валюта и деньги

AFFY
источник
5

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

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

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

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

Кемаль Даğ
источник
4

В большинстве ответов подчеркиваются причины, по которым нельзя использовать двойные числа для расчета денег и валюты. И я с ними полностью согласен.

Это не значит, что двойники никогда не могут быть использованы для этой цели.

Я работал над несколькими проектами с очень низкими требованиями к gc, и наличие объектов BigDecimal внесло большой вклад в эти накладные расходы.

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

Вы можете заставить его работать, если вы в состоянии справиться с требованиями к точности и точности вашего проекта, что должно быть сделано на основе того, с каким диапазоном двойных значений имеет дело одно.

Вы можете обратиться к методу FuzzyCompare в guava, чтобы получить больше идей. Параметр допуска является ключевым. Мы рассмотрели эту проблему для приложения для торговли ценными бумагами и провели исчерпывающее исследование того, какие допуски использовать для различных числовых значений в разных диапазонах.

Кроме того, могут возникнуть ситуации, когда вы захотите использовать двойные обертки в качестве ключа карты, при этом хеш-карта является реализацией. Это очень рискованно, потому что Double.equals и хэш-код, например, значения «0.5» и «0.6 - 0.1», вызовут большой беспорядок.

Дев Амитабх
источник
2

Во многих ответах на этот вопрос обсуждаются 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.

fishermanhat
источник
1
Что касается SciPy / Numpy, фиксированная точность (т. Е. Десятичная дробь Python. Десятичная) не поддерживается ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Некоторые функции не будут правильно работать с десятичными (например, isnan). Pandas основана на Numpy и была основана в AQR, одном из основных количественных хедж-фондов. Таким образом, у вас есть свой ответ относительно финансовых расчетов (не учета продуктов).
конт
2

Чтобы добавить к предыдущим ответам, есть также вариант реализации 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

Тадия малич
источник
0

Некоторые примеры ... это работает (на самом деле не работает должным образом), практически на любом языке программирования ... Я пробовал с 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!!!

WilliamK
источник
3
Проблема не в том, что происходит ошибка округления, а в том, что вы с ней не справляетесь. Округлите результат до двух десятичных знаков (если вы хотите центов), и все готово.
Maaartinus
0

Float - это двоичная форма Decimal с другим дизайном; это две разные вещи. Есть небольшие ошибки между двумя типами при преобразовании друг в друга. Кроме того, float предназначен для представления бесконечно большого количества значений для научных. Это означает, что он предназначен для потери точности до предельно малого и предельно большого числа с этим фиксированным числом байтов. Десятичное число не может представлять бесконечное количество значений, оно ограничивается только этим количеством десятичных цифр. Так что Float и Decimal предназначены для разных целей.

Есть несколько способов управления ошибкой для значения валюты:

  1. Используйте длинное целое и вместо этого считать в центах.

  2. Используйте двойную точность, сохраняйте ваши значащие цифры до 15, чтобы десятичное число могло быть точно смоделировано. Округлить до представления значений; Круглый часто при выполнении расчетов.

  3. Используйте десятичную библиотеку, такую ​​как Java BigDecimal, так что вам не нужно использовать double для имитации десятичной дроби.

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

Крис Цанг
источник