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

IT技术 javascript decimal finance
2021-01-31 19:06:47

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

6个回答

您可能应该将十进制值缩放 100,并以整分表示所有货币值。这是为了避免浮点逻辑和算术出现问题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:好的部分:附录 A - 糟糕的部分(第 105 页)
2 David Flanagan:JavaScript:权威指南,第四版:3.1.3 浮点文字(第 31 页)

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

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

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

https://github.com/ericelliott/moneysafe

适用于 Node.js 和浏览器。

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

由于只有两个小数位数,因此没有“精确”财务计算之类的东西,但这是一个更普遍的问题。

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

使用decimaljs ......它是一个非常好的库,解决了一个棘手的问题......

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

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

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

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

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

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

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