Cálculo financeiro preciso em JavaScript. Quais são as pegadinhas?

125

No interesse de criar código de plataforma cruzada, eu gostaria de desenvolver um aplicativo financeiro simples em JavaScript. Os cálculos necessários envolvem juros compostos e números decimais relativamente longos. Gostaria de saber quais erros evitar ao usar o JavaScript para fazer esse tipo de matemática - se for possível!

james_womack
fonte

Respostas:

107

Você provavelmente deve escalar seus valores decimais em 100 e representar todos os valores monetários em centavos inteiros. Isso é para evitar problemas com a lógica de ponto flutuante e a aritmética . Não há tipo de dados decimal no JavaScript - o único tipo de dado numérico é de ponto flutuante. Portanto, geralmente é recomendável lidar com dinheiro como 2550centavos em vez de 25.50dólares.

Considere isso em JavaScript:

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

Mas:

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

A expressão 0.1 + 0.2 === 0.3retorna false, mas felizmente a aritmética inteira em ponto flutuante é exata, portanto, erros de representação decimal podem ser evitados escalando 1 .

Observe que, embora o conjunto de números reais seja infinito, apenas um número finito deles (18.437.736.874.454.810.627 para ser exato) pode ser representado exatamente pelo formato de ponto flutuante do JavaScript. Portanto, a representação dos outros números será uma aproximação do número real 2 .


1 Douglas Crockford: JavaScript: As peças boas : Apêndice A - Peças horríveis (página 105) .
2 David Flanagan: JavaScript: The Definitive Guide, quarta edição : 3.1.3 Literais de ponto flutuante (página 31) .

Daniel Vassallo
fonte
3
E, como lembrete, sempre arredondar os cálculos para o centavo e fazê-lo da maneira menos benéfica para o consumidor, ou seja, se você estiver calculando impostos, arredonde para cima. Se você estiver calculando os juros ganhos, trunque.
Josh
6
@ Cirrostratus: convém verificar stackoverflow.com/questions/744099 . Se você seguir em frente com o método de dimensionamento, em geral, você deseja dimensionar seu valor pelo número de dígitos decimais que deseja manter a precisão. Se você precisa de 2 casas decimais, escala de 100, se você precisar de 4, a escala de 10000.
Daniel Vassallo
2
... Em relação ao valor 3000.57, sim, se você armazenar esse valor em variáveis ​​JavaScript e pretender fazer aritmética nele, convém armazená-lo na escala de 300057 (número de centavos). Porque 3000.57 + 0.11 === 3000.68retorna false.
Daniel Vassallo
7
Contar moedas de um centavo em vez de dólares não ajudará. Ao contar moedas de um centavo, você perde a capacidade de adicionar 1 a um número inteiro em cerca de 10 ^ 16. Ao contar dólares, você perde a capacidade de adicionar 0,01 a um número em 10 ^ 14. É o mesmo de qualquer maneira.
Slashingweapon
1
Acabei de criar um módulo npm e Bower que, espero, ajudará nessa tarefa!
Pensierinmusica
18

Escalar cada valor em 100 é a solução. Fazer isso manualmente é provavelmente inútil, pois você pode encontrar bibliotecas que fazem isso por você. Eu recomendo o moneysafe, que oferece uma API funcional adequada para aplicativos ES6:

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

https://github.com/ericelliott/moneysafe

Funciona no Node.js e no navegador.

François Zaninotto
fonte
2
Votado. O ponto "escalar por 100" já está coberto na resposta aceita, no entanto, é bom que você tenha adicionado uma opção de pacote de software com a sintaxe JavaScript moderna. FWIW os in$, $ nomes dos valores são ambíguos para alguém que não usou o pacote anteriormente. Eu sei que foi a escolha de Eric nomear as coisas dessa maneira, mas ainda acho que é um erro suficiente que eu provavelmente as renomeie na declaração de importação / desestruturação.
precisa saber é o seguinte
4
Escalar em 100 ajuda apenas até você começar a querer fazer algo como calcular porcentagens (realizar divisão, essencialmente).
Pointy
2
Eu gostaria de poder votar um comentário várias vezes. Escalar em 100 simplesmente não é suficiente. O único tipo de dados numérico no JavaScript ainda é um tipo de dados de ponto flutuante e você ainda terá erros de arredondamento significativos.
Craig
1
E outro heads-up do readme: Money$afe has not yet been tested in production at scale.. Apenas apontar isso para que qualquer pessoa pode então considerar se isso é apropriado para o seu caso de uso
Nobita
7

Não existe cálculo financeiro "preciso" por causa de apenas dois dígitos da fração decimal, mas esse é um problema mais geral.

