통화를 나타 내기 위해 Double 또는 Float을 사용하지 않습니까?


938

나는 항상 돈 이나 유형 을 대표 하지 말라고 들었습니다. 이번에는 나는 당신에게 질문을 제기합니다 : 왜?doublefloat

나는 아주 좋은 이유가 있다고 확신합니다. 나는 그것이 무엇인지 모릅니다.


4
이 SO 질문을 참조하십시오 : 반올림 오류?
Jeff Ogata

80
분명히 말하면 통화뿐만 아니라 정확성이 필요한 모든 것에 사용해서는 안됩니다.
Jeff

152
정확성 을 요구하는 것에 사용해서는 안됩니다 . 그러나 double의 53 개의 유효 비트 (~ 16 진수)는 일반적으로 정확도가 필요한 것들에 충분합니다 .
dan04

21
@jeff 귀하의 의견은 바이너리 부동 소수점이 무엇이고 무엇이 좋지 않은지를 완전히 잘못 나타냅니다. 아래에서 zneak의 답변을 읽고 오해의 소지가있는 의견을 삭제하십시오.
Pascal Cuoq

답변:


983

float와 double은 우리가 돈을 위해 사용하는 기본 10 배수를 정확하게 나타낼 수 없기 때문입니다. 이 문제는 Java만을위한 것이 아니라 기본 2 부동 소수점 유형을 사용하는 모든 프로그래밍 언어에 관한 것입니다.

10 진법에서는 10.25를 1025 * 10으로 쓸 수 있습니다. 10-2 (정수 곱하기 10의 거듭 제곱) . IEEE-754 부동 소수점 숫자 는 다르지만이를 생각하는 매우 간단한 방법은 2의 거듭 제곱을 곱하는 것입니다. 예를 들어 164 * 2 -4 (정수 곱하기 2의 거듭 제곱)를 볼 수 있으며 10.25와 같습니다. 그것이 숫자가 메모리에 표현되는 방식은 아니지만 수학적인 의미는 동일합니다.

10 진법에서도이 표기법은 가장 간단한 분수를 정확하게 나타낼 수 없습니다. 예를 들어 1/3을 표현할 수 없습니다. 10 진수 표현이 반복되고 (0.3333 ...), 10의 거듭 제곱에 곱하여 1/3을 구할 수있는 유한 정수는 없습니다. 333333333 * 10 -10 과 같이 3의 긴 시퀀스와 작은 지수를 정할 수는 있지만 정확하지는 않습니다. 3을 곱하면 1을 얻지 못합니다.

그러나 돈을 세는 목적으로, 적어도 돈이 미국 달러의 규모 내에서 가치가있는 국가의 경우 보통 10-2의 배수를 저장할 수 있기 때문에 중요하지 않습니다. 1/3은 표현할 수 없습니다.

수레와 복식의 문제는 실수 대부분 의 돈과 같은 숫자가 정수 곱하기 2의 정수로 정확하게 표현되지 않는다는 것입니다. 실제로, 0과 1 사이의 0.01의 유일한 배수는 (처리 할 때 중요합니다) IEEE-754 이진 부동 소수점 숫자로 정확하게 표현할 수있는 정수 센트이므로 돈으로) 0, 0.25, 0.5, 0.75 및 1입니다. 나머지는 모두 소량입니다. 0.333333 예제와 유사하게 0.1의 부동 소수점 값을 가져 와서 10을 곱하면 1이되지 않습니다.

돈을 대표하는 doublefloat소프트웨어가 작은 오류를 반올림하면 또는 처음에 좋아 보일 것입니다. 정확하지 않습니다. 이로 인해 10 개의 기본 거듭 제곱의 완벽한 정확도가 필요한 돈을 처리하기에 플로트와 배가 부적절합니다.

거의 모든 언어로 작동하는 솔루션은 정수를 대신 사용하고 센트를 계산하는 것입니다. 예를 들어 1025는 $ 10.25입니다. 일부 언어에는 돈을 처리하기위한 내장 유형이 있습니다. 무엇보다도 Java에는 BigDecimal클래스가 있고 C #에는 decimal유형이 있습니다.


