JavaScript中的精确财务计算。有哪些需要注意的地方?

164

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

9个回答

146

你应该将小数值乘以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道格拉斯·克罗克福德:JavaScript语言精粹: 附录A - 糟糕的部分 (第105页)
2大卫·弗拉纳根:JavaScript权威指南,第四版: 3.1.3 浮点字面量 (第31页)


6
提醒一下,计算时要将结果舍入到分的位数,并以对消费者最不利的方式进行,例如,如果你正在计算税费,则向上舍入;如果你正在计算利息收益,则截断小数位。 - Josh
6
@Cirrostratus:你可能想要查看http://stackoverflow.com/questions/744099/。如果你决定使用缩放方法,通常你需要通过希望保留的小数位数来缩放你的值。如果你需要2位小数,就乘以100;如果你需要4位小数,就乘以10000。 - Daniel Vassallo
2
关于3000.57的值,如果您将该值存储在 JavaScript 变量中,并打算对其进行算术运算,您可能希望将其存储为300057(以分为单位)。因为 3000.57 + 0.11 === 3000.68 的结果是 false - Daniel Vassallo
12
将注意力放在计算分而不是元上并不能帮助到你。当你开始计算分时,当数值达到10的16次方时便失去了将1加入整数的能力;而当你计算元时,当数值达到10的14次方时便失去了将0.01加入数字的能力。无论哪种方式,情况都是一样的。 - slashingweapon
7
@slashingweapon,如果我没错的话,10的16次方分之一美分相当于一百万亿美元。在大多数情况下,这不会成为一个问题。在你处理的金额没有超过一百万亿美元之前,0.1加上0.2就已经出现了。 - Dave
显示剩余10条评论

33

将每个值都乘以100是解决方案。手动操作可能是无用的,因为您可以找到为您完成此操作的库。我推荐使用moneysafe,它提供了适用于ES6应用程序的功能API:

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

https://github.com/ericelliott/moneysafe

可同时在Node.js和浏览器中使用。


4
赞同。在已接受的答案中,"将比例乘以100"这个点已经被讨论过了,但你提供了一个带有现代JavaScript语法的软件包选项是很好的补充。就我而言,in$, $ 这些变量名对于那些没有使用过该软件包的人来说是含糊不清的。我知道这是Eric的选择来命名它们,但我仍然觉得这是一个足够严重的错误,以至于我可能会在导入/解构需要声明中重新命名它们。 - james_womack
17
将数值乘以100只有在进行百分比计算(实质上是除法运算)之类的操作之前有效。 - Pointy
4
我希望我可以多次给评论点赞,将数字乘以100仍然不够。JavaScript中唯一的数值数据类型仍然是浮点数据类型,你仍然会遇到显著的四舍五入误差。 - Craig Tullis
4
自自读我还需要提醒大家一件事:“Money$afe尚未在生产环境中进行大规模测试。” 只是想指出这一点,以便任何人都可以考虑是否适合他们的使用情况。 - Aurelio

13
很不幸,到目前为止所有的答案都忽略了一个事实,即并非所有货币都有100个子单位(例如,美元(USD)的子单位是分)。像伊拉克第纳尔(IQD)这样的货币有1000个子单位:1伊拉克第纳尔等于1000菲尔斯。日元(JPY)没有子单位。因此,“乘以100进行整数运算”并不总是正确的答案。
此外,在货币计算中,您还需要跟踪货币。您不能将美元(USD)加上印度卢比(INR)(除非先将其中一个转换为另一个)。
JavaScript的整数数据类型也有最大表示量的限制。
在货币计算中,您还必须记住,金钱具有有限的精度(通常为0-3个小数点),并且需要以特定方式进行舍入(例如,“正常”舍入与银行家舍入)。要执行的舍入类型也可能因司法管辖区/货币而异。 如何在Javascript中处理货币对相关要点进行了很好的讨论。
在我的搜索中,我发现dinero.js解决了许多与货币计算相关的问题。尚未在生产系统中使用过,因此无法对其发表知情意见。

12

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

在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”,然后将其转换为二进制浮点数,这样就会出现意外的四舍五入。问题在于_表示_(二进制 vs 十进制),而不是_精度_。 - mgd
我已经给这个答案投了反对票,因为我发现它有多处误导。1.精确的财务计算是按照你所在国家的会计准则进行的。如果你在美国,那就是GAAP。2)建议自己开发是一个非常糟糕的想法,因为某人的钱正在冒险,错误可能会成为作者的法律责任。要想了解如何正确地进行货币计算,请查看例如Java的BigDecimal类。 - Igor Urisman

9

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/

4

由于二进制编码的本质,一些十进制数无法被完全准确地表示。例如:

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);

避免在JavaScript中出现十进制计算问题

有一个专门用于财务计算的库,文档非常好。 Finance.js


我喜欢 Finance.js 也提供示例应用程序。 - james_womack

1
使用此代码进行货币计算并将数字舍入为两位小数。

<!DOCTYPE html>
<html>
<body>

<h1>JavaScript Variables</h1>

<p id="test1"></p>
<p id="test2"></p>
<p id="test3"></p>

<script>

function setDecimalPoint(num) {
    if (isNaN(parseFloat(num)))
        return 0;
    else {
        var Number = parseFloat(num);
        var multiplicator = Math.pow(10, 2);
        Number = parseFloat((Number * multiplicator).toFixed(2));
        return (Math.round(Number) / multiplicator);
    }
}

document.getElementById("test1").innerHTML = "Without our method O/P is: " + (655.93 * 9)/100;
document.getElementById("test2").innerHTML = "Calculator O/P: 59.0337, Our value is: " + setDecimalPoint((655.93 * 9)/100);
document.getElementById("test3").innerHTML = "Calculator O/P: 32.888.175, Our value is: " + setDecimalPoint(756.05 * 43.5);
</script>

</body>
</html>


0

在这里,我们使用JavaScript和Node.js在金融市场上交易真实货币。 我们设计了一个自定义的Decimal类,代表不可变的十进制数,并提供计算逻辑。

import { decimal, } from "@reiryoku/mida";

0.1 + 0.2; //= 0.30000000000000004
decimal(0.1).add(0.2); //= 0.3
decimal("0.1").add("0.2"); //= 0.3

如果你感到好奇,可以在这里查看定义https://github.com/Reiryoku-Technologies/Mida/blob/master/src/core/decimals/MidaDecimal.ts

该软件包可供任何人在npm上使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接