Чому б не використовувати Double або Float для представлення валюти?


938

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

Я впевнений, що це дуже вагома причина, я просто не знаю, що це.


4
Дивіться це питання ТАК: Помилки округлення?
Джефф Огата

80
Щоб було зрозуміло, вони не повинні використовуватися для чогось, що вимагає точності - не лише для валюти.
Джефф

152
Вони не повинні використовуватися для нічого, що вимагає точності . Але подвійні 53 значущі біти (~ 16 десяткових цифр), як правило, досить хороші для речей, які просто вимагають точності .
dan04

21
@jeff Ваш коментар повністю хибно представляє, що двійкова плаваюча точка підходить і для чого це не годиться. Прочитайте відповідь на zneak нижче та видаліть ваш оманливий коментар.
Паскаль Куок

Відповіді:


983

Оскільки поплавці та парні не можуть точно представляти базові 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тип C # - тип.


3
@Fran Ви отримаєте помилки округлення, і в деяких випадках, коли використовуються великі кількості валюти, обчислення процентних ставок можна грубо відмовити
linuxuser27

5
... більшість базових 10 фракцій, тобто. Наприклад, 0,1 не має точного бінарного подання з плаваючою комою. Отже, 1.0 / 10 * 10може не збігатися з 1,0.
Кріс Єстер-Янг

6
@ linuxuser27 Я думаю, що Фран намагався бути смішним. У будь-якому випадку, відповідь zneak - це найкраще, що я бачив, краще навіть ніж класична версія від Bloch.
Ісаак Рабінович

5
Звичайно, якщо ви знаєте точність, ви завжди можете округлити результат і таким чином уникнути цілого питання. Це набагато швидше і простіше, ніж використання BigDecimal. Іншою альтернативою є використання фіксованої точності int або long.
Пітер Лорі

2
@zneak Ви знаєте, що раціональні числа - це підмножина реальних чисел, правда? Реальні числа IEEE-754 - це дійсні числа. Вони просто виявилися також бути раціональним.
Тім Сегуїн

314

З Bloch, J., Ефективна Java, 2-е видання, пункт 48:

Ці floatта doubleтипи особливо не підходять для грошових розрахунків, оскільки неможливо представити 0,1 (або будь-яку іншу негативну силу десять) як таку floatчи doubleточну.

Наприклад, припустимо, що у вас є 1,03 долара, а ви витрачаєте 42 градуси. Скільки грошей у вас залишилось?

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 ... і ти не думаєш, що використання дубля для таких речей є помилковим? Я бачив, як питання повороту поплавця важко вразило реальні системи . Навіть у банківській справі. Будь ласка , не рекомендується, або якщо ви робите, передбачено , що в якості окремого відповіді (так що ми можемо downvote його: P)
EIS

75

Це не питання точності, а також не питання точності. Це питання задоволення очікувань людей, які використовують базу 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
Пов’язано, цікаво: У моїй хромовій консолі js: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Curtis Yallop

16
Ця відповідь вводить в оману. 1,40 * 165 = 231. Будь-яке число, крім 231, є неправильним у математичному розумінні (та у всіх інших сенсах).
Кару

2
@Karu Я думаю, що тому Ренді каже, що плавання погані ... Моя консоль Chrome JS показує 230,99999999999997 як результат. Те є не так, що точка зроблено у відповідь.
trusktr

6
@Karu: Імхо, відповідь не є математично помилковою. Просто є два питання, на одне відповіді на які, а не на запитання. Питання, на яке відповідає ваш компілятор, - це 1.39999999 * 164.99999999 і так далі, математично правильне дорівнює 230.99999 .... Очевидно, це не питання, яке задавали в першу чергу ....
markus

2
@CurtisYallop тому, що закривається подвійне значення до 0,49999999999999999 - 0,5 Чому Math.round(0.49999999999999994)повертається 1?
phuclv

53

Мене турбують деякі з цих відповідей. Я думаю, що парні і плавці мають місце у фінансових розрахунках. Звичайно, при додаванні та відніманні нефракційних грошових сум не буде втрати точності при використанні цілих класів або класів BigDecimal. Але виконуючи більш складні операції, ви часто закінчуєтесь результатами, які виходять з декількох чи багатьох десяткових знаків, незалежно від того, як ви зберігаєте числа. Питання полягає в тому, як ви представляєте результат.

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

Проблема з парними, а тим більше з поплавцями, полягає в тому, коли вони використовуються для поєднання великої кількості та невеликої кількості. У java,

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

призводить до

1.1875

16
ЦЕ !!!! Я шукав усі відповіді, щоб знайти цей ВІДПОВІДНИЙ ФАКТ !!! У звичайних підрахунках нікого не хвилює, якщо ви на якусь частку відсотка, але тут з високими цифрами легко долари втрачаються за транзакцію!
Фалько

