Cálculo financiero preciso en JavaScript. ¿Qué son las gotchas?

125

Con el interés de crear código multiplataforma, me gustaría desarrollar una aplicación financiera simple en JavaScript. Los cálculos requeridos involucran interés compuesto y números decimales relativamente largos. Me gustaría saber qué errores evitar al usar JavaScript para hacer este tipo de matemáticas, ¡si es posible!

james_womack
fuente

Respuestas:

107

Probablemente debería escalar sus valores decimales en 100 y representar todos los valores monetarios en centavos enteros. Esto es para evitar problemas con la lógica de punto flotante y la aritmética . No hay ningún tipo de datos decimales en JavaScript: el único tipo de datos numéricos es el punto flotante. Por lo tanto, generalmente se recomienda manejar el dinero como 2550centavos en lugar de 25.50dólares.

Considere eso en JavaScript:

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

Pero:

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

La expresión 0.1 + 0.2 === 0.3regresa false, pero afortunadamente la aritmética de enteros en coma flotante es exacta, por lo que los errores de representación decimal se pueden evitar al escalar 1 .

Tenga en cuenta que si bien el conjunto de números reales es infinito, solo un número finito de ellos (18,437,736,874,454,810,627 para ser exactos) puede representarse exactamente mediante el formato de coma flotante de JavaScript. Por lo tanto, la representación de los otros números será una aproximación del número real 2 .


1 Douglas Crockford: JavaScript: The Good Parts : Apéndice A - Awful Parts (página 105) .
2 David Flanagan: JavaScript: la guía definitiva, cuarta edición : 3.1.3 Literales en coma flotante (página 31) .

Daniel Vassallo
fuente
3
Y como recordatorio, siempre redondee los cálculos al centavo, y hágalo de la manera menos beneficiosa para el consumidor, es decir, si está calculando impuestos, redondee. Si está calculando el interés ganado, truncar.
Josh
66
@Cirrostratus: puede consultar stackoverflow.com/questions/744099 . Si continúa con el método de escalado, en general desearía escalar su valor por el número de dígitos decimales que desea retener precisión. Si necesita 2 decimales, escale por 100, si necesita 4, escale por 10000.
Daniel Vassallo
2
... Con respecto al valor de 3000.57, sí, si almacena ese valor en variables de JavaScript, y tiene la intención de hacer aritmética en él, es posible que desee almacenarlo escalado a 300057 (número de centavos). Porque 3000.57 + 0.11 === 3000.68vuelve false.
Daniel Vassallo
77
Contar centavos en lugar de dólares no ayudará. Al contar centavos, pierdes la capacidad de sumar 1 a un entero a aproximadamente 10 ^ 16. Al contar dólares, pierde la capacidad de agregar .01 a un número en 10 ^ 14. Es lo mismo de cualquier manera.
slashingweapon
1
¡Acabo de crear un módulo npm y Bower que debería ayudar con esta tarea!
Pensierinmusica
18

Escalar cada valor en 100 es la solución. Probablemente sea inútil hacerlo a mano, ya que puede encontrar bibliotecas que lo hagan por usted. Recomiendo moneysafe, que ofrece una API funcional muy adecuada para aplicaciones ES6:

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

https://github.com/ericelliott/moneysafe

Funciona tanto en Node.js como en el navegador.

François Zaninotto
fuente
2
Votado El punto "escalar en 100" ya está cubierto en la respuesta aceptada, sin embargo, es bueno que haya agregado una opción de paquete de software con sintaxis JavaScript moderna. FWIW los in$, $ nombres de los valores son ambiguos para alguien que no ha usado el paquete antes. Sé que fue la elección de Eric nombrar las cosas de esa manera, pero sigo sintiendo que es un error suficiente que probablemente cambiaría el nombre en la declaración de importación / desestructuración requerida.
james_womack
44
Escalar por 100 solo ayuda hasta que comiences a querer hacer algo como calcular porcentajes (realizar la división, esencialmente).
Puntiagudo
2
Desearía poder votar un comentario varias veces. Escalar por 100 simplemente no es suficiente. El único tipo de datos numérico en JavaScript sigue siendo un tipo de datos de punto flotante, y todavía va a terminar con errores de redondeo significativos.
Craig
1
Y otro mano a mano desde el readme: Money$afe has not yet been tested in production at scale.. Solo señalarlo para que cualquiera pueda considerar si es apropiado para su caso de uso
Nobita
7