Em JavaScript, você pode dimensionar cada valor em 100 e usar Math.round()sempre que uma fração pode ocorrer.

Você pode usar um objeto para armazenar os números e incluir o arredondamento em seu valueOf()método de protótipo . Como isso:

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

Dessa forma, sempre que você usar um objeto Money, ele será representado como arredondado para duas casas decimais. O valor não arredondado ainda é acessível via m.amount.

Você pode criar seu próprio algoritmo de arredondamento Money.prototype.valueOf(), se quiser.

selfawaresoup
fonte
Eu gosto dessa abordagem orientada a objetos, o fato de o objeto Money conter ambos os valores é muito útil. É o tipo exato de funcionalidade que eu gosto de criar nas minhas classes Objective-C personalizadas.
James_womack 20/05
4
Não é preciso o suficiente para arredondar.
Henry Tseng
3
Sys.puts não deve (m + n); //80.76 realmente lê sys.puts (m + n); //80,77? Eu acredito que você esqueceu de arredondar os 0,5.
Dave L
2
Esse tipo de abordagem tem vários problemas sutis que podem surgir. Por exemplo, você não implementaram métodos seguros de adição, subtração, multiplicação e assim por diante, assim que você é provável funcionar em erros de arredondamento quando se combina quantias em dinheiro
1800 INFORMATION
2
O problema aqui é que, por exemplo, Money(0.1)significa que o lexer JavaScript lê a string "0.1" da fonte e a converte em um ponto flutuante binário e você já fez um arredondamento não intencional. O problema é sobre representação (binária vs decimal), não sobre precisão .
mgd
3

use decimaljs ... É uma biblioteca muito boa que resolve uma parte dura do problema ...

basta usá-lo em todas as suas operações.

https://github.com/MikeMcl/decimal.js/

tlejmi
fonte
2

Seu problema decorre da imprecisão nos cálculos de ponto flutuante. Se você estiver usando o arredondamento para resolver isso, terá um erro maior ao multiplicar e dividir.

A solução está abaixo, segue uma explicação:

Você precisará pensar em matemática por trás disso para entender. Números reais como 1/3 não podem ser representados em matemática com valores decimais, pois são infinitos (por exemplo, - .333333333333333 ...). Alguns números em decimal não podem ser representados em binário corretamente. Por exemplo, 0,1 não pode ser representado no binário corretamente com um número limitado de dígitos.

Para uma descrição mais detalhada, consulte aqui: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Dê uma olhada na implementação da solução: http://floating-point-gui.de/languages/javascript/

Henry Tseng
fonte
1

Devido à natureza binária de sua codificação, alguns números decimais não podem ser representados com precisão perfeita. Por exemplo

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

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

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

Se você precisar usar javascript puro, precisará pensar na solução para cada cálculo. Para o código acima, podemos converter decimais em números inteiros.

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

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

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

Evitando problemas com a matemática decimal em JavaScript

Existe uma biblioteca dedicada para cálculos financeiros com ótima documentação. Finance.js

Ali Rehman
fonte
Eu gosto que o Finance.js também tenha aplicativos de exemplo
james_womack 17/01
0

Infelizmente, todas as respostas até agora ignoram o fato de que nem todas as moedas têm 100 subunidades (por exemplo, o centavo é a subunidade do dólar dos EUA (USD)). Moedas como o Dinar Iraquiano (IQD) possuem 1000 subunidades: um Dinar Iraquiano possui 1000 fils. O iene japonês (JPY) não possui subunidades. Portanto, "multiplique por 100 para fazer aritmética inteira" nem sempre é a resposta correta.

Além disso, para cálculos monetários, você também precisa acompanhar a moeda. Você não pode adicionar um dólar americano (USD) a uma rúpia indiana (INR) (sem antes converter um para o outro).

Também há limitações na quantidade máxima que pode ser representada pelo tipo de dados inteiro do JavaScript.

Nos cálculos monetários, você também deve ter em mente que o dinheiro tem precisão finita (normalmente de 0 a 3 pontos decimais) e o arredondamento precisa ser feito de maneiras específicas (por exemplo, arredondamento "normal" versus arredondamento de banqueiros). O tipo de arredondamento a ser executado também pode variar de acordo com a jurisdição / moeda.

Como lidar com dinheiro em javascript tem uma discussão muito boa dos pontos relevantes.

Nas minhas pesquisas, encontrei a biblioteca dinero.js que aborda muitos dos problemas por meio de cálculos monetários . Ainda não o utilizou em um sistema de produção, portanto não pode dar uma opinião informada sobre ele.

markvgti
fonte