Money
给定货币中金额的精确、类型安全的表示。
这一功能在《Flight School Guide to Swift Numbers》的第 3 章中有详细介绍。
需求
- Swift 4.0+
安装
Swift 包管理器
将 Money 包添加到您的 Package.swift
中的目标依赖项
import PackageDescription
let package = Package(
name: "YourProject",
dependencies: [
.package(
url: "https://github.com/Flight-School/Money",
from: "1.4.0"
),
]
)
然后运行 swift build
命令来构建您的项目。
CocoaPods
您可以通过在 Podfile
中添加以下行来使用 CocoaPods 安装 Money
:
pod 'Money-FlightSchool', '~> 1.4.0'
运行 pod install
命令以下载库并将其集成到您的 Xcode 项目中。
注意此库的模块名为 "Money",即使用时,您只需将
import Money
添加到 Swift 代码的顶部,就像使用任何其他安装方法一样。因为这个名字已经有了一个 pod,所以这个 pod 被称为 “Money-FlightSchool”。
Carthage
要在 Xcode 项目中使用 Carthage 安装 Money
,请将其添加到 Cartfile
中
github "Flight-School/Money" ~> 1.4.0
然后运行 carthage update
命令来构建框架,并将构建好的 Money.framework 文件拖动到您的 Xcode 项目中。
用法
创建货币金额
Money
类型有一个必须的关联 Currency
类型。这些货币类型名称按照它们的三个字母 ISO 4701 货币代码命名。您可以使用 Decimal
值初始化货币金额
let amount = Decimal(12)
let monetaryAmount = Money<USD>(amount)
一些货币指定了一个小单位。例如,USD 金额通常以分表示,每分相当于 1/100 美元。您可以从小单位数量的货币初始化货币金额。对于没有小单位的货币,如 JPY,这与标准初始化器等效。
let twoCents = Money<USD>(minorUnits: 2)
twoCents.amount // 0.02
let ichimonEn = Money<JPY>(minorUnits: 10_000)
ichimonEn.amount // 10000
您还可以使用整数、浮点数和字符串字面量来创建货币金额。
12 as Money<USD>
12.00 as Money<USD>
"12.00" as Money<USD>
重要:Swift 浮点数字面量当前使用二进制浮点数类型初始化,这不能精确表示某些值。作为解决方案,从浮点数字面量初始化的货币金额将四舍五入到小数位货币单位。如果您想要表示更小的分数货币金额,请从字符串字面量或 Decimal
值初始化。
let preciseAmount: Money<USD> = "123.4567"
let roundedAmount: Money<USD> = 123.4567
preciseAmount.amount // 123.4567
roundedAmount.amount // 123.46
更多信息,请参阅 https://bugs.swift.org/browse/SR-920。
比较货币金额
您可以使用相同的货币比较两个货币金额
let amountInWallet: Money<USD> = 60.00
let price: Money<USD> = 19.99
amountInWallet >= price // true
尝试比较不同货币的货币金额会导致编译错误
let dollarAmount: Money<USD> = 123.45
let euroAmount: Money<EUR> = 4567.89
dollarAmount == euroAmount // Error: Binary operator '==' cannot be applied
添加、减法和乘法货币金额
可以使用标准的二进制算术运算符(+
、-
、*
)添加、减法和乘法货币金额
let prices: [Money<USD>] = [2.19, 5.39, 20.99, 2.99, 1.99, 1.99, 0.99]
let subtotal = prices.reduce(0.00, +) // "$36.53"
let tax = 0.08 * subtotal // "$2.92"
let total = subtotal + tax // "$39.45"
重要:将货币金额乘以浮点数会得到四舍五入到小数单位数位的金额。如果您想生成更小的分数货币金额,请用Decimal值相乘。
格式化货币金额
您可以使用NumberFormatter
创建货币金额的本地化表示。将格式化器的currencyCode
属性设置为Money
值的currency.code
属性,并将amount
属性传递给格式化器的string(for:)
方法。
let allowance: Money<USD> = 10.00
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "fr-FR")
formatter.currencyCode = allowance.currency.code
formatter.string(for: allowance.amount) // "10,00 $US"
编码和解码货币金额
编码
默认情况下,Money
值被编码为键控容器,其中amount
作为数值编码。
let value: Money<USD> = 123.45
let encoder = JSONEncoder()
let data = try encoder.encode(value)
String(data: data, encoding: .utf8) // #"{"amount":123.45,"currencyCode":"USD"}"#
要配置编码行为,请设置编码器的userInfo
属性的CodingUserInfoKey.moneyEncodingOptions
键或JSONEncoder.moneyEncodingOptions
属性。
var encoder = JSONEncoder()
encoder.moneyEncodingOptions = [.omitCurrency, .encodeAmountAsString]
let data = try encoder.encode([value])
String(data: data, encoding: .utf8) // #"["123.45"]"#
解码
默认解码行为灵活,支持带键和单值容器,用字符串或数字值的 amount
。
let json = #"""
[
{ "currencyCode": "USD", "amount": "100.00" },
50.00,
"10"
]
"""#.data(using: .utf8)!
let decoder = JSONDecoder()
let values = try decoder.decode([Money<USD>].self, from: json)
values.first?.amount // 100.00
values.last?.currency.code // "USD"
要配置解码行为,请设置解码器 userInfo
属性中的 JSONDecoder.moneyDecodingOptions
属性或 CodingUserInfoKey.moneyDecodingOptions
键。
var decoder = JSONDecoder()
decoder.moneyDecodingOptions = [.requireExplicitCurrency]
重要:当前 Foundation 解码器使用二进制浮点数类型解码数字值,这不能精确表示某些值。作为一种解决方案,您可以指定 requireStringAmount
解码选项,强制货币金额从字符串表示精确解码。
let json = #"""
{ "currencyCode": "USD", "amount": "27.31" }
"""#.data(using: .utf8)!
var decoder = JSONDecoder()
try decoder.decode(Money<USD>.self, from: json) // DecodingError
decoder.moneyDecodingOptions = [.requireStringAmount]
let preciseAmount = try decoder.decode(Money<USD>.self, from: json)
preciseAmount.amount // 27.31
或者,您可以使用 roundFloatingPointAmount
解码选项将解码的浮点值四舍五入到货币单位的小数位数。
let json = #"""
{ "currencyCode": "USD", "amount": 27.31 }
"""#.data(using: .utf8)!
var decoder = JSONDecoder()
let impreciseAmount = try decoder.decode(Money<USD>.self, from: json)
impreciseAmount.amount // 27.30999999...
decoder.moneyDecodingOptions = [.roundFloatingPointAmount]
let roundedAmount = try decoder.decode(Money<USD>.self, from: json)
roundedAmount.amount // 27.31
更多信息,请参阅 https://bugs.swift.org/browse/SR-7054。
自定义解码键
默认情况下,Money
值使用字符串键 "amount"
和 "currencyCode"
编码和解码,它们分别对应于它们各自属性。
如果您正在处理编码货币金额不同的数据,您可以设置 JSONDecoder
的 keyDecodingStrategy
属性以映射到不同的键名。
let json = #"""
{
"value": "3.33",
"currency": "USD"
}
"""#.data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ keys in
switch keys.last?.stringValue {
case "value":
return MoneyCodingKeys.amount
case "currency":
return MoneyCodingKeys.currencyCode
default:
return keys.last!
}
})
let amount = try decoder.decode(Money<USD>.self, from: json) // $3.33
另一个选择是创建符合您数据形状的结构,并派生计算属性以返回 Money
类型。
struct Item: Codable {
struct Price: Codable {
let value: String
let currency: String
}
let name: String
private let unitPrice: Price
var unitPriceInUSD: Money<USD>? {
guard unitPrice.currency == USD.code else { return nil }
return Money(unitPrice.value)
}
}
let json = #"""
{
"name": "Widget",
"unitPrice": {
"value": "3.33",
"currency": "USD"
}
}
"""#.data(using: .utf8)!
let decoder = JSONDecoder()
let item = try decoder.decode(Item.self, from: json)
item.unitPriceInUSD // $3.33
支持多种货币
考虑一个具有 price
属性的 Product
结构。如果您只支持单一货币,例如美元,则将 price
定义为类型 Money<USD>
。
struct Product {
var price: Money<USD>
}
但是,如果想要支持多种货币,您无法在属性声明中指定显式的货币类型。相反,必须将 Product
定义为泛型类型。
struct Product<Currency: CurrencyType> {
var price: Money<Currency>
}
不幸的是,这种方法不太方便,因为每个与 Product
交互的类型也必须泛型,依此类推,直到整个代码库对货币类型泛型。
class ViewController<Currency: CurrencyType> : UIViewController { ... } // 😭
更好的解决方案是定义一个新的 Price
协议,其要求匹配 Money
类型。
protocol Price {
var amount: Decimal { get }
var currency: CurrencyType.Type { get }
}
extension Money: Price {}
这样做可以在不使 Product
对货币类型泛型的情况下定义多种货币的价格。
struct Product {
var price: Price
}
let product = Product(price: 12.00 as Money<USD>)
product.price // "$12.00"
如果您只想支持某些货币,例如美元和欧元,您可以定义一个 SupportedCurrency
协议,并通过扩展将其添加到每个货币类型中。
protocol SupportedCurrency: CurrencyType {}
extension USD: SupportedCurrency {}
extension EUR: SupportedCurrency {}
extension Money: Price where Currency: SupportedCurrency {}
现在,尝试在不受支持的货币中使用 Product
创建一个程序将产生编译器错误。
Product(price: 100.00 as Money<EUR>)
Product(price: 100.00 as Money<GBP>) // Error
支持货币
本软件包为ISO 4217标准中定义的每种货币提供了一个Currency
类型,除了特殊代码,如USN(美元,次日到账)和XBC(欧洲债券市场单位,会计单位9)。
定义可用货币的源文件Currency.swift是从iso4217.csv文件使用GYB生成的。这个数据源与2018年8月17日发布的ISO 4217修订号169保持最新。
您可以通过安装GYB并在终端中运行make
命令从Resources/iso4217.csv
重新生成Sources/Money/Currency.swift
。
$ make
我们目前没有自动更新此数据源的机制。如果您知道对ISO 4217进行的任何新修订,请提交问题。
您可以使用iso4217Currency(for:)
函数通过其三位字母代码查找任何内置货币类型。
iso4217Currency(for: "USD")?.name // "US Dollar"
iso4217Currency(for: "invalid") // nil
添加自定义货币
您可以通过定义遵守《CurrencyType》协议的枚举来创建自己的自定义货币类型。例如,以下是如何表示比特币(BTC)的方法:
enum BTC: CurrencyType {
static var name: String { return "Bitcoin" }
static var code: String { return "BTC" }
static var minorUnit: Int { return 8 }
}
let satoshi: Money<BTC> = 0.00000001
NumberFormatter
只支持ISO 4217定义的货币,因此您必须配置符号、货币代码和其他必要的参数
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "₿"
formatter.currencyCode = "BTC"
formatter.maximumFractionDigits = 8
formatter.string(for: satoshi.amount) // ₿0.00000001
重要:函数
iso4217Currency(for:)
返回内置货币,因此调用iso4217Currency(for: "BTC")
将返回nil
。
使用表情符号炫耀
如果你是喜欢在源代码中插入剪贴画的人,这里有一个会让你的同事印象深刻的技巧
typealias 💵 = Money<USD>
typealias 💴 = Money<JPY>
typealias 💶 = Money<EUR>
typealias 💷 = Money<GBP>
let tubeFare: 💷 = 2.40 // "£2.40"
可以考虑的其他替代方案
类似于本包提供的类型安全的Money
结构可以降低某些编程错误的可能性。然而,您可能会发现使用这种抽象的成本大于其在您的代码库中可以带来的好处。
如果是这样的话,您可以考虑实现自己的简单Money
类型,如下使用嵌套的Currency
枚举
struct Money {
enum Currency: String {
case USD, EUR, GBP, CNY // supported currencies here
}
var amount: Decimal
var currency: Currency
}
最终,您需要决定哪种抽象最适合您特定的用例。无论您选择什么,都要确保使用带明确定义的货币类型的Decimal
来表示货币金额。
许可证
MIT
联系方式
Mattt (@mattt)