金额

License

Money 是一个便于处理和显示货币及金额的包。

目标

  1. 精确性:允许处理金额而不损失精度。这是通过使用 Decimal 类型来表示金额来实现的。 Decimal 不会遭受(可能)不精确的表示形式的影响,这使得浮点数不适合作为金额的载体。
  2. 类型安全:禁止不同货币的金额之间的算术运算。这是通过使具体的货币类型(Tender)泛型化其货币来实现的,从而在处理货币类型时启用编译器级别的支持。
  3. 灵活性:但是,我们必须灵活地允许动态创建具体的金额,并允许包含不同货币金额的集合。 这是通过 Money 协议实现的。
  4. 便捷性:使用金额应该像使用内置数字类型一样容易。 这是通过添加便于处理金额的日常需求的规定来实现的。

实现

货币由符合 Currency 协议的无大小写枚举表示。我们使用无大小写枚举,因为我们只想将货币用作类型。 我们不需要货币的实例,因为我们不需要它们,并且无大小写枚举无法实例化。货币具有 codeminorUnitScaleminorUnitScale 表示主(主要)货币单位的细分。例如:欧元和美元分为美分,因此它们的 minorUnitScale2。每种货币也有一个名称。我们从平台派生(本地化)名称,因为 Apple 平台提供此信息。

我们为协议中定义的所有属性提供默认实现,以避免代码重复。唯一预计偶尔会被覆盖的实现是 minorUnitScale 的实现。绝大多数货币的单位刻度为 2,但有些货币偏离了这一点。我们提供了一个已知货币的默认列表,我们预计很少需要更新该列表。我们提供的列表基于 Apple 平台已知的常见货币,因此不能保证与 ISO 4217 货币列表完全 100% 相同,但在撰写本文时,差异非常小,可以忽略不计。但是,您的需求可能会有所不同,因此您可以选择根据需要更改列表。

对于金额的表示,我们使用两种相关类型:Money,一个表示金额的协议;以及 Tender,一种符合 Money 的类型。 Tender 是泛型化的,它代表的货币,这提供了编译器辅助的类型安全(例如,禁止添加不同货币的 Tender),而它对 Money 协议的符合性提供了必要的灵活性,可以处理各种货币的集合,以及在编译时货币未知的情况下运行时实例化金额。

Money 视为抽象类型,在运行时提供动态灵活性,并将 Tender 视为用于处理的具体金额。

虽然您不能添加或减去不同货币的 Tender(如果不首先将它们转换为相同的货币,这将毫无意义),或者将一个 Tender 乘以另一个 Tender,但您可以Tender 执行算术运算。例如,您可以将 Tender 乘以一个数字类型:€5.42 * 3.14。

用法

货币已经提供,因此您无需创建货币。但是,如果出于任何原因,您发现需要创建自定义货币,则可以将新的货币类型创建为符合 Currency 协议的枚举。如果您的自定义货币需要的 minorUnitScale 不是标准的 2,那么您需要在枚举的主体中提供自定义实现

public enum MyCustomCurrency: Currency { public static var minorUnitScale: Int { 4 } }

否则,您可以创建一个带有空主体的枚举

public enum MyCustomCurrency: Currency {}

通过创建 Tender 的实例来创建具体的金额,并提供它泛型化的货币。最好从 DecimalIntString 创建 Tender,而不是从浮点值创建,尽管如果您真的需要,也可以使用它们。

    let amount: Decimal = 3.14 // (or, preferably: Decimal(string: "3.14"))
    let someMoney1 = Tender<EUR>(amount)
    let someMoney2 = Tender<USD> = 5
    let someMoney3 = Tender<JPY> = 3000
    let someMoney4 = Tender<SEK>("42")
    let someMoney5 = Tender<USD>("5,501", locale: Locale(identifier: "nl_NL")
    let someMoney6 = Tender<USD>("5.501", locale: Locale(identifier: "en_US")

    let ratherNot = Tender<EUR>(3.02) // 3.02 cannot be represented accurately as a floating point number, hence the tender initiated from it will inherently be flawed.

您不能添加或减去不同货币的 Tender。但是,您可以添加和减去相同货币的 Tender

    let sum = someMoney5 + someMoney6

您还可以比较相同货币的 Tender

    if someMoney5 < someMoney6 {
        …do something…
    }

查看 TenderTests 和 TenderAlgebraTests 文件,以获得对可能内容的良好概述。

当然,我们通常需要向用户显示我们的金额。为此,Money 类型提供了一个类型为 Displayabledisplayable,它有助于将金额转换为用户友好的字符串。当您想向用户显示金额时,向实例请求 displayable,并向 Displayable 请求所需的字符串

        someMoney5.displayable.formatted
        someMoney5.displayable.formattedTruncatingDecimals()
        someMoney5.displayable.formattedTruncatingDecimals(for: Locale(identifier: "en_GB")
        someMoney5.displayable.formattedWithoutCurrencySymbol()
        someMoney5.displayable.formattedWithoutCurrencySymbol(for: Locale(identifier: "pt_PT")

您还可以向 displayable 请求货币符号、小数点分隔符和货币名称。

如果您需要存储具有各种货币的金额的集合,则无法创建 Tender 的集合,因为 Tender 是泛型化的,它代表的货币。 在这种情况下,您应该创建一个 Money 的集合,因为 Money 虽然持有货币,但不是泛型化的。

        var variousCurrencies = [any Money]()
        variousCurrencies.append(Tender<EUR>(1))
        variousCurrencies.append(Tender<USD>(0.5))

此外,如果您需要在运行时创建金额,而您在编译时不知道其货币,请使用 Money 类型来建模您的数据。 使用 MoneyFactory 从金额和货币代码获取具体的 Tender(类型为 Money

        let myRuntTimeAmount = Decimal(42)
        let myRuntimeCurrency = SupportedCurrency.INR
        
        try MoneyFactory.moneyFrom(amount: myRuntTimeAmount, currency: myRuntimeCurrency)