我应该使用哪种Swift数据类型来表示货币?

46

我有一个iOS应用程序,将对代表美元货币的数字执行许多基本算术运算(例如25.00表示$25.00)。

我在其他语言(如Java和Javascript)中使用Double数据类型时遇到很多问题,因此我想知道在Swift中使用哪种最佳数据类型来表示货币。


1
只是好奇,你在使用双精度作为货币数据类型时遇到了什么问题? - userx
8
@AbhishekMukherjee 我在比较数值时遇到了问题。当我认为 5.20 == 5.20 时,实际上是 5.200000001 == 5.20。 - PeonProgrammer
3
这个问题已经被广泛讨论过了。在Java中,正确的类型应该使用BigDecimal。 - Rob
对于我的项目,我最初将货币金额存储为Int(将美元金额乘以100来存储,然后在访问时将其反转并转换为Double)。然而,在实现了一个多货币系统之后,其中各种货币具有不同数量的小数位数,跟踪存储的Int或访问的Double金额以及它是原始货币还是基础货币,使用Int方法开销太大。当使用Double时,数学计算更容易,但最终我也推荐使用NSDecimalNumber - blwinters
@userx 我发现使用Double类型的问题在于仅仅进行减法运算就会导致非常长的浮点数值,我不知道为什么。例如,像650.50 - 300.50这样简单的计算结果却是350.0038420489380933,我也不知道为什么会这样。 - Hedylove
1
@JozemiteApps 阅读 http://www.toves.org/books/float/,你就会明白。浮点数值在其范围内的许多值都是固有的不精确的。 - Matt Kantor
6个回答

42

使用Decimal,并 确保正确初始化

正确的做法


// Initialising a Decimal from a Double:
let monetaryAmountAsDouble = 32.111
let decimal: Decimal = NSNumber(floatLiteral: 32.111).decimalValue
print(decimal) // 32.111  
let result = decimal / 2
print(result) // 16.0555 


// Initialising a Decimal from a String:
let monetaryAmountAsString = "32,111.01"

let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.numberStyle = .decimal

if let number = formatter.number(from: monetaryAmountAsString) {
    let decimal = number.decimalValue
    print(decimal) // 32111.01 
    let result = decimal / 2.1
    print(result) // 15290.9571428571428571428571428571428571 
}

不正确的

let monetaryAmountAsDouble = 32.111
let decimal = Decimal(monetaryAmountAsDouble) 
print(decimal) // 32.11099999999999488  

let monetaryAmountAsString = "32,111.01"
if let decimal = Decimal(string: monetaryAmountAsString, locale: Locale(identifier: "en_US")) {
    print(decimal) // 32  
}

在货币金额中进行双精度(Double)或单精度(Float)算术运算会产生不准确的结果。 这是因为DoubleFloat类型不能准确地表示大多数十进制数。更多信息请点击此处

简而言之:使用DecimalInt对货币金额执行算术运算


感谢您展示了正确的初始化方式。您能否也展示一下将 Decimal 转换回 String 的正确方式?我不确定 StackOverflow 上的其他答案是否展示得正确。 - zeeshan
使用 Decimal(floatLiteral:) 和使用 NSNumber(floatLiteral:).decimalValue 是否相等? - IloneSP
@Roberto 不知何故,Decimal(floatLiteral:) 会产生一个不正确的结果 (32.11099999999999488)。 - Shengchalover
1
使用 NSNumber(floatLiteral: 32.111).decimalValue 也是不正确的。问题在于将十进制数(浮点字面量)转换为有损二进制数(double),然后再转换为十进制数(Decimal)。如果您想要精度,请始终从字符串初始化。 - Sulthan
Decimal(string:locale:) 不识别分组分隔符。对于 "32111.01",结果将是正确的。 - Roman Aliyev

33

我建议你从为Decimal创建一个typealias开始。示例:

typealias Dollars = Decimal
let a = Dollars(123456)
let b = Dollars(1000)
let c = a / b
print(c)

输出:

123.456
如果您尝试从字符串中解析货币值,请使用NumberFormatter并将其generatesDecimalNumbers属性设置为true

我最喜欢这种方法,因为它感觉最容易使用。 - PeonProgrammer
1
为什么不直接创建一个Money类呢? - Rob
@Rob 为什么要创建一个类,而不直接使用“类型别名”。 - Hedylove
4
这是一种针对货币而言的糟糕解决方案,因为使用Double初始化Decimal很容易产生舍入误差。例如: Decimal(2.13) == 2.129999999999999488 - Eric
2
我的回答并不建议将Double转换为Decimal。我的回答展示了如何通过除以10的幂来准确地创建带有小数位的Decimal - rob mayoff
显示剩余3条评论

4

有一个非常好用的库叫做Money

let money: Money = 100
let moreMoney = money + 50 //150

除此之外,还有许多不错的功能,比如类型安全的货币:

let euros: EUR = 100
let dollars: USD = 1500
euros + dollars //Error

二元运算符“+”不能应用于类型为'EUR'(也称为'_Money')和'USD'(也称为'_Money')的操作数。

3
这个类有一个警告:它不符合NSCoding协议,这意味着您将无法使用NSCoder保存Money类型的值。Xcode会抛出一个错误。 - Hedylove

1

飞行学校/货币可能是一个充满财富的项目的最佳选择。

README结尾中,@mattt提供了一个简单的货币类型的不错解决方案:

struct Money {
    enum Currency: String {
      case USD, EUR, GBP, CNY // supported currencies here
    }

    var amount: Decimal
    var currency: Currency
}

《飞行学校指南:Swift数字》第3章提供了出色的介绍。


1
我们使用这种方法,其中Currency是一个巨大的枚举,在预先生成(不要忘记货币价值只是一个没有其货币的数字 - 它们都属于一起):
struct Money: Hashable, Equatable {

    let value: Decimal
    let currency: Currency
}

extension Money {

    var string: String {
        CurrencyFormatter.shared.string(from: self)
    }
}

0
你需要使用 Decimal 和它的初始化器:
Decimal(string: "12345.67890", locale: Locale(identifier: "en-US"))

考虑其他答案时,请注意FloatDouble字面量会影响十进制结果的精度。同样适用于NumberFormatter
Decimal(5.01) // 5.009999999999998976 ❌

Decimal(floatLiteral: 5.01) // 5.009999999999998976 ❌

NSNumber(floatLiteral: 0.9999999).decimalValue // 0.9999999000000001 ❌

let decimalFormatter = NumberFormatter()
decimalFormatter.numberStyle = .decimal
decimalFormatter.number(from: "0.9999999")?.decimalValue // 0.9999999000000001 ❌

Decimal(string: "5.01", locale: Locale(identifier: "en-US")) // 5.01 ✅
Decimal(string: "0.9999999", locale: Locale(identifier: "en-US")) // 0.9999999 ✅


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