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

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

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

Ответ 1

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

Это не вопрос точности, и это не вопрос точности. Это вопрос удовлетворения ожиданий людей, которые используют базу 10 для вычислений вместо базы 2. Например, использование удвоений для финансовых расчетов не дает ответов, которые являются "неправильными" в математическом смысле, но может давать ответы, которые не то, что ожидается в финансовом смысле.

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

Используя калькулятор или рассчитывая результаты вручную, 1.40 * 165 = 231 точно. Тем не менее, внутренне использующий дубликаты, в моей среде компилятора/операционной системы, он хранится как двоичное число, близкое к 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 для своих внутренних вычислений вместо удвоений, ответы всегда будут точно такими, какие ожидаются люди, не предполагая никаких других ошибок в вашем коде.

Ответ 4

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

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

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

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

приводит к

1.1875

Ответ 5

Поплавки и удвоения являются приблизительными. Если вы создаете BigDecimal и передаете float в конструктор, вы увидите, что фактическое значение float равно:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

это, вероятно, не то, как вы хотите представлять $1.01.

Проблема в том, что спецификация IEEE не имеет возможности точно представлять все дроби, некоторые из них заканчиваются как повторяющиеся дроби, поэтому вы получаете ошибки аппроксимации. Поскольку бухгалтеры любят вещи, которые выходят именно на копейки, а клиенты будут раздражаться, если они оплачивают свой счет, и после того, как платеж будет обработан, они должны 0,01, и им будет взиматься плата или не может закрыть их учетную запись, лучше использовать точные типы, такие как десятичные (в С#) или java.math.BigDecimal в Java.

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

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

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

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

Ответ 11

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

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

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

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

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

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

Кроме того, могут возникнуть ситуации, когда у вас возникает соблазн использовать Double wrappers в качестве ключа карты с картой хэша, являющейся реализацией. Это очень рискованно, потому что Double.equals и хэш-код, например, значения "0,5" и "0,6-1,1", вызовут большой беспорядок.

Ответ 12

Я предпочитаю использовать Integer или Long для представления валюты. BigDecimal слишком сильно увеличивает исходный код.

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

Ответ 13

Многие ответы на этот вопрос обсуждаются IEEE и стандарты, связанные с арифметикой с плавающей запятой.

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

Каковы альтернативы? Есть много (и многие другие из которых я не знаю!).

BigDecimal в Java является родным для языка Java. Apfloat - это еще одна библиотека с произвольной точностью для Java.

Тип десятичных данных на С# является альтернативой Microsoft.NET для 28 значимых цифр.

SciPy (Scientific Python), вероятно, также может обрабатывать финансовые расчеты (я не пробовал, но я подозреваю, что так).

Библиотека множественной точности GNU (GMP) и библиотека MFPR GNU - это два бесплатных и открытых источника ресурсов для C и С++.

Существуют также числовые библиотеки точности для JavaScript (!), и я думаю, что PHP может обрабатывать финансовые расчеты.

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

Я не компьютерный ученый по образованию. Тем не менее, я склонен склоняться к BigDecimal в Java или десятичному значению в С#. Я не пробовал другие решения, которые я перечислил, но они, вероятно, тоже очень хороши.

Для меня мне нравится BigDecimal из-за методов, которые он поддерживает. С# decimal очень приятный, но у меня не было возможности работать с ним столько, сколько хотелось бы. Я занимаюсь научными вычислениями, представляющими интерес для меня в свое свободное время, и BigDecimal, кажется, работает очень хорошо, потому что я могу установить точность моих чисел с плавающей запятой. Недостаток BigDecimal? Это может быть медленным время от времени, особенно если вы используете метод разделения.

Вы можете, для скорости, заглянуть в бесплатные и проприетарные библиотеки в C, С++ и Fortran.

Ответ 14

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

Ответ 15

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

Ответ 16

Эти аргументы в пользу использования float для валюты смешны. Я мог бы кодировать платежную систему, используя клеточные автоматы, или исчисление лямбда, но это неправильный выбор. Для точных значений используйте точное представление. Целые числа или какой-то "десятичный" тип, который предоставляет большинство языков. Вся эта дискуссия является доказательством того, что разработчики программного обеспечения будут спорить о чем угодно, вплоть до смехотворности.

Ответ 17

Я достиг довольно приятной точности, просто имея дело с центами.

Вот класс:

public class Money implements Comparable<Money> {

    private static Locale CURRENT_LOCALE = new Locale("pt", "br");

    private Long amount = 0L;

    public Money() { }

    public Money(long cents) {
        super();
        this.setAmount(cents);
    }

    public Money(float cents) {
        super();
        this.setAmount(cents);
    }

    public Money(double cents) {
        super();
        this.setAmount(cents);
    }

    public void setAmount(Long cents) {
        this.amount = cents;
    }

    public void setAmount(Float amount) {
        this.amount = new Long(Math.round(amount * 100));
    }

    public void setAmount(Double amount) {
        this.amount = Math.round(amount * 100);
    }

    public Double amount() {
        return ((double) this.amount/100);
    }

    public Money add(Money portion) {
        if (amount != null) {
            this.amount += portion.amount;
        }
        return this;
    }

    public Money subtract(Money portion) {
        if (amount != null) {
            this.amount -= portion.amount;
        }
        return this;
    }

    public Money multiplyBy(double times) {
        this.amount = Math.round(this.amount * times);
        return this;
    }

    public Money divideBy(double divisor) {
        this.amount = Math.round(this.amount / divisor);
        return this;
    }

    @Override
    public String toString() {
        return NumberFormat.getCurrencyInstance(currentLocale()).format(amount());
    }

    @Override
    public int compareTo(Money value) {
        return (int) (amount - value.amount);
    }

    protected static void currentLocale(Locale locale) {
        CURRENT_LOCALE = locale;
    }

    protected static Locale currentLocale() {
        return CURRENT_LOCALE;
    }
}