我应该使用 NSDecimalNumber 来处理金钱吗?

Posted

技术标签:

【中文标题】我应该使用 NSDecimalNumber 来处理金钱吗?【英文标题】:Should I use NSDecimalNumber to deal with money? 【发布时间】:2010-09-30 02:31:09 【问题描述】:

当我开始编写我的第一个应用程序时,我不假思索地使用 NSNumber 来表示货币价值。然后我想也许 c 类型足以处理我的价值观。然而,iPhone SDK 论坛建议我使用 NSDecimalNumber,因为它具有出色的舍入功能。

我不是一个气质的数学家,我认为尾数/指数范式可能有点矫枉过正;尽管如此,谷歌搜索,我意识到大多数关于可可货币/货币的讨论都提到了 NSDecimalNumber。

请注意,我正在开发的应用程序将国际化,因此以美分计算金额的选项实际上并不可行,因为货币结构很大程度上取决于所使用的语言环境。

我 90% 确定我需要使用 NSDecimalNumber,但由于我在网络上没有找到明确的答案(例如:“如果你处理金钱,请使用 NSDecimalNumber!”)我想我会在这里问。也许答案对大多数人来说是显而易见的,但在开始对我的应用程序进行大规模重构之前,我想确定一下。

说服我:)

【问题讨论】:

【参考方案1】:

Marcus Zarra 对此有非常明确的立场:"If you are dealing with currency at all, then you should be using NSDecimalNumber." 他的文章启发了我研究 NSDecimalNumber,我对此印象非常深刻。处理 base-10 数学时的 IEEE 浮点错误让我恼火了一段时间 (1 * (0.5 - 0.4 - 0.1) = -0.00000000000000002776) 并且 NSDecimalNumber 消除了它们。

NSDecimalNumber 不只是添加另外几位二进制浮点精度,它实际上是做以 10 为底的数学运算。这消除了上面示例中所示的错误。

现在,我正在编写一个符号数学应用程序,所以我希望 30+ 十进制数字精度和没有奇怪的浮点错误可能是一个例外,但我认为它值得一看。这些操作比简单的 var = 1 + 2 风格的数学有点尴尬,但它们仍然易于管理。如果您担心在数学运算期间分配各种实例,则 NSDecimal 是 NSDecimalNumber 的 C 结构等价物,并且有 C 函数可以使用它进行完全相同的数学运算。根据我的经验,除了要求最苛刻的应用程序(MacBook Air 上 3,344,593 个添加/秒、254,017 个分区/秒、281,555 个添加/秒、iPhone 上 12,027 个分区/秒)之外,这些速度都非常快。

作为一个额外的好处,NSDecimalNumber 的 descriptionWithLocale: 方法提供了一个带有数字本地化版本的字符串,包括正确的小数分隔符。它的 initWithString:locale: 方法也是如此。

【讨论】:

我从没说过它做浮点运算。我说它有一个有限的范围,它确实如此。引用文档:“一个实例可以表示任何可以表示为尾数 x 10^exponent 的数字,其中尾数是一个最多 38 位的十进制整数,而指数是一个从 –128 到 127 的整数。” 感谢您的回答,尤其是 Marcus Zarra 的链接,我已经在重构以将我所有的货币值转换为 NSDecimalNumber。 我已经测试过 (1 * (0.5 - 0.4 - 0.1)) 并打印 -0.000000 ( xcode 6)。有什么变化吗? @samir - 浮点与双精度。只是二进制浮点问题的一个例子,其中有很多。不同的实现/精度会以不同的方式不准确。【参考方案2】:

是的。你必须使用

NSDecimalNumber

当您在 ios 上处理货币时,不是 doublefloat

为什么会这样??

因为我们不想得到 $9.9999999998 而不是 $10

这是怎么回事??

浮点数和双精度数是近似值。它们总是带有舍入误差。计算机用于存储小数的格式会导致此路由错误。 如果您需要更多详细信息,请阅读

http://floating-point-gui.de/

根据苹果文档,

NSDecimalNumber 是 NSNumber 的不可变子类,为进行 base-10 算术提供了一个面向对象的包装器。一个实例可以表示任何可以表示为尾数 x 10^exponent 的数字,其中尾数是长度为 38 位的十进制整数,指数是从 –128 到 127 的整数。用于进行 base-10 算术的包装器。

所以建议使用 NSDecimalNumber 来处理货币。

【讨论】:

【参考方案3】:

(改编自我对另一个答案的评论。)

是的,你应该这样做。整数个便士仅在您不需要代表(例如,半美分)时才有效。如果发生这种情况,您可以将其更改为计算半美分,但如果您需要表示四分之一美分或八分之一美分怎么办?

唯一合适的解决方案是 NSDecimalNumber(或类似的东西),它将问题推迟到 10^-128¢(即, 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000001¢)。

(另一种方法是任意精度算术,但这需要一个单独的库,例如GNU MP Bignum library。GMP 属于 LGPL。我从未使用过该库,也不知道它是如何工作的,所以我不能说它对你有多好。)

[编辑:显然,至少有一个人 - Brad Larson - 认为我在这个答案的某个地方谈论二进制浮点。我不是。]

【讨论】:

NSDecimalNumber 将其有效位存储为 8 个 uint16,最大有效位为 2^128 (~3.4 × 10^38),不是 10^128。因此,最大范围是(大约)-3.4E38 到 +3.4E38,指数为 0,最大指数为 127,范围为(大约)-3.4E165 到 +3.4E165。【参考方案4】:

我发现使用整数表示美分数然后除以 100 来表示很方便。避免了整个问题。

【讨论】:

OP 说“以美分计算金额的选项实际上并不可行”,但这对我来说没有意义。只需将金额从表示“美分的数量”更改为表示“[最小货币单位]的数量”并更改倍数即可。 是的,詹姆斯威廉姆斯说的。所有货币都可以正确地表示为整数。 不完全。货币,是的,只要您不需要代表半美分或其他任何东西。然后你可能会说“所以数一半或十分之一美分”,但四分之一呢?八分之一?唯一合适的解决方案是 NSDecimalNumber(或类似的东西),它将问题推迟到 10^-128 ¢。 您好 Wisequark,感谢您的回答。事实上,它在发布时是完美的,但它让我意识到我需要指定我是在国际化之后;我相应地编辑了我的帖子,并使这个答案过时了,这是我的错。谢谢你帮助我集中注意力。大卫【参考方案5】:

一个更好的问题是,什么时候应该使用 NSDecimalNumber 来处理金钱。对这个问题的简短回答是,当您不能容忍 NSDecimalNumber 的性能开销并且您不关心小的舍入误差时,因为您处理的精度永远不会超过几位数。更简短的答案是,您应该始终在处理金钱时使用 NSDecimalNumber。

【讨论】:

【参考方案6】:

VISA、万事达卡和其他卡在传递金额时使用整数值。根据货币指数(除以或乘以 10^num,其中 num - 是货币的指数)正确解析金额取决于发送方和接收方。请注意,不同的货币有不同的指数。通常是 2(因此我们除以 100),但有些货币的指数 = 0(VND 等),或 = 3。

【讨论】:

以上是关于我应该使用 NSDecimalNumber 来处理金钱吗?的主要内容,如果未能解决你的问题,请参考以下文章

核心数据和 iPhone 的 NSDecimalNumber 问题

iOS - 数值精度处理之NSDecimalNumber

NSDecimalNumber 和从末尾弹出数字

NSDecimalNumber 和大的无符号长长(64 位)整数

NSDecimalNumber用于精度准确的计算

Cocoa:尾数和指数为 0 的 NSDecimalNumber