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

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

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



답변

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유형이 있습니다.


답변

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몇 가지주의 사항이 있지만 (현재 허용되는 답변을 참조하십시오).


답변

이것은 정확성의 문제가 아니며 정확성의 문제도 아닙니다. 기초 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을 사용하면 코드에 다른 버그가 없다고 가정 할 때 항상 사람이 기대하는 것입니다.


답변

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

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

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

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

결과

1.1875


답변

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


답변

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

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

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


답변

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

일반적으로 이중 유형의 정밀도는 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;
    }
}