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

857

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

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

  • 3
    Посмотрите этот ТАК вопрос: Ошибки округления?
  • 74
    Просто чтобы быть ясно, они не должны использоваться для чего-то, что требует точности - не только валюта.
Показать ещё 2 комментария
Теги:
floating-point
currency

16 ответов

893
Лучший ответ

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

  • 3
    @Fran Вы получите ошибки округления, и в некоторых случаях, когда используется большое количество валюты, вычисления процентной ставки могут быть сильно отклонены
  • 5
    ... большинство базовых 10 фракций, то есть. Например, 0.1 не имеет точного двоичного представления с плавающей точкой. Таким образом, 1.0 / 10 * 10 может не совпадать с 1.0.
Показать ещё 27 комментариев
295

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

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

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

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

выводит 0.6100000000000001.

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

  • 4
    Меня немного смущает рекомендация использовать int или long для денежных расчетов. Как вы представляете 1.03 как int или long? Я пробовал "long a = 1.04;" и "длинный а = 104/100;" но безрезультатно.
  • 42
    @Peter, вы используете long a = 104 и считаете в центах вместо долларов.
Показать ещё 9 комментариев
67

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

  • 2
    Связанный, интересный: В моей консоли chrome js: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
  • 13
    Этот ответ вводит в заблуждение. 1.40 * 165 = 231. Любое число, отличное от точно 231 , неверно в математическом смысле (и во всех других смыслах).
Показать ещё 9 комментариев
44

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

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

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

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

приводит к

1.1875
  • 10
    ЭТОТ!!!! Я искал все ответы, чтобы найти этот СООТВЕТСТВУЮЩИЙ ФАКТ !!! В обычных вычислениях никого не волнует, если вы стоите на долю процента, но здесь с большими числами легко теряются несколько долларов за транзакцию!
  • 14
    А теперь представьте, что кто-то получает ежедневный доход в размере 0,01% на свои 1 миллион долларов - он ничего не получает каждый день - и через год он не получит 1000 долларов, ЭТО БУДЕТ ВАЖНО
Показать ещё 1 комментарий
37

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

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

  • 2
    float , double и BigDecimal представляют точные значения. Преобразование кода в объект является неточным, как и другие операции. Сами типы не являются неточными.
  • 1
    @chux: перечитывая это, я думаю, у вас есть точка зрения, что моя формулировка может быть улучшена. Я отредактирую это и перефразирую.
17

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

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

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

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

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

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

Результат числа с плавающей запятой не является точным, что делает их непригодными для любого финансового расчета, который требует точного результата, а не приближения. 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
  • 0
    Давайте попробуем что-то иное, чем простое сложение / вычитание и целочисленное смещение. Если бы код вычислял месячную ставку ссуды в 7%, оба типа не смогли бы обеспечить точное значение и потребовали бы округления до ближайшего 0,01. Округление до самой низкой денежной единицы является частью расчета денег. Использование десятичных типов позволяет избежать этого с добавлением / вычитанием, но не более того.
7

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

4

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

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

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

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

2

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

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

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

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

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

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

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

1

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

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

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

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

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

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

  • 7
    Что делать, если вы берете плату за электроэнергию по 0,1 цента за единицу? А как насчет того, когда вы рассчитываете налоги и имеете значения ниже цента? Например, добавьте 1.2c + 1.9c, и теперь вам не хватает ни цента.
  • 2
    Тогда я бы использовал Integer или Long в единицах .1 цент, поэтому 1 = одна десятая процента, а 10 - цент.
Показать ещё 4 комментария
0

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

-6

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

Вот класс:

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;
    }
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню