JavaScript의 정확한 재무 계산. Gotchas는 무엇입니까?


125

크로스 플랫폼 코드를 만들기 위해 JavaScript로 간단한 금융 애플리케이션을 개발하고 싶습니다. 필요한 계산에는 복리와 비교적 긴 십진수가 포함됩니다. JavaScript를 사용하여 이러한 유형의 수학을 수행 할 때 피해야 할 실수가 무엇인지 알고 싶습니다. 가능하다면!

답변:


107

십진수 값을 100으로 조정하고 모든 통화 값을 전체 센트로 표시해야합니다. 이는 부동 소수점 논리 및 산술 문제 를 방지하기위한 것입니다 . JavaScript에는 10 진수 데이터 유형이 없습니다. 유일한 숫자 데이터 유형은 부동 소수점입니다. 따라서 일반적으로 돈 255025.50달러 대신 센트로 처리하는 것이 좋습니다 .

JavaScript에서 고려하십시오.

var result = 1.0 + 2.0;     // (result === 3.0) returns true

그러나:

var result = 0.1 + 0.2;     // (result === 0.3) returns false

표현식은를 0.1 + 0.2 === 0.3반환 false하지만 다행스럽게도 부동 소수점의 정수 산술은 정확하므로 10 진수 표현 오류는 스케일링 1 로 피할 수 있습니다 .

실수 세트는 무한하지만 한정된 수 (정확히 18,437,736,874,454,810,627) 만 JavaScript 부동 소수점 형식으로 정확하게 표현 될 수 있습니다. 따라서 다른 숫자의 표현은 실제 숫자 2 의 근사치입니다 .


1 Douglas Crockford : JavaScript : 좋은 부분 : 부록 A-끔찍한 부분 (페이지 105) .
2 David Flanagan : JavaScript : The Definitive Guide, Fourth Edition : 3.1.3 부동 소수점 리터럴 (페이지 31) .


3
그리고 다시 말해, 항상 계산을 센트로 반올림하고 소비자에게 가장 덜 유익한 방법으로 수행하십시오. 세금을 계산하는 경우 반올림하십시오. 얻은이자를 계산하는 경우 자릅니다.
Josh

6
@Cirrostratus : stackoverflow.com/questions/744099 를 확인하는 것이 좋습니다. 스케일링 방법으로 진행하는 경우 일반적으로 정밀도를 유지하려는 십진수 숫자로 값을 스케일링 할 수 있습니다. 소수점 이하 2 자리가 필요하면 100으로, 4가 필요하면 10000으로 조정하십시오.
Daniel Vassallo

2
... 3000.57 값과 관련하여, 예, 해당 값을 JavaScript 변수에 저장하고 산술을 수행하려는 경우 300057 (센트 수)로 스케일링하여 저장할 수 있습니다. 3000.57 + 0.11 === 3000.68반환 하기 때문에 false.
Daniel Vassallo

7
달러 대신 동전을 세는 것은 도움이되지 않습니다. 페니를 세면 약 10 ^ 16에서 정수에 1을 더하는 능력을 잃게됩니다. 달러를 계산할 때 10 ^ 14의 숫자에 .01을 더하는 기능을 잃게됩니다. 어느 쪽이든 동일합니다.
slashingweapon

1
이 작업에 도움이 될 npm 및 Bower 모듈 을 방금 만들었습니다 !
Pensierinmusica

18

모든 값을 100으로 조정하는 것이 해결책입니다. 손으로하는 것은 아마도 쓸모가 없을 것입니다. 그렇게 해주는 라이브러리를 찾을 수 있기 때문입니다. ES6 애플리케이션에 적합한 기능적 API를 제공하는 moneysafe를 권장합니다.