3
@Fran 당신은 반올림 오류를 얻고 통화의 많은 양이 사용되는 경우에, 금리 계산 오프 크게 할 수있다
linuxuser27

5
... 가장 밑이 10 인 분수입니다. 예를 들어 0.1에는 이진 부동 소수점 표현이 없습니다. 따라서 1.0 / 10 * 101.0과 같지 않을 수 있습니다.
Chris Jester-Young

6
@ linuxuser27 나는 프랜이 웃기려고했다고 생각합니다. 어쨌든 zneak의 대답은 내가 본 것 중 최고이며 Bloch의 클래식 버전보다 좋습니다.
Isaac Rabinovitch

5
물론 정밀도를 알고 있다면 항상 결과를 반올림하여 전체 문제를 피할 수 있습니다. 이것은 BigDecimal을 사용하는 것보다 훨씬 빠르고 간단합니다. 다른 대안은 고정 정밀도 int 또는 long을 사용하는 것입니다.
Peter Lawrey 2012

2
@zneak 당신은 유리수 가 실수 의 부분 집합 이라는 것을 알고 있습니까? IEEE-754 실수 실수입니다. 그들은 또한 합리적입니다.
Tim Seguine

314

Bloch, J., Effective Java, 2nd ed, 항목 48에서 :

floatdouble는 0.1 (또는 임의의 다른 열 음극 전원)이 같은 표현하는 것은 불가능하기 때문에, 특히 화폐 종류 계산에 부적당된다 float또는 double정확하게한다.

예를 들어, $ 1.03이고 42c를 소비한다고 가정하십시오. 돈이 얼마나 남았습니까?

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

인쇄합니다 0.6100000000000001.

이 문제를 해결하는 올바른 방법은 사용하는 것입니다 BigDecimal, int또는 long 금전적 계산.

BigDecimal몇 가지주의 사항이 있지만 (현재 허용되는 답변을 참조하십시오).


6
통화 계산에 int 또는 long을 사용하는 것이 좋습니다. 1.03을 int 또는 long으로 어떻게 표현합니까? "long a = 1.04;"를 시도했습니다 및 "long a = 104/100;" 아무 소용이 없습니다.
Peter

49
@ 피터, 당신 long a = 104은 달러 대신 센트로 사용 하고 계산합니다.
zneak

@zneak 복리 관심사 또는 이와 유사한 비율을 적용해야하는 경우는 어떻습니까?
trusktr

3
@ trusktr, 나는 당신의 플랫폼의 10 진수 유형으로 갈 것입니다. 자바에서 그것은 BigDecimal입니다.
zneak

13
@maaartinus ... 그리고 그런 일에 double을 사용하는 것이 오류가 발생하기 쉽다고 생각하지 않습니까? 나는 부동 소수점 반올림 문제가 실제 시스템에 충돌 보았다 하드 . 뱅킹에서도. 권장하지 않거나 그렇지 않은 경우 별도의 답변으로 제공하십시오 (따라서 공감할 수 있습니다 : P)
eis

75

이것은 정확성의 문제가 아니며 정확성의 문제도 아닙니다. 기초 2 대신 계산에 기초 10을 사용하는 인간의 기대치를 충족시키는 것이 문제입니다. 예를 들어, 재무 계산에 복식을 사용하면 수학적으로 "틀린"답이 나오지 않지만 다음과 같은 답을 얻을 수 있습니다. 재정적 인 의미에서 기대되는 것이 아닙니다.

출력 전 마지막 순간에 결과를 반올림하더라도 기대치에 맞지 않는 배가를 사용하여 결과를 얻을 수 있습니다.

계산기를 사용하거나 직접 손으로 결과를 계산하면 1.40 * 165 = 231입니다. 그러나 내부적으로 내 컴파일러 / 운영 체제 환경에서 doubles를 사용하면 230.99999에 가까운 이진수로 저장됩니다. 따라서 숫자를 자르면 231 대신 230이됩니다. 잘라 내기 대신 반올림하는 이유는 다음과 같습니다. 231의 원하는 결과를 얻었습니다. 사실이지만 반올림은 항상 잘립니다. 어떤 반올림 기술을 사용하든 반올림을 기대할 때 반올림되는 경계 조건이 여전히 있습니다. 그들은 일상적인 테스트 또는 관찰을 통해 종종 발견되지 않을 정도로 희귀합니다. 예상대로 작동하지 않는 결과를 나타내는 예제를 검색하려면 코드를 작성해야 할 수도 있습니다.

