Money-FlightSchool 1.4.0

Money-FlightSchool 1.4.0

Mattt 维护。



Money

Build Status License Swift Version Cocoapods platforms Cocoapods compatible Carthage compatible

给定货币中金额的精确、类型安全的表示。

这一功能在《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" 编码和解码,它们分别对应于它们各自属性。

如果您正在处理编码货币金额不同的数据,您可以设置 JSONDecoderkeyDecodingStrategy 属性以映射到不同的键名。

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)