22
А тепер уявіть, що хтось отримує щоденний дохід у розмірі 0,01% від своїх 1 мільйонів доларів - він би нічого не отримував щодня - і через рік він не отримав 1000 доларів, ЦЕ БУДЕ ВАМИ
Falco

6
Проблема не в точності, але те, що поплавок не говорить про те, що він стає неточним. Ціле число може містити до 10 цифр, поплавок може містити до 6, не стаючи неточним (якщо ви вирізаєте його відповідно). Це дозволяє це робити, тоді як ціле число отримує переповнення, а мова на зразок java попередить вас або не дозволить. Якщо ви використовуєте подвійний, ви можете отримати до 16 цифр, що достатньо для багатьох випадків використання.
sigi

39

Поплавці та дублі - приблизні. Якщо ви створите BigDecimal і передасте поплавок в конструктор, ви побачите, що саме float дорівнює:

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

можливо, це не так, як ви хочете представляти $ 1,01.

Проблема полягає в тому, що специфікація IEEE не має способу точно представити всі дроби, деякі з них закінчуються як повторювані дроби, так що ви отримуєте помилки наближення. Оскільки бухгалтерам подобається, що речі виходять точно до копійки, а клієнти будуть роздратовані, якщо вони оплатять рахунок і після сплати платежу вони зобов'язані .01, і їм стягується плата або не можуть закрити свій рахунок, краще скористатися точні типи, як десятковий (у C #) або java.math.BigDecimal на Java.

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


5
float, doubleі BigDecimalпредставляють точні значення. Перетворення коду в об'єкт є неточним, як і інші операції. Самі типи не є точними.
chux

1
@chux: перечитавши це, я думаю, ви маєте сенс, що моє формулювання могло б бути вдосконалено. Я відредагую це і переформулюю.
Натан Х'юз

28

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

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

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


3
Це насправді досить гідна відповідь. У більшості випадків їх цілком чудово використовувати.
Вахід Амірі

2
Слід зазначити, що більшість інвестиційних банків використовують подвійно, як і більшість програм C ++. Деякі користуються довго, але, таким чином, є своя проблема відстеження масштабу.
Пітер Лоурі

20

Хоча це правда, що тип з плаваючою комою може представляти лише приблизні десяткові дані, але також правда, що якщо один округлює числа до необхідної точності перед їх поданням, ви отримуєте правильний результат. Зазвичай.

Зазвичай тому, що подвійний тип має точність менше 16 фігур. Якщо вам потрібна краща точність, це не підходящий тип. Також можуть накопичуватися наближення.

Потрібно сказати, що навіть якщо ви використовуєте арифметику з фіксованою точкою, ви все одно повинні округляти числа, чи не факт, що BigInteger і BigDecimal дають помилки, якщо отримуєте періодичні десяткові числа. Тож є наближення і тут.

Наприклад, COBOL, який історично використовувався для фінансових розрахунків, має максимальну точність 18 цифр. Тому часто відбувається неявне округлення.

Підсумовуючи, на мою думку, дубль непридатний здебільшого за його 16-значну точність, що може бути недостатньою, не тому, що є приблизною.

Розглянемо наступний результат наступної програми. Звідси видно, що після округлення подвійні дають той же результат, що і BigDecimal, до точності 16.

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

15

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

@supercat У моєму коментарі підкреслюється використання двійкового FP найменшої грошової одиниці або десяткової FP, і в них виникають подібні проблеми округлення - як у вашому коментарі з "і розділити, округлюючи до найближчої копійки". Використання бази 2 або 10 FP не дає переваги в будь-якому випадку у вашому сценарії.
chux

@ chux-ReinstateMonica: У вищенаведеному сценарії, якщо математика виявляється, що відсоток повинен бути точно рівний деякій кількості півцентрів, правильна фінансова програма повинна округлюватись точно вказаним чином. Якщо підрахунки з плаваючою комою дають значення відсотка, наприклад, $ 1,23499941, але математично точне значення перед округленням повинно було становити 1,235 дол. знижуватиметься на 0,000059 доларів, а скоріше на цілих 0,01 дол. США, що для цілей бухгалтерського обліку - це просто звичайна помилка.
supercat

@supercat Використання двійкового doubleFP до центру не матиме жодних проблем з розрахунком до 0,5 відсотка, як і десятковий FP. Якщо обчислення з плаваючою комою дають значення відсотка, наприклад, 123.499941 ¢, або через двійкову FP, або через десяткову 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"));

ВалютаУніта та грошова сума

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

Грошові суми можна округлити за допомогою оператора округлення:

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 9 грошей та валюти Java 9 (JSR 354)

Див. Також: JSR 354 - Валюта та гроші


5

Якщо ваші обчислення включають різні етапи, арифметика довільної точності не покриє вас на 100%.