가장 가까운 페니로 반올림한다고 가정하십시오. 따라서 최종 결과에 100을 곱하고 0.5를 더한 다음 잘라 내고 결과를 100으로 나누어 페니로 돌아갑니다. 저장 한 내부 번호가 3.465 대신 3.46499999 .... 인 경우 가장 가까운 페니로 반올림하면 3.47 대신 3.47이됩니다. 그러나 기본 10 계산에서는 답이 정확히 3.465 여야하며, 3.46이 아니라 3.47로 올림해야합니다. 이러한 종류의 일은 재무 계산에 복식을 사용할 때 실제 생활에서 가끔 발생합니다. 드물기 때문에 종종 문제로 눈에 띄지 않지만 발생합니다.

배가 아닌 내부 계산에 기수 10을 사용하면 코드에 다른 버그가 없다고 가정 할 때 항상 사람이 기대하는 것입니다.


2
관련, 재미있는 : 내 크롬 js 콘솔에서 : Math.round (.4999999999999999) : 0 Math.round (.49999999999999999) : 1
커티스

16
이 답변은 잘못된 것입니다. 정확히 231 이외 1.40 * 165 = 231 임의의 수 이다 수학적 센스 (다른 모든 감각)에 잘못.
Karu

2
@Karu Randy가 플로트가 나쁘다고 말하는 이유라고 생각합니다. Chrome JS 콘솔에 230.99999999999997이 표시됩니다. 즉 이다 이 질문에 만든 점이다, 잘못.
trusktr

6
@ 카루 : Imho 대답은 수학적으로 잘못되지 않습니다. 질문에 대한 질문이 아닌 질문에 대한 두 가지 질문이 있습니다. 질문 컴파일러 답변은 .... 1.39999999 * 164.99999999 등 수학적으로 정확한 등호 230.99999이 .... 분명히 그쪽은 처음에 질문을 받았다 문제는 아니다하는 것입니다
마르쿠스

2
@CurtisYallop은 0.49999999999999999에 대한 double double 값이 0.5이므로 1을 반환하는 이유는 무엇 Math.round(0.49999999999999994)입니까?
phuclv

53

나는 이러한 응답 중 일부에 어려움을 겪고 있습니다. 나는 복식과 수레가 재무 계산에 자리를 잡고 있다고 생각합니다. 확실히, 분수가 아닌 화폐 금액을 더하고 뺄 때 정수 클래스 또는 BigDecimal 클래스를 사용할 때 정밀도가 손실되지 않습니다. 그러나 더 복잡한 연산을 수행 할 때 숫자를 저장하는 방법에 관계없이 종종 소수 또는 소수 자릿수로 끝나는 결과가 나타납니다. 문제는 결과를 제시하는 방법입니다.

결과가 반올림과 반올림 사이의 경계선에 있고 마지막 페니가 실제로 중요하다면, 소수 자릿수를 더 많이 표시하여 답변이 거의 중간에 있음을 시청자에게 알려 주어야합니다.

복식의 문제, 그리고 수레의 문제는 큰 숫자와 작은 숫자를 결합하는 데 사용되는 경우입니다. 자바에서는

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

결과

1.1875

16
이!!!! 나는이 관련 사실을 찾기 위해 모든 대답을 찾고있었습니다! 정상적인 계산에서는 아무도 당신이 센트의 몇 분의 1을 차지하는지 신경 쓰지 않지만, 높은 숫자를 사용하면 거래 당 몇 달러가 손실됩니다!
Falco

22
이 의지 MATTER 1000 달러를 1 년 후 그는 못했다 - 그는 매일 아무것도 얻지 않을 것이다 - 그리고 지금 누군가가 그의 1 백만 달러에 0.01 %의 통행료 수입을 얻는 상상
팔코

