JavaScript 中的精确财务计算。啥是陷阱?

Posted

技术标签:

【中文标题】JavaScript 中的精确财务计算。啥是陷阱?【英文标题】:Precise Financial Calculation in JavaScript. What Are the Gotchas?JavaScript 中的精确财务计算。什么是陷阱? 【发布时间】:2011-02-22 00:32:21 【问题描述】:

为了创建跨平台代码,我想用 javascript 开发一个简单的金融应用程序。所需的计算涉及复利和相对较长的十进制数。我想知道在使用 JavaScript 进行此类数学运算时要避免哪些错误——如果可能的话!

【问题讨论】:

【参考方案1】:

您可能应该将十进制值缩放 100,并以整美分表示所有货币值。这是为了避免problems with floating-point logic and arithmetic。 JavaScript 中没有十进制数据类型——唯一的数字数据类型是浮点数。因此,一般建议以2550 美分而不是25.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,但幸运的是浮点中的整数运算是精确的,因此可以通过缩放来避免十进制表示错误1

请注意,尽管实数集是无限的,但只有有限数量的实数(准确地说是 18,437,736,874,454,810,627)可以用 JavaScript 浮点格式精确表示。因此,其他数字的表示将是实际数字的近似值2


1 Douglas Crockford:JavaScript: The Good Parts: Appendix A - Awful Parts (page 105)。2 David Flanagan:JavaScript: The Definitive Guide, Fourth Edition: 3.1.3 Floating-Point Literals (page 31)。

【讨论】:

提醒一下,始终将计算四舍五入到美分,并且以对消费者最不利的方式进行,即如果你在计算税收,四舍五入。如果您要计算所赚取的利息,请截断。 @Cirrostratus:您可能需要查看***.com/questions/744099。如果您继续使用缩放方法,通常您希望通过您希望保持精度的小数位数来缩放您的值。如果需要 2 位小数,则按 100 缩放,如果需要 4,则按 10000 缩放。 ... 关于 3000.57 值,是的,如果您将该值存储在 JavaScript 变量中,并且您打算对其进行算术运算,您可能希望将其存储为 300057(美分数) .因为3000.57 + 0.11 === 3000.68 返回false 计算便士而不是美元将无济于事。 计算便士时,您无法在大约 10^16 的整数上加 1。数美元时,您无法将 0.01 添加到 10^14 的数字上。无论哪种方式都是一样的。 我刚刚创建了一个npm and Bower module,希望对这项任务有所帮助!【参考方案2】:

将每个值缩放 100 是解决方案。手动操作可能没用,因为您可以找到为您执行此操作的库。我推荐 moneysafe,它提供了一个非常适合 ES6 应用程序的功能性 API:

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

https://github.com/ericelliott/moneysafe

适用于 Node.js 和浏览器。

【讨论】:

赞成。 “按 100 缩放”点已包含在已接受的答案中,但是您添加了具有现代 JavaScript 语法的软件包选项是件好事。 FWIW in$, $ 值名称对于以前未使用过该软件包的人来说是模棱两可的。我知道 Eric 选择以这种方式命名事物,但我仍然觉得我可能会在 import/destructed require 语句中重命名它们,这已经够大的错误了。 缩放 100 仅在您开始想要执行诸如计算百分比之类的操作(本质上是执行除法)之前有所帮助。 我希望我可以多次为评论点赞。仅按 100 缩放是不够的。 JavaScript 中唯一的数字数据类型仍然是浮点数据类型,并且最终会出现严重的舍入错误。 自述文件中的另一个提示:Money$afe has not yet been tested in production at scale.。只是指出这一点,以便任何人都可以考虑这是否适合他们的用例【参考方案3】:

由于只有两位小数,所以没有所谓的“精确”财务计算,但这是一个更普遍的问题。

在 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 类中创建的功能类型。 四舍五入不够准确。 不应该 sys.puts(m+n); //80.76 实际读取 sys.puts(m+n); //80.77?我相信你忘了把 .5 向上取整。 这种方法可能会出现许多微妙的问题。例如,您还没有实现安全的加法、减法、乘法等方法,因此在合并金额时可能会遇到舍入错误 这里的问题是,例如Money(0.1)表示 JavaScript 词法分析器从源中读取字符串“0.1”,然后将其转换为二进制浮点数,然后您已经进行了意外舍入。问题是关于representation(二进制与十进制)而不是关于precision【参考方案4】:

使用decimaljs ...它是一个非常好的库,可以解决问题的严重部分...

只需在您的所有操作中使用它。

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

【讨论】:

【参考方案5】:

您的问题源于浮点计算不准确。如果您只是使用舍入来解决这个问题,那么在乘法和除法时会出现更大的错误。

解决方法如下,解释如下:

您需要考虑其背后的数学才能理解它。像 1/3 这样的实数不能用十进制值在数学中表示,因为它们是无穷无尽的(例如 - .333333333333333 ...)。一些十进制数字不能正确地用二进制表示。例如,0.1 不能用有限的位数正确表示为二进制。

更详细的描述请看这里:http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

看看解决方案的实现:http://floating-point-gui.de/languages/javascript/

【讨论】:

【参考方案6】:

不幸的是,到目前为止,所有答案都忽略了并非所有货币都有 100 个子单位的事实(例如,美分是美元 (USD) 的子单位)。像伊拉克第纳尔 (IQD) 这样的货币有 1000 个子单位:一个伊拉克第纳尔有 1000 个菲尔。日元 (JPY) 没有子单位。所以“乘以 100 做整数运算”并不总是正确的答案。

此外,对于货币计算,您还需要跟踪货币。您无法将美元 (USD) 与印度卢比 (INR) 相加(除非先将一个转换为另一个)。

JavaScript 的整数数据类型可以表示的最大数量也有限制。

在货币计算中,您还必须记住,货币具有有限的精度(通常为 0-3 个小数点)并且需要以特定方式进行舍入(例如,“正常”舍入与银行家的舍入)。要执行的舍入类型也可能因司法管辖区/货币而异。

How to handle money in javascript对相关点进行了很好的讨论。

在我的搜索中,我找到了 dinero.js 库 addresses many of the issues wrt monetary calculations。还没有在生产系统中使用过,所以无法给出明智的意见。

【讨论】:

【参考方案7】:

由于其编码的二进制性质,某些十进制数字无法完全准确地表示。例如

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

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

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

如果您需要使用纯 javascript,那么您需要为每个计算考虑解决方案。对于上面的代码,我们可以将小数转换为整数。

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

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

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

Avoiding Problems with Decimal Math in JavaScript

有一个用于财务计算的专用库,其中包含大量文档。 Finance.js

【讨论】:

我喜欢 Finance.js 也有示例应用

以上是关于JavaScript 中的精确财务计算。啥是陷阱?的主要内容,如果未能解决你的问题,请参考以下文章

计算金额 财务计算 精确运算 BigDecimal用法详解

4 个用于执行高级数学计算的 JavaScript 库

Java浮点数精确计算

JavaScript 浮点数陷阱以及解法

JavaScript中的this陷阱的最全收集

JavaScript----this陷阱的最全收集