No existe el cálculo financiero "preciso" debido a solo dos dígitos de fracción decimal, pero ese es un problema más general.

En JavaScript, puede escalar cada valor en 100 y usarlo Math.round()cada vez que puede ocurrir una fracción.

Podría usar un objeto para almacenar los números e incluir el redondeo en su valueOf()método de prototipos . Me gusta esto:

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

De esa manera, cada vez que use un objeto Money, se representará como redondeado a dos decimales. El valor sin redondear todavía es accesible a través de m.amount.

Puede construir su propio algoritmo de redondeo Money.prototype.valueOf(), si lo desea.

selfawaresoup
fuente
Me gusta este enfoque orientado a objetos, el hecho de que el objeto Money tenga ambos valores es muy útil. Es el tipo exacto de funcionalidad que me gusta crear en mis clases personalizadas de Objective-C.
james_womack
44
No es lo suficientemente preciso para redondear.
Henry Tseng
3
No debería sys.puts (m + n); //80.76 realmente lee sys.puts (m + n); //80.77? Creo que olvidó redondear el .5 hacia arriba.
Dave L
2
Este tipo de enfoque tiene una serie de problemas sutiles que pueden surgir. Por ejemplo, no ha implementado métodos seguros de suma, resta, multiplicación, etc., por lo que es probable que encuentre errores de redondeo al combinar cantidades de dinero
1800 INFORMACIÓN
2
El problema aquí es que, por ejemplo, Money(0.1)significa que el lexer de JavaScript lee la cadena "0.1" de la fuente y luego la convierte en un punto flotante binario y luego ya realizó un redondeo involuntario. El problema es sobre la representación (binario vs decimal) no sobre la precisión .
mgd
2

Su problema proviene de la inexactitud en los cálculos de coma flotante. Si solo está usando el redondeo para resolver esto, tendrá un mayor error al multiplicar y dividir.

La solución está a continuación, sigue una explicación:

Tendrás que pensar en las matemáticas detrás de esto para entenderlo. Los números reales como 1/3 no se pueden representar en matemáticas con valores decimales ya que son infinitos (por ejemplo, .333333333333333 ...). Algunos números en decimal no se pueden representar en binario correctamente. Por ejemplo, 0.1 no se puede representar en binario correctamente con un número limitado de dígitos.

Para obtener una descripción más detallada, consulte aquí: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Eche un vistazo a la implementación de la solución: http://floating-point-gui.de/languages/javascript/

Henry Tseng
fuente
1

Debido a la naturaleza binaria de su codificación, algunos números decimales no se pueden representar con una precisión perfecta. Por ejemplo

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

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

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

Si necesita usar JavaScript puro, debe pensar en la solución para cada cálculo. Para el código anterior, podemos convertir decimales en enteros enteros.

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

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

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

Evitar problemas con la matemática decimal en JavaScript

Hay una biblioteca dedicada para cálculos financieros con excelente documentación. Finance.js

Ali Rehman
fuente
Me gusta que Finance.js también tenga aplicaciones de ejemplo
james_womack
0

Desafortunadamente, todas las respuestas hasta ahora ignoran el hecho de que no todas las monedas tienen 100 subunidades (por ejemplo, el centavo es la subunidad del dólar estadounidense (USD)). Las monedas como el dinar iraquí (IQD) tienen 1000 subunidades: un dinar iraquí tiene 1000 fils. El yen japonés (JPY) no tiene subunidades. Entonces, "multiplicar por 100 para hacer aritmética de enteros" no siempre es la respuesta correcta.

Además, para los cálculos monetarios, también debe realizar un seguimiento de la moneda. No puede agregar un dólar estadounidense (USD) a una rupia india (INR) (sin convertir primero uno a otro).

También hay limitaciones en la cantidad máxima que puede ser representada por el tipo de datos enteros de JavaScript.

En los cálculos monetarios, también debe tener en cuenta que el dinero tiene una precisión finita (generalmente de 0 a 3 puntos decimales) y el redondeo debe hacerse de formas particulares (por ejemplo, redondeo "normal" versus redondeo bancario). El tipo de redondeo a realizar también puede variar según la jurisdicción / moneda.

Cómo manejar el dinero en JavaScript tiene una muy buena discusión de los puntos relevantes.

En mis búsquedas encontré la biblioteca dinero.js que aborda muchos de los problemas con los cálculos monetarios . Todavía no lo he usado en un sistema de producción, así que no puedo dar una opinión informada al respecto.

markvgti
fuente