6
문제는 정확성은 아니지만 플로트가 정확하지 않다는 것을 알려주지는 않습니다. 정수는 최대 10 자리까지만 보유 할 수 있습니다. 부동 소수점은 부정확하지 않고 최대 6 자리를 보유 할 수 있습니다 (잘라낼 때). 정수가 오버플로되고 java와 같은 언어가 경고하거나 허용하지 않는 동안 이것을 허용합니다. 두 배를 사용하면 많은 사용 사례에 충분한 최대 16 자리 숫자를 사용할 수 있습니다.
sigi

39

수레와 복식은 근사치입니다. BigDecimal을 생성하고 float를 생성자에 전달하면 float이 실제로 동일한 것을 볼 수 있습니다.

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

이것은 아마도 당신이 $ 1.01을 표현하고 싶지 않은 방법 일 것입니다.

문제는 IEEE 사양에 모든 분수를 정확하게 표현할 수있는 방법이 없으며, 일부는 반복 분수로 끝나기 때문에 근사 오차가 발생한다는 것입니다. 회계사가 정확히 1 페니로 나오고 고객이 청구서를 지불하면 고객이 화가 났고 지불이 처리 된 후 .01을 지불해야하며 수수료가 부과되거나 계정을 폐쇄 할 수 없으므로 사용하는 것이 좋습니다 소수점 이하 자릿수 (C #) 또는 Java의 java.math.BigDecimal과 같은 정확한 유형

반올림하면 오류를 제어 할 수있는 것은 아닙니다 . Peter Lawrey의이 기사를 참조하십시오 . 처음에는 반올림하지 않아도 더 쉽습니다. 돈을 처리하는 대부분의 응용 프로그램은 많은 수학을 요구하지 않습니다. 작업은 물건을 추가하거나 다른 버킷에 금액을 할당하는 것으로 구성됩니다. 부동 소수점과 반올림을 도입하면 문제가 복잡해집니다.


5
float, double하고 BigDecimal있는 대표 정확한 값을. 다른 작업뿐만 아니라 코드에서 개체로의 변환도 정확하지 않습니다. 유형 자체는 정확하지 않습니다.
chux-복원 Monica Monica

1
@ chux : 이것을 다시 읽으면 내 문구가 향상 될 수 있다고 생각합니다. 이것을 편집하고 다시 말하겠습니다.
Nathan Hughes

28

하락세가 발생할 위험이 있지만 통화 계산에 부동 소수점 숫자의 부적합이 과대 평가되었다고 생각합니다. zneak이 설명하는 2 진법 표현 불일치를 해결하기 위해 센트 반올림을 올바르게 수행하고 작업하기에 충분한 유효 자릿수가있는 한 문제는 없습니다.

Excel에서 통화로 계산하는 사람들은 항상 배정 밀도 부동 소수점을 사용했으며 (Excel에는 통화 유형이 없습니다) 반올림 오류에 대해 불평하는 사람은 아직 없습니다.

물론, 당신은 이성 안에 머물러 있어야합니다. 예를 들어 간단한 웹숍은 배정 밀도 부동 소수점에 문제가 발생하지 않지만 회계 또는 많은 양의 (제한되지 않은) 숫자를 추가 해야하는 다른 작업을 수행하는 경우 10 피트로 부동 소수점 숫자를 만지고 싶지 않습니다. 폴.


3
이것은 실제로 꽤 괜찮은 대답입니다. 대부분의 경우 사용하는 것이 좋습니다.
Vahid Amiri

2
대부분의 투자 은행은 대부분의 C ++ 프로그램과 마찬가지로 이중을 사용합니다. 일부는 오래 사용하지만 추적 척도 자체 문제가 있습니다.
Peter Lawrey

20

부동 소수점 유형은 근사 적으로 십진 데이터 만 나타낼 수 있지만 숫자를 제시하기 전에 필요한 정밀도로 반올림하면 올바른 결과를 얻는다는 것도 사실입니다. 보통.

일반적으로 이중 유형의 정밀도는 16 자리 미만입니다. 더 나은 정밀도가 필요한 경우 적합한 유형이 아닙니다. 또한 근사치가 누적 될 수 있습니다.

고정 소수점 산술을 사용하더라도 여전히 숫자를 반올림해야하는데,주기적인 10 진수를 얻는 경우 BigInteger 및 BigDecimal에서 오류가 발생하지 않기 때문입니다. 여기에도 근사치가 있습니다.

예를 들어 역사적으로 재무 계산에 사용 된 COBOL의 최대 정밀도는 18입니다. 따라서 종종 암시 적 반올림이 있습니다.

결론적으로, 이중은 16 자리 정밀도에 부적합하며, 이는 대략적이기 때문에 충분하지 않을 수 있습니다.

후속 프로그램의 다음 출력을 고려하십시오. double을 반올림 한 후에는 BigDecimal과 동일한 결과를 정밀도 16까지 제공합니다.

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

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

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

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

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

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

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

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

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

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

        //calculation
        double price0 = amount0 / quantity0;

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

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

        //calculation
        double price0 = amount0 / quantity0;

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

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

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

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

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

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

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

15

부동 소수점 숫자의 결과는 정확하지 않으므로 정확한 결과가 필요하고 근사값이 아닌 재무 계산에 적합하지 않습니다. float 및 double은 공학 및 과학 계산 용으로 설계되었으며 여러 번 정확한 결과를 생성하지 않으며 부동 소수점 계산 결과는 JVM마다 다를 수 있습니다. 돈 가치를 나타내는 데 사용되는 BigDecimal 및 double primitive의 예를 아래에서 살펴보십시오. 부동 소수점 계산이 정확하지 않을 수 있으며 재무 계산에 BigDecimal을 사용해야한다는 것이 분명합니다.

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

산출:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

3
코드가 7 % 대출의 월간 이율을 계산한다면, 두 유형 모두 정확한 값을 제공하지 못하고 가장 가까운 0.01로 반올림해야합니다. 가장 낮은 통화 단위로 반올림하는 것은 돈 계산의 일부입니다. 소수 유형을 사용하면 더하기 / 빼기가 필요하지 않습니다.
chux-복원 모니카

@ chux-ReinstateMonica :이자가 매월 복리로 계산되면 일일 잔고를 합산하여 매월이자를 계산하고 7 (이자율)을 곱한 다음 가장 가까운 페니로 반올림하여 일 수로 나눕니다. 그 해. 마지막 단계에서 한 달에 한 번을 제외하고는 반올림이 없습니다.
슈퍼 캣

@supercat 내 의견 은 가장 작은 화폐 단위의 이진 FP 또는 10 진수 FP를 사용하면 비슷한 반올림 문제가 발생한다는 점을 강조합니다. "및 나누기, 가장 가까운 페니로 반올림"이라는 의견과 유사합니다. Base 2 또는 Base 10 FP를 사용한다고해서 시나리오에서 어느 쪽이든 유리하지는 않습니다.
chux – 복원 Monica Monica

@ chux-ReinstateMonica : 위의 시나리오에서, 수학이이자가 정확히 반 센트의 수와 같아야한다는 것을 계산하면 정확한 재무 프로그램이 정확하게 지정된 방식으로 반올림해야합니다. 부동 소수점 계산이 $ 1.23499941의이자 값을 산출하지만 반올림 전의 수학적으로 정확한 값은 $ 1.235이고 반올림이 "가장 짝수"로 지정된 경우 이러한 부동 소수점 계산을 사용하면 결과가 발생하지 않습니다. $ 0.000059 할인이 아닌 전체 $ 0.01 할인으로 회계 목적 상 Just Plain Wrong입니다.
supercat

@supercat 바이너리 doubleFP를 센트로 사용하면 FP를 소수점으로 계산하지 않아도 0.5 센트로 계산하는 데 문제가 없습니다. 부동 소수점 계산이 이진 FP 또는 십진 FP를 통해 123.499941 ¢의이자 값을 생성하는 경우 이중 반올림 문제는 동일합니다. 전제는 수학적으로 정확한 값으로 가정하고 십진수 FP는 동일하다고 가정합니다. 십진 FP조차도 보장하지 않습니다. 0.5 / 7.0 * 7.0은 이진 및 십진 FP의 문제입니다. IAC는 다음 버전의 C가 십진 FP를 제공 할 것으로 기대하기 때문에 대부분은 혼란 스러울 것입니다.
chux-

11

앞서 언급했듯이 소프트웨어가 작은 오류를 반올림하면 돈을 이중 또는 부동으로 표현하는 것이 처음에는 좋아 보일 것입니다. 이로 인해 10 개의 기본 거듭 제곱의 배수에 대한 완벽한 정확도가 필요한 돈을 처리하기에는 플로트와 배가 부적절합니다. "

마지막으로 Java에는 Currency And Money를 사용하는 표준 방법이 있습니다!

JSR 354 : 화폐 및 통화 API

JSR 354는 Money and Currency를 사용하여 포괄적 인 계산을 표현, 전송 및 수행하기위한 API를 제공합니다. 이 링크에서 다운로드 할 수 있습니다.

JSR 354 : 화폐 및 통화 API 다운로드

사양은 다음과 같이 구성됩니다.

  1. 화폐 금액 및 통화 처리를위한 API
  2. 상호 교환 가능한 구현을 지원하는 API
  3. 구현 클래스의 인스턴스를 작성하기위한 팩토리
  4. 금액 계산, 변환 및 서식 지정 기능
  5. Java 9에 포함될 예정인 Money and Currencies 작업을위한 Java API
  6. 모든 사양 클래스와 인터페이스는 javax.money. * 패키지에 있습니다.

JSR 354의 샘플 예 : Money and Currency 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

MonetaryAmount 콜렉션으로 작업 할 때 필터링, 정렬 및 그룹화를위한 유용한 유틸리티 메소드를 사용할 수 있습니다.

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"));

사용자 정의 통화 금액 조작

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

자원:

JSR 354를 사용하여 Java에서 돈과 통화 처리

Java 9 Money and Currency API 살펴보기 (JSR 354)

다음 사항도 참조 : JSR 354-통화 및 돈


5

계산에 다양한 단계가 포함 된 경우 임의 정밀도 연산은 100 %를 포함하지 않습니다.

완벽한 결과 표현을 사용하는 신뢰할 수있는 유일한 방법 (마지막 단계로 나누기 작업을 수행하는 사용자 지정 분수 데이터 형식 사용)은 마지막 단계에서 10 진수 표기법으로 만 변환합니다.

소수점 이하 자릿수가 너무 많은 숫자가 항상있을 수 있기 때문에 임의의 정밀도는 도움이되지 않습니다 0.6666666. 따라서 각 단계마다 작은 오류가 발생합니다.

이러한 오류는 더해져 결국에는 더 이상 무시하기 쉽지 않을 수 있습니다. 이것을 오류 전파 라고 합니다.


4

대부분의 답변은 돈과 통화 계산에 복식을 사용해서는 안되는 이유를 강조했습니다. 그리고 나는 그들에게 완전히 동의합니다.

복식을 절대 그 목적으로 사용할 수는 없습니다.

나는 매우 낮은 gc 요구 사항을 가진 많은 프로젝트에서 일했으며 BigDecimal 객체를 갖는 것이 그 오버 헤드에 큰 기여를했습니다.

이중 표현에 대한 이해 부족과이 현명한 제안을 제공하는 정확성과 정밀도 처리 경험이 부족합니다.

프로젝트의 정밀도 및 정확도 요구 사항을 처리 할 수 ​​있으면 처리 할 수 ​​있습니다.이 범위는 다루는 이중 값의 범위를 기반으로 수행되어야합니다.

구아바의 FuzzyCompare 방법을 참조하여 더 많은 아이디어를 얻을 수 있습니다. 파라미터 공차가 핵심입니다. 우리는 증권 거래 애플리케이션에 대해이 문제를 다루었 고, 다른 범위의 다른 숫자 값에 사용할 공차를 철저히 조사했습니다.

또한 이중 래퍼를 해시 맵이 구현 된 맵 키로 사용하려는 경우가 있습니다. Double.equals 및 해시 코드 (예 : 값 "0.5"및 "0.6-0.1")가 큰 혼란을 초래하기 때문에 매우 위험합니다.


2

이 질문에 게시 된 많은 답변은 IEEE와 부동 소수점 산술 관련 표준에 대해 설명합니다.

컴퓨터 과학이 아닌 배경 (물리 및 공학)에서 나온 문제를 다른 관점에서 보는 경향이 있습니다. 나에게 수학 계산에서 double 또는 float을 사용하지 않는 이유는 너무 많은 정보를 잃어 버릴 수 있기 때문입니다.

대안은 무엇입니까? 많은 것들이 있습니다.

Java의 BigDecimal은 Java 언어에 고유합니다. Apfloat는 Java를위한 또 다른 임의 정밀도 라이브러리입니다.

C #의 10 진수 데이터 형식은 28 개의 유효 숫자에 대한 Microsoft의 .NET 대안입니다.

SciPy (Scientific Python)는 아마도 재무 계산을 처리 할 수도 있습니다 (시도하지는 않았지만 그렇게 생각합니다).

GMP (GNU Multiple Precision Library) 및 GNU MFPR 라이브러리는 C 및 C ++를위한 2 개의 무료 오픈 소스 리소스입니다.

JavaScript (!)에 대한 수치 정밀도 라이브러리도 있으며 재무 계산을 처리 할 수있는 PHP라고 생각합니다.

많은 컴퓨터 언어뿐만 아니라 독점적 인 (특히 Fortran의 경우) 공개 소스 솔루션도 있습니다.

나는 훈련을받는 컴퓨터 과학자가 아닙니다. 그러나 Java의 BigDecimal 또는 C #의 10 진수에 의존하는 경향이 있습니다. 내가 나열한 다른 솔루션을 시도하지 않았지만 아마도 매우 좋습니다.

저에게는 BigDecimal이 지원하는 방법 때문에 BigDecimal을 좋아합니다. C #의 10 진수는 매우 좋지만 원하는만큼 사용할 수있는 기회가 없었습니다. 나는 여가 시간에 과학적으로 관심있는 계산을 수행하며 BigDecimal은 부동 소수점 숫자의 정밀도를 설정할 수 있기 때문에 잘 작동하는 것 같습니다. BigDecimal의 단점은 무엇입니까? 특히 나누기 방법을 사용하는 경우 시간이 느려질 수 있습니다.

C, C ++ 및 Fortran의 무료 독점 라이브러리를 빠른 속도로 살펴볼 수 있습니다.


1
SciPy / Numpy와 관련하여 고정 정밀도 (예 : Python의 decimal.Decimal)는 지원되지 않습니다 ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). 일부 기능은 Decimal에서 제대로 작동하지 않습니다 (예 : isnan). 팬더는 Numpy를 기반으로하며 하나의 주요 정량적 헤지 펀드 인 AQR에서 시작되었습니다. 따라서 식료품 계산이 아닌 재무 계산에 대한 답변이 있습니다.
comte