const { in$, $ } = require('moneysafe');
console.log(in$($(10.5) + $(.3)); // 10.8

https://github.com/ericelliott/moneysafe

Node.js와 브라우저에서 모두 작동합니다.


2
찬성. "100으로 확장"포인트는 이미 허용 된 답변에서 다루어졌지만 최신 JavaScript 구문으로 소프트웨어 패키지 옵션을 추가 한 것이 좋습니다. FWIW in$, $ 값 이름은 이전에 패키지를 사용하지 않은 사람에게는 모호합니다. 나는 그렇게 이름을 짓는 것이 Eric의 선택이라는 것을 알고 있지만, 여전히 import / destructured require 문에서 이름을 바꾸는 것으로 충분하다고 생각합니다.
james_womack

4
100으로 스케일링하면 백분율 계산 (기본적으로 나눗셈 수행)과 같은 작업을 시작하기 전까지 만 도움이됩니다.
Pointy

2
댓글을 여러 번 찬성 할 수 있으면 좋겠습니다. 100으로 확장하는 것만으로는 충분하지 않습니다. JavaScript의 유일한 숫자 데이터 유형은 여전히 ​​부동 소수점 데이터 유형이며 여전히 심각한 반올림 오류가 발생합니다.
Craig

1
그리고 readme에서 또 다른 헤드 업 : Money$afe has not yet been tested in production at scale.. 사람이 그 자신의 사용 사례에 적합한의 경우 고려할 수 있도록 그냥 그 밖을 가리키는
노비타

7

소수 두 자릿수로 인해 "정확한"재무 계산과 같은 것은 없지만 더 일반적인 문제입니다.

JavaScript에서는 모든 값을 100으로 확장 Math.round()하고 분수가 발생할 때마다 사용할 수 있습니다.

객체를 사용하여 숫자를 저장하고 프로토 타입 valueOf()메서드에 반올림을 포함 할 수 있습니다 . 이렇게 :

sys = require('sys');

var Money = function(amount) {
        this.amount = amount;
    }
Money.prototype.valueOf = function() {
    return Math.round(this.amount*100)/100;
}

var m = new Money(50.42355446);
var n = new Money(30.342141);

sys.puts(m.amount + n.amount); //80.76569546
sys.puts(m+n); //80.76

이렇게하면 Money 개체를 사용할 때마다 소수점 이하 두 자리로 반올림하여 표시됩니다. 반올림되지 않은 값은 여전히를 통해 액세스 할 수 있습니다 m.amount.

원하는 Money.prototype.valueOf()경우 자체 반올림 알고리즘을으로 빌드 할 수 있습니다 .


저는이 객체 지향 접근 방식을 좋아합니다. Money 객체가 두 값을 모두 보유한다는 사실은 매우 유용합니다. 사용자 지정 Objective-C 클래스에서 만들고 싶은 기능의 정확한 유형입니다.
james_womack

4
반올림하기에 충분히 정확하지 않습니다.
Henry Tseng

3
sys.puts (m + n); 안됩니다. //80.76 실제로 읽기 sys.puts (m + n); //80.77? .5를 반올림하는 것을 잊었다 고 생각합니다.
Dave L

2
이러한 접근 방식에는 발생할 수있는 여러 가지 미묘한 문제가 있습니다. 예를 들어 덧셈, 뺄셈, 곱셈 등의 안전한 방법을 구현하지 않았으므로 금액을 결합 할 때 반올림 오류가 발생할 가능성이 있습니다.
1800 INFORMATION

2
여기서 문제는 예를 들어 Money(0.1)JavaScript 렉서가 소스에서 문자열 "0.1"을 읽고 이진 부동 소수점으로 변환 한 다음 이미 의도하지 않은 반올림을 수행했음을 의미합니다. 문제는 정밀도가 아닌 표현 (이진 대 십진수)에 관한 것 입니다.
mgd


2

문제는 부동 소수점 계산의 부정확성으로 인해 발생합니다. 이 문제를 해결하기 위해 반올림 만 사용하는 경우 곱하고 나눌 때 더 큰 오류가 발생합니다.

해결책은 다음과 같습니다. 설명은 다음과 같습니다.

이를 이해하려면이 뒤에있는 수학에 대해 생각해야합니다. 1/3과 같은 실수는 끝이 없기 때문에 10 진수 값으로 수학에서 표현할 수 없습니다 (예 : .333333333333333 ...). 소수의 일부 숫자는 이진수로 올바르게 표현할 수 없습니다. 예를 들어 0.1은 제한된 자릿수로 이진수로 올바르게 표현 될 수 없습니다.

자세한 설명은 여기를 참조 하십시오 : http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

솔루션 구현을 살펴보십시오. http://floating-point-gui.de/languages/javascript/


1

인코딩의 이진 특성으로 인해 일부 십진수는 완벽한 정확도로 표현할 수 없습니다. 예를 들면

var money = 600.90;
var price = 200.30;
var total = price * 3;

// Outputs: false
console.log(money >= total);

// Outputs: 600.9000000000001
console.log(total);

순수한 자바 스크립트를 사용해야한다면 모든 계산에 대한 해결책을 생각해야합니다. 위 코드의 경우 소수를 정수로 변환 할 수 있습니다.

var money = 60090;
var price = 20030;
var total = price * 3;

// Outputs: true
console.log(money >= total);

// Outputs: 60090
console.log(total);

JavaScript에서 십진 수학 문제 방지

훌륭한 문서와 함께 재무 계산을위한 전용 라이브러리가 있습니다. Finance.js


Finance.js에 예제 앱도 있다는 점이 마음에 듭니다
james_womack

0

불행히도 지금까지 모든 답변은 모든 통화가 100 개의 하위 단위를 가지고 있지 않다는 사실을 무시합니다 (예 : 센트는 미국 달러 (USD)의 하위 단위). 이라크 디나르 (IQD)와 같은 통화에는 1000 개의 하위 단위가 있습니다. 이라크 디나르에는 1000 개의 필이 있습니다. 일본 엔 (JPY)에는 하위 단위가 없습니다. 따라서 "정수 연산을 위해 100을 곱한다"는 것이 항상 정답은 아닙니다.

또한 통화 계산을 위해 통화를 추적해야합니다. 인도 루피 (INR)에 미국 달러 (USD)를 추가 할 수 없습니다 (먼저 하나를 다른 루피로 변환하지 않고).

JavaScript의 정수 데이터 유형으로 표현할 수있는 최대량에도 제한이 있습니다.

화폐 계산에서 돈은 유한 한 정밀도 (일반적으로 소수점 이하 0 ~ 3 점)를 가지며 특정 방식으로 반올림해야합니다 (예 : "정상"반올림 대 은행가의 반올림). 수행 할 반올림 유형은 관할권 / 통화에 따라 다를 수도 있습니다.

자바 스크립트에서 돈을 처리하는 방법 은 관련 요점에 대한 좋은 토론을 가지고 있습니다.

내 검색에서 나는 금전적 계산과 관련된 많은 문제해결 하는 dinero.js 라이브러리를 발견했다 . 아직 프로덕션 시스템에서 사용하지 않았으므로 정보에 입각 한 의견을 제시 할 수 없습니다.

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