Єдиний надійний спосіб використовувати ідеальне подання результатів (Використовуйте спеціальний тип даних фракції, який буде проводити операції поділу до останнього кроку) та конвертувати лише в десяткові позначення на останньому кроці.

Довільна точність не допоможе, оскільки завжди можуть бути числа з такою кількістю десяткових знаків, або деякі результати, такі як 0.6666666... Жодне довільне представлення не покриє останній приклад. Так у вас будуть невеликі помилки на кожному кроці.

Ці помилки стануть доповненнями, з часом можуть ігноруватися більше непросто. Це називається Поширення помилок .


4

Більшість відповідей висвітлюють причини, чому не слід використовувати подвійні гроші для розрахунку грошей та валюти. І я з ними повністю згоден.

Це не означає, що подвійне неможливо використовувати для цієї мети.

Я працював над низкою проектів з дуже низькими вимогами до gc, і наявність об'єктів BigDecimal була великим фактором у цій роботі.

Це нерозуміння щодо подвійного представництва та відсутність досвіду поводження з точністю та точністю, що викликає цю мудру пропозицію.

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

Щоб отримати більше уявлень, ви можете скористатися методом FuzzyCompare від guava. Допуск параметрів є ключовим. Ми вирішили цю проблему для заявки на торгівлю цінними паперами, і ми провели вичерпне дослідження того, які допуски використовувати для різних числових значень у різних діапазонах.

Також можуть виникнути ситуації, коли ви спокушаєтесь використовувати подвійні обгортки як ключ картки, а хеш-карта є реалізацією. Це дуже ризиковано, тому що Double.equals і хеш-код, наприклад, значення "0,5" та "0,6 - 0,1", спричинить великий безлад.


2

Багато відповідей на це питання обговорюють IEEE та стандарти навколо арифметики з плаваючою комою.

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

Які альтернативи? Є багато (і ще багато з яких я не знаю!).

BigDecimal на Java є рідною мовою Java. Apfloat - ще одна довільно-точна бібліотека для Java.

Десятковий тип даних у C # є альтернативою Microsoft .NET для 28 значущих цифр.

SciPy (Scientific Python), ймовірно, може також обробляти фінансові розрахунки (я не намагався, але я підозрюю, що так).

Бібліотека множинної точності GNU (GMP) та Бібліотека GNU MFPR - це два безкоштовні та відкриті ресурси для C та C ++.

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

Існують також фірмові (зокрема, я думаю, для Fortran) та рішення з відкритим кодом, а також для багатьох мов комп'ютерів.

Я не є інформатиком за навчанням. Однак я схиляюся до BigDecimal на Java або до десяткової у C #. Я не пробував інших перерахованих нами рішень, але вони, мабуть, також дуже хороші.

Для мене мені подобається BigDecimal через методи, які він підтримує. Десятковий знак C # дуже приємний, але я не мав можливості працювати з ним стільки, скільки хотів би. Я займаюся науковими розрахунками, що цікавлять мене у вільний час, і, здається, BigDecimal працює дуже добре, бо можу встановити точність чисел з плаваючою комою. Недолік BigDecimal? Часом це може бути повільно, особливо якщо ви використовуєте метод ділення.

Можливо, для швидкості загляньте у безкоштовні та власні бібліотеки C, C ++ та Fortran.


1
Що стосується SciPy / Numpy, фіксована точність (тобто десяткова величина Python) не підтримується ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Деяка функція не буде належним чином працювати з Decimal (наприклад, isnan). Панда заснована на Numpy і була ініційована в AQR, одному з основних кількісних хедж-фондів. Отже, у вас є відповідь щодо фінансових розрахунків (а не продуктового обліку).
comte

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!!!


3
Проблема полягає не в тому, що помилка округлення трапляється, а в тому, що ви з цим не впораєтеся. Округніть результат на два десяткових знаки (якщо ви хочете центів) і ви закінчите.
maaartinus

0

Float - двійкова форма десяткової форми з різним дизайном; це дві різні речі. Між двома типами помилок при перетворенні один на одного є невеликими. Також поплавок призначений для відображення нескінченної великої кількості значень для наукових. Це означає, що він розрахований на втрату точності до надзвичайно малої та надзвичайно великої кількості з такою фіксованою кількістю байтів. Десяткове число не може представляти нескінченну кількість значень, воно прив'язується до саме такої кількості десяткових цифр. Отже, Float і Decimal призначені для різних цілей.

Є кілька способів управління помилкою щодо вартості валюти:

  1. Використовуйте довге ціле число і рахуйте в копіях замість цього.

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

  3. Використовуйте десяткову бібліотеку, як Java BigDecimal, тому вам не потрібно використовувати подвійний, щоб імітувати десятковий.

ps цікаво знати, що більшість марок портативних наукових калькуляторів працює на десятковий, а не поплавковий. Тож жодна скарга не плаває помилок перетворення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.