2

이전 답변을 추가하기 위해 BigDecimal 외에도 Java에서 Joda-Money 를 구현 하여 질문에서 해결 된 문제를 해결할 수있는 옵션이 있습니다. 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는 다른 디자인의 이진 형태의 Decimal입니다. 그것들은 서로 다른 두 가지입니다. 서로 변환 할 때 두 유형간에 오류가 거의 없습니다. 또한 float는 과학적으로 무한한 많은 수의 값을 나타내도록 설계되었습니다. 즉, 고정 바이트 수를 사용하여 정밀도를 극도로 작고 극도로 크게 잃도록 설계되었습니다. 10 진수는 무한한 수의 값을 나타낼 수 없으며 10 진수의 수에만 한정됩니다. Float와 Decimal은 다른 목적을위한 것입니다.

통화 값에 대한 오류를 관리하는 몇 가지 방법이 있습니다.

  1. 긴 정수를 사용하고 대신 센트 단위로 계산하십시오.

  2. 배정도를 사용하고 유효 숫자를 15로만 유지하면 소수점을 정확하게 시뮬레이션 할 수 있습니다. 값을 제시하기 전에 반올림하십시오. 계산할 때 자주 반올림합니다.

  3. Java BigDecimal과 같은 10 진수 라이브러리를 사용하므로 10 진수를 시뮬레이션하기 위해 double을 사용할 필요가 없습니다.

추신 : 대부분의 휴대용 과학 계산기 브랜드는 부동 소수점 대신 십진수로 작동한다는 것을 알고 있습니다. 따라서 하나의 불만 제기 플로팅 오류가 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.