From cf39c185cf80384da3c7cb28a45d774aa1ce0c21 Mon Sep 17 00:00:00 2001 From: Nathan Harris Date: Fri, 28 Feb 2020 21:02:36 -0800 Subject: [PATCH] Refactor CurrencyMint (#21) Motivation: The current implementation of CurrencyMint is highly rigid and requires a ton of support from SwiftCurrency to be of true value. Cases of custom currencies and quick fallbacks to a default type requires a fair bit of boilerplate code that can be simplified with an improved re-implementation. In addition, there were several places of code duplication that are possible to simplify with Swift's literal syntax for easier maintenance. Modifications: - Add: `CurrencyIdentifier` for CurrencyMint to use in lookup resolution methods - Add: `standard` static singleton for CurrencyMint to resolve only ISO 4217 currencies - Change: `CurrencyMint` to store a fallback lookup closure for 3rd party and default currency resolution Result: Developers using CurrencyMint should have an easier time supporting their own currencies, or unsupported/missing ISO 4217 currencies. --- .../Currency/CurrencyMint+ISOCurrencies.swift | 355 +++++++++++++ .../CurrencyMint+ISOCurrencies.swift.gyb | 54 ++ Sources/Currency/CurrencyMint.swift | 492 ++++-------------- Sources/Currency/CurrencyMint.swift.gyb | 116 ----- Tests/CurrencyTests/CurrencyMintTests.swift | 66 ++- Tests/CurrencyTests/XCTestManifests.swift | 2 + swift-currency.xcodeproj/project.pbxproj | 8 +- 7 files changed, 573 insertions(+), 520 deletions(-) create mode 100644 Sources/Currency/CurrencyMint+ISOCurrencies.swift create mode 100644 Sources/Currency/CurrencyMint+ISOCurrencies.swift.gyb delete mode 100644 Sources/Currency/CurrencyMint.swift.gyb diff --git a/Sources/Currency/CurrencyMint+ISOCurrencies.swift b/Sources/Currency/CurrencyMint+ISOCurrencies.swift new file mode 100644 index 0000000..7ecf3d0 --- /dev/null +++ b/Sources/Currency/CurrencyMint+ISOCurrencies.swift @@ -0,0 +1,355 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCurrency open source project +// +// Copyright (c) 2020 SwiftCurrency project authors +// Licensed under MIT License +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCurrency project authors +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +// WARNING: This file's contents are automatically generated. Any edits to the `.swift` file will be overwritten. + +// MARK: Lookup by Alpha + +extension CurrencyMint { + internal static func lookup(byAlphaCode code: String) -> AnyCurrency.Type? { + switch code { + case "AED": return AED.self + case "AFN": return AFN.self + case "ALL": return ALL.self + case "AMD": return AMD.self + case "ANG": return ANG.self + case "AOA": return AOA.self + case "ARS": return ARS.self + case "AUD": return AUD.self + case "AWG": return AWG.self + case "AZN": return AZN.self + case "BAM": return BAM.self + case "BBD": return BBD.self + case "BDT": return BDT.self + case "BGN": return BGN.self + case "BHD": return BHD.self + case "BIF": return BIF.self + case "BMD": return BMD.self + case "BND": return BND.self + case "BOB": return BOB.self + case "BRL": return BRL.self + case "BSD": return BSD.self + case "BTN": return BTN.self + case "BWP": return BWP.self + case "BYN": return BYN.self + case "BZD": return BZD.self + case "CAD": return CAD.self + case "CDF": return CDF.self + case "CHF": return CHF.self + case "CLP": return CLP.self + case "CNY": return CNY.self + case "COP": return COP.self + case "CRC": return CRC.self + case "CUC": return CUC.self + case "CUP": return CUP.self + case "CVE": return CVE.self + case "CZK": return CZK.self + case "DJF": return DJF.self + case "DKK": return DKK.self + case "DOP": return DOP.self + case "DZD": return DZD.self + case "EGP": return EGP.self + case "ERN": return ERN.self + case "ETB": return ETB.self + case "EUR": return EUR.self + case "FJD": return FJD.self + case "FKP": return FKP.self + case "GBP": return GBP.self + case "GEL": return GEL.self + case "GHS": return GHS.self + case "GIP": return GIP.self + case "GMD": return GMD.self + case "GNF": return GNF.self + case "GTQ": return GTQ.self + case "GYD": return GYD.self + case "HKD": return HKD.self + case "HNL": return HNL.self + case "HRK": return HRK.self + case "HTG": return HTG.self + case "HUF": return HUF.self + case "IDR": return IDR.self + case "ILS": return ILS.self + case "INR": return INR.self + case "IQD": return IQD.self + case "IRR": return IRR.self + case "ISK": return ISK.self + case "JMD": return JMD.self + case "JOD": return JOD.self + case "JPY": return JPY.self + case "KES": return KES.self + case "KGS": return KGS.self + case "KHR": return KHR.self + case "KMF": return KMF.self + case "KPW": return KPW.self + case "KRW": return KRW.self + case "KWD": return KWD.self + case "KYD": return KYD.self + case "KZT": return KZT.self + case "LAK": return LAK.self + case "LBP": return LBP.self + case "LKR": return LKR.self + case "LRD": return LRD.self + case "LSL": return LSL.self + case "LYD": return LYD.self + case "MAD": return MAD.self + case "MDL": return MDL.self + case "MGA": return MGA.self + case "MKD": return MKD.self + case "MMK": return MMK.self + case "MNT": return MNT.self + case "MOP": return MOP.self + case "MRU": return MRU.self + case "MUR": return MUR.self + case "MVR": return MVR.self + case "MWK": return MWK.self + case "MXN": return MXN.self + case "MYR": return MYR.self + case "MZN": return MZN.self + case "NAD": return NAD.self + case "NGN": return NGN.self + case "NIO": return NIO.self + case "NOK": return NOK.self + case "NPR": return NPR.self + case "NZD": return NZD.self + case "OMR": return OMR.self + case "PAB": return PAB.self + case "PEN": return PEN.self + case "PGK": return PGK.self + case "PHP": return PHP.self + case "PKR": return PKR.self + case "PLN": return PLN.self + case "PYG": return PYG.self + case "QAR": return QAR.self + case "RON": return RON.self + case "RSD": return RSD.self + case "RUB": return RUB.self + case "RWF": return RWF.self + case "SAR": return SAR.self + case "SBD": return SBD.self + case "SCR": return SCR.self + case "SDG": return SDG.self + case "SEK": return SEK.self + case "SGD": return SGD.self + case "SHP": return SHP.self + case "SLL": return SLL.self + case "SOS": return SOS.self + case "SRD": return SRD.self + case "SSP": return SSP.self + case "STN": return STN.self + case "SVC": return SVC.self + case "SYP": return SYP.self + case "SZL": return SZL.self + case "THB": return THB.self + case "TJS": return TJS.self + case "TMT": return TMT.self + case "TND": return TND.self + case "TOP": return TOP.self + case "TRY": return TRY.self + case "TTD": return TTD.self + case "TWD": return TWD.self + case "TZS": return TZS.self + case "UAH": return UAH.self + case "UGX": return UGX.self + case "USD": return USD.self + case "UYU": return UYU.self + case "UYW": return UYW.self + case "UZS": return UZS.self + case "VES": return VES.self + case "VND": return VND.self + case "VUV": return VUV.self + case "WST": return WST.self + case "XAF": return XAF.self + case "XCD": return XCD.self + case "XOF": return XOF.self + case "XPF": return XPF.self + case "YER": return YER.self + case "ZAR": return ZAR.self + case "ZMW": return ZMW.self + case "ZWL": return ZWL.self + case "XTS": return XTS.self + case "XXX": return XXX.self + default: return nil + } + } +} + +// MARK: Lookup by Numeric + +extension CurrencyMint { + internal static func lookup(byNumCode code: UInt16) -> AnyCurrency.Type? { + switch code { + case 784: return AED.self + case 971: return AFN.self + case 8: return ALL.self + case 51: return AMD.self + case 532: return ANG.self + case 973: return AOA.self + case 32: return ARS.self + case 36: return AUD.self + case 533: return AWG.self + case 944: return AZN.self + case 977: return BAM.self + case 52: return BBD.self + case 50: return BDT.self + case 975: return BGN.self + case 48: return BHD.self + case 108: return BIF.self + case 60: return BMD.self + case 96: return BND.self + case 68: return BOB.self + case 986: return BRL.self + case 44: return BSD.self + case 64: return BTN.self + case 72: return BWP.self + case 933: return BYN.self + case 84: return BZD.self + case 124: return CAD.self + case 976: return CDF.self + case 756: return CHF.self + case 152: return CLP.self + case 156: return CNY.self + case 170: return COP.self + case 188: return CRC.self + case 931: return CUC.self + case 192: return CUP.self + case 132: return CVE.self + case 203: return CZK.self + case 262: return DJF.self + case 208: return DKK.self + case 214: return DOP.self + case 12: return DZD.self + case 818: return EGP.self + case 232: return ERN.self + case 230: return ETB.self + case 978: return EUR.self + case 242: return FJD.self + case 238: return FKP.self + case 826: return GBP.self + case 981: return GEL.self + case 936: return GHS.self + case 292: return GIP.self + case 270: return GMD.self + case 324: return GNF.self + case 320: return GTQ.self + case 328: return GYD.self + case 344: return HKD.self + case 340: return HNL.self + case 191: return HRK.self + case 332: return HTG.self + case 348: return HUF.self + case 360: return IDR.self + case 376: return ILS.self + case 356: return INR.self + case 368: return IQD.self + case 364: return IRR.self + case 352: return ISK.self + case 388: return JMD.self + case 400: return JOD.self + case 392: return JPY.self + case 404: return KES.self + case 417: return KGS.self + case 116: return KHR.self + case 174: return KMF.self + case 408: return KPW.self + case 410: return KRW.self + case 414: return KWD.self + case 136: return KYD.self + case 398: return KZT.self + case 418: return LAK.self + case 422: return LBP.self + case 144: return LKR.self + case 430: return LRD.self + case 426: return LSL.self + case 434: return LYD.self + case 504: return MAD.self + case 498: return MDL.self + case 969: return MGA.self + case 807: return MKD.self + case 104: return MMK.self + case 496: return MNT.self + case 446: return MOP.self + case 929: return MRU.self + case 480: return MUR.self + case 462: return MVR.self + case 454: return MWK.self + case 484: return MXN.self + case 458: return MYR.self + case 943: return MZN.self + case 516: return NAD.self + case 566: return NGN.self + case 558: return NIO.self + case 578: return NOK.self + case 524: return NPR.self + case 554: return NZD.self + case 512: return OMR.self + case 590: return PAB.self + case 604: return PEN.self + case 598: return PGK.self + case 608: return PHP.self + case 586: return PKR.self + case 985: return PLN.self + case 600: return PYG.self + case 634: return QAR.self + case 946: return RON.self + case 941: return RSD.self + case 643: return RUB.self + case 646: return RWF.self + case 682: return SAR.self + case 90: return SBD.self + case 690: return SCR.self + case 938: return SDG.self + case 752: return SEK.self + case 702: return SGD.self + case 654: return SHP.self + case 694: return SLL.self + case 706: return SOS.self + case 968: return SRD.self + case 728: return SSP.self + case 930: return STN.self + case 222: return SVC.self + case 760: return SYP.self + case 748: return SZL.self + case 764: return THB.self + case 972: return TJS.self + case 934: return TMT.self + case 788: return TND.self + case 776: return TOP.self + case 949: return TRY.self + case 780: return TTD.self + case 901: return TWD.self + case 834: return TZS.self + case 980: return UAH.self + case 800: return UGX.self + case 840: return USD.self + case 858: return UYU.self + case 927: return UYW.self + case 860: return UZS.self + case 928: return VES.self + case 704: return VND.self + case 548: return VUV.self + case 882: return WST.self + case 950: return XAF.self + case 951: return XCD.self + case 952: return XOF.self + case 953: return XPF.self + case 886: return YER.self + case 710: return ZAR.self + case 967: return ZMW.self + case 932: return ZWL.self + case 963: return XTS.self + case 999: return XXX.self + default: return nil + } + } +} diff --git a/Sources/Currency/CurrencyMint+ISOCurrencies.swift.gyb b/Sources/Currency/CurrencyMint+ISOCurrencies.swift.gyb new file mode 100644 index 0000000..fdbf56f --- /dev/null +++ b/Sources/Currency/CurrencyMint+ISOCurrencies.swift.gyb @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCurrency open source project +// +// Copyright (c) 2020 SwiftCurrency project authors +// Licensed under MIT License +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCurrency project authors +// +// SPDX-License-Identifier: MIT +// +//===----------------------------------------------------------------------===// + +// WARNING: This file's contents are automatically generated. Any edits to the `.swift` file will be overwritten. + +// MARK: Lookup by Alpha + +%{ import csv }% +% with open('../../Resources/ISO4217.csv') as file: + % reader = csv.DictReader(file) +extension CurrencyMint { + internal static func lookup(byAlphaCode code: String) -> AnyCurrency.Type? { + switch code { + % for row in reader: + % alphaCode = row["Ccy"] + % if alphaCode: + case "${alphaCode}": return ${alphaCode}.self + %end + %end + default: return nil + } + } +} + +// MARK: Lookup by Numeric + +extension CurrencyMint { + internal static func lookup(byNumCode code: UInt16) -> AnyCurrency.Type? { + switch code { + % file.seek(0) + % next(reader) + % for row in reader: + %{ + alphaCode = row["Ccy"] + numCode = row["CcyNbr"] + }% + case ${numCode}: return ${alphaCode}.self + %end + default: return nil + } + } +} +%end diff --git a/Sources/Currency/CurrencyMint.swift b/Sources/Currency/CurrencyMint.swift index 701a737..d25686d 100644 --- a/Sources/Currency/CurrencyMint.swift +++ b/Sources/Currency/CurrencyMint.swift @@ -14,403 +14,123 @@ import struct Foundation.Decimal -/// A generator class that supports lookup of ISO 4217 currencies by their alphabetic and numeric codes. -public final class CurrencyMint { - public init() { } - - /// Attempts to find the appropriate currency type that matches the provided alphabetic code and intialize it. - /// - Paramter code: The alphabetic ISO 4217 code to search for. - /// - Returns: An instance of a currency that matches the provided `code`, with a `.zero` amount. Otherwise `nil`. - public func make(alphabeticCode code: String) -> AnyCurrency? { - return self.make(alphabeticCode: code, minorUnits: .zero) - } +// MARK: CurrencyIdentifier - /// Attempts to find the appropriate currency type that matches the provided alphabetic code and initialize it. - /// - Parameters: - /// - code: The alphabetic ISO 4217 code to search for. - /// - amount: The amount the instance should be set to. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(alphabeticCode code: String, amount: Decimal) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byAlphaCode: code) else { return nil } - return currencyType.init(amount: amount) - } - - /// Attempts to find the appropriate currency type that matches the provided alphabetic code and initialize it. +extension CurrencyMint { + /// An identifier of a currency. /// - /// See `CurrencyMetadata.minorUnits`. - /// - Parameters: - /// - code: The alphabetic ISO 4217 code to search for. - /// - minorUnits: The amount the instance should be set to, in the scale of it's smallest unit. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(alphabeticCode code: String, minorUnits: Int64) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byAlphaCode: code) else { return nil } - return currencyType.init(minorUnits: minorUnits) + /// Numeric codes are normally between 0-999 in value. + /// + /// Alphabetic codes are normally 3 Latin characters, and will be normalized to all capital letters. + public enum CurrencyIdentifier: Equatable, ExpressibleByStringLiteral, ExpressibleByIntegerLiteral { + case alphaCode(String), numericCode(UInt16) + + /// Creates an identifier to represent the alphabetic code. + /// - Parameter value: The alphabetic code identifier. + public init(_ value: String) { self = .alphaCode(value.uppercased()) } + + /// Creates an identifier to represent the numeric code. + /// - Parameter value: The numeric code identifier. + public init(_ value: UInt16) { self = .numericCode(value) } + + public init(stringLiteral value: String) { self.init(value) } + public init(integerLiteral value: UInt16) { self.init(value) } + + public static func ==(lhs: CurrencyIdentifier, rhs: CurrencyIdentifier) -> Bool { + switch (lhs, rhs) { + case let (.alphaCode(lhsValue), .alphaCode(rhsValue)): return lhsValue == rhsValue + case let (.numericCode(lhsValue), .numericCode(rhsValue)): return lhsValue == rhsValue + default: return false + } + } } - - /// Attempts to find the appropriate currency type that matches the provided numeric code and initialize it. - /// - Parameter code: The numeric ISO 4217 code to search for. - /// - Returns: An instance of a currency that matches the provided `code`, with a `.zero` amount. Otherwise `nil`. - public func make(numericCode code: UInt16) -> AnyCurrency? { - return self.make(numericCode: code, minorUnits: .zero) +} + +// MARK: CurrencyMint + +/// A generator object that supports creation of type-safe currencies by their alphabetic or numeric code identifiers. +/// +/// By default, the generator only references the ISO 4217 specification of identifiers to determine which currency type to create. +/// +/// let usd = CurrencyMint.standard.make(identifier: "USD", amount: 30.23) +/// print(usd) +/// // USD(30.23) +/// +/// If it's desired to support currencies not found in the supported ISO 4217 specification, +/// a closure that will be referenced as a fallback can be provided at initialization: +/// +/// let mint = CurrencyMint(fallbackLookup: { identifier in +/// guard identifier == .alphaCode("KED") else { return USD.self /* or nil */ } +/// return KED.self // the custom currency type +/// } +/// let ked = mint.make(identifier: "KED") +/// let unknownCurrency = mint.make(identifier: 666) +/// print(ked, unknownCurrency) +/// // KED(0), USD(0) +/// +/// In most cases, it's desired to just provide a single fallback currency when type lookup fails: +/// +/// let mint = CurrencyMint(defaultCurrency: USD.self) +/// let value = mint.make(identifier: "_SL", amount: 300) +/// print(value) +/// // USD(300) +/// +/// See [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) +public final class CurrencyMint { + /// A closure that receives a currency identifier and finds a matching concrete currency type. + public typealias IdentifierLookup = (CurrencyIdentifier) -> AnyCurrency.Type? + + /// Returns a shared currency generator that only provides ISO 4217 defined currencies. + public static var standard: CurrencyMint { return .init() } + + private init() { + self.fallbackLookup = { _ in return nil } } - - /// Attempts to find the appropriate currency type that matches the provided numeric code and initialize it. - /// - Parameters: - /// - code: The numeric ISO 4217 code to search for. - /// - amount: The amount the instance should be set to. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(numericCode code: UInt16, amount: Decimal) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byNumCode: code) else { return nil } - return currencyType.init(amount: amount) + + /// Creates an instance that will use the provided lookup closure if an identifier doesn't match the ISO 4217 specification. + /// - Parameter fallbackLookup: An escaping closure that will be invoked when a currency's identifier is not found in the ISO 4217 specification. + public init(fallbackLookup: @escaping IdentifierLookup) { + self.fallbackLookup = fallbackLookup } - - /// Attempts to find the appropriate currency type that matches the provided numeric code and initialize it. - /// - /// See `CurrencyMetadata.minorUnits`. - /// - Parameters: - /// - code: The numeric ISO 4217 code to search for. - /// - minorUnits: The amount the instance should be set to, in the scale of it's smallest unit. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(numericCode code: UInt16, minorUnits: Int64) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byNumCode: code) else { return nil } - return currencyType.init(minorUnits: minorUnits) + + /// Creates an instance that will always resolves the provided currency type when ISO 4217 specification lookup fails. + /// - Parameter defaultCurrency: The default currency type to provide when a currency's identifier is not found in the ISO 4217 specification. + public init(defaultCurrency: T.Type) { + self.fallbackLookup = { _ in return defaultCurrency } } -} -// Contents following this line are automatically generated, and should not be edited. + private let fallbackLookup: IdentifierLookup +} +// MARK: Factory Methods extension CurrencyMint { - public static func lookup(byAlphaCode code: String) -> AnyCurrency.Type? { - switch code { - case "AED": return AED.self - case "AFN": return AFN.self - case "ALL": return ALL.self - case "AMD": return AMD.self - case "ANG": return ANG.self - case "AOA": return AOA.self - case "ARS": return ARS.self - case "AUD": return AUD.self - case "AWG": return AWG.self - case "AZN": return AZN.self - case "BAM": return BAM.self - case "BBD": return BBD.self - case "BDT": return BDT.self - case "BGN": return BGN.self - case "BHD": return BHD.self - case "BIF": return BIF.self - case "BMD": return BMD.self - case "BND": return BND.self - case "BOB": return BOB.self - case "BRL": return BRL.self - case "BSD": return BSD.self - case "BTN": return BTN.self - case "BWP": return BWP.self - case "BYN": return BYN.self - case "BZD": return BZD.self - case "CAD": return CAD.self - case "CDF": return CDF.self - case "CHF": return CHF.self - case "CLP": return CLP.self - case "CNY": return CNY.self - case "COP": return COP.self - case "CRC": return CRC.self - case "CUC": return CUC.self - case "CUP": return CUP.self - case "CVE": return CVE.self - case "CZK": return CZK.self - case "DJF": return DJF.self - case "DKK": return DKK.self - case "DOP": return DOP.self - case "DZD": return DZD.self - case "EGP": return EGP.self - case "ERN": return ERN.self - case "ETB": return ETB.self - case "EUR": return EUR.self - case "FJD": return FJD.self - case "FKP": return FKP.self - case "GBP": return GBP.self - case "GEL": return GEL.self - case "GHS": return GHS.self - case "GIP": return GIP.self - case "GMD": return GMD.self - case "GNF": return GNF.self - case "GTQ": return GTQ.self - case "GYD": return GYD.self - case "HKD": return HKD.self - case "HNL": return HNL.self - case "HRK": return HRK.self - case "HTG": return HTG.self - case "HUF": return HUF.self - case "IDR": return IDR.self - case "ILS": return ILS.self - case "INR": return INR.self - case "IQD": return IQD.self - case "IRR": return IRR.self - case "ISK": return ISK.self - case "JMD": return JMD.self - case "JOD": return JOD.self - case "JPY": return JPY.self - case "KES": return KES.self - case "KGS": return KGS.self - case "KHR": return KHR.self - case "KMF": return KMF.self - case "KPW": return KPW.self - case "KRW": return KRW.self - case "KWD": return KWD.self - case "KYD": return KYD.self - case "KZT": return KZT.self - case "LAK": return LAK.self - case "LBP": return LBP.self - case "LKR": return LKR.self - case "LRD": return LRD.self - case "LSL": return LSL.self - case "LYD": return LYD.self - case "MAD": return MAD.self - case "MDL": return MDL.self - case "MGA": return MGA.self - case "MKD": return MKD.self - case "MMK": return MMK.self - case "MNT": return MNT.self - case "MOP": return MOP.self - case "MRU": return MRU.self - case "MUR": return MUR.self - case "MVR": return MVR.self - case "MWK": return MWK.self - case "MXN": return MXN.self - case "MYR": return MYR.self - case "MZN": return MZN.self - case "NAD": return NAD.self - case "NGN": return NGN.self - case "NIO": return NIO.self - case "NOK": return NOK.self - case "NPR": return NPR.self - case "NZD": return NZD.self - case "OMR": return OMR.self - case "PAB": return PAB.self - case "PEN": return PEN.self - case "PGK": return PGK.self - case "PHP": return PHP.self - case "PKR": return PKR.self - case "PLN": return PLN.self - case "PYG": return PYG.self - case "QAR": return QAR.self - case "RON": return RON.self - case "RSD": return RSD.self - case "RUB": return RUB.self - case "RWF": return RWF.self - case "SAR": return SAR.self - case "SBD": return SBD.self - case "SCR": return SCR.self - case "SDG": return SDG.self - case "SEK": return SEK.self - case "SGD": return SGD.self - case "SHP": return SHP.self - case "SLL": return SLL.self - case "SOS": return SOS.self - case "SRD": return SRD.self - case "SSP": return SSP.self - case "STN": return STN.self - case "SVC": return SVC.self - case "SYP": return SYP.self - case "SZL": return SZL.self - case "THB": return THB.self - case "TJS": return TJS.self - case "TMT": return TMT.self - case "TND": return TND.self - case "TOP": return TOP.self - case "TRY": return TRY.self - case "TTD": return TTD.self - case "TWD": return TWD.self - case "TZS": return TZS.self - case "UAH": return UAH.self - case "UGX": return UGX.self - case "USD": return USD.self - case "UYU": return UYU.self - case "UYW": return UYW.self - case "UZS": return UZS.self - case "VES": return VES.self - case "VND": return VND.self - case "VUV": return VUV.self - case "WST": return WST.self - case "XAF": return XAF.self - case "XCD": return XCD.self - case "XOF": return XOF.self - case "XPF": return XPF.self - case "YER": return YER.self - case "ZAR": return ZAR.self - case "ZMW": return ZMW.self - case "ZWL": return ZWL.self - case "XTS": return XTS.self - case "XXX": return XXX.self - default: return nil - } + /// Creates a currency value for the provided identifier. + /// - Parameters: + /// - identifier: The identifier of the currency to be created. + /// - minorUnits: The quantity of minor units the currency value should represent. The default is `0`. + /// - Returns: An instance of a currency that matches the provided identifier with the desired amount; otherwise `nil`. + public func make(identifier: CurrencyIdentifier, minorUnits value: Int64 = .zero) -> AnyCurrency? { + guard let currencyType = self.lookup(identifier) else { return nil } + return currencyType.init(minorUnits: value) + } + + /// Creates a currency value for the provided identifier. + /// - Parameters: + /// - identifier: The identifier of the currency to be created. + /// - amount: The amount the currency value should represent. + /// - Returns: An instance of a currency that matches the provided identifier with the desired amount; otherwise `nil`. + public func make(identifier: CurrencyIdentifier, amount value: Decimal) -> AnyCurrency? { + guard let currencyType = self.lookup(identifier) else { return nil } + return currencyType.init(amount: value) } - - public static func lookup(byNumCode code: UInt16) -> AnyCurrency.Type? { - switch code { - case 784: return AED.self - case 971: return AFN.self - case 8: return ALL.self - case 51: return AMD.self - case 532: return ANG.self - case 973: return AOA.self - case 32: return ARS.self - case 36: return AUD.self - case 533: return AWG.self - case 944: return AZN.self - case 977: return BAM.self - case 52: return BBD.self - case 50: return BDT.self - case 975: return BGN.self - case 48: return BHD.self - case 108: return BIF.self - case 60: return BMD.self - case 96: return BND.self - case 68: return BOB.self - case 986: return BRL.self - case 44: return BSD.self - case 64: return BTN.self - case 72: return BWP.self - case 933: return BYN.self - case 84: return BZD.self - case 124: return CAD.self - case 976: return CDF.self - case 756: return CHF.self - case 152: return CLP.self - case 156: return CNY.self - case 170: return COP.self - case 188: return CRC.self - case 931: return CUC.self - case 192: return CUP.self - case 132: return CVE.self - case 203: return CZK.self - case 262: return DJF.self - case 208: return DKK.self - case 214: return DOP.self - case 12: return DZD.self - case 818: return EGP.self - case 232: return ERN.self - case 230: return ETB.self - case 978: return EUR.self - case 242: return FJD.self - case 238: return FKP.self - case 826: return GBP.self - case 981: return GEL.self - case 936: return GHS.self - case 292: return GIP.self - case 270: return GMD.self - case 324: return GNF.self - case 320: return GTQ.self - case 328: return GYD.self - case 344: return HKD.self - case 340: return HNL.self - case 191: return HRK.self - case 332: return HTG.self - case 348: return HUF.self - case 360: return IDR.self - case 376: return ILS.self - case 356: return INR.self - case 368: return IQD.self - case 364: return IRR.self - case 352: return ISK.self - case 388: return JMD.self - case 400: return JOD.self - case 392: return JPY.self - case 404: return KES.self - case 417: return KGS.self - case 116: return KHR.self - case 174: return KMF.self - case 408: return KPW.self - case 410: return KRW.self - case 414: return KWD.self - case 136: return KYD.self - case 398: return KZT.self - case 418: return LAK.self - case 422: return LBP.self - case 144: return LKR.self - case 430: return LRD.self - case 426: return LSL.self - case 434: return LYD.self - case 504: return MAD.self - case 498: return MDL.self - case 969: return MGA.self - case 807: return MKD.self - case 104: return MMK.self - case 496: return MNT.self - case 446: return MOP.self - case 929: return MRU.self - case 480: return MUR.self - case 462: return MVR.self - case 454: return MWK.self - case 484: return MXN.self - case 458: return MYR.self - case 943: return MZN.self - case 516: return NAD.self - case 566: return NGN.self - case 558: return NIO.self - case 578: return NOK.self - case 524: return NPR.self - case 554: return NZD.self - case 512: return OMR.self - case 590: return PAB.self - case 604: return PEN.self - case 598: return PGK.self - case 608: return PHP.self - case 586: return PKR.self - case 985: return PLN.self - case 600: return PYG.self - case 634: return QAR.self - case 946: return RON.self - case 941: return RSD.self - case 643: return RUB.self - case 646: return RWF.self - case 682: return SAR.self - case 90: return SBD.self - case 690: return SCR.self - case 938: return SDG.self - case 752: return SEK.self - case 702: return SGD.self - case 654: return SHP.self - case 694: return SLL.self - case 706: return SOS.self - case 968: return SRD.self - case 728: return SSP.self - case 930: return STN.self - case 222: return SVC.self - case 760: return SYP.self - case 748: return SZL.self - case 764: return THB.self - case 972: return TJS.self - case 934: return TMT.self - case 788: return TND.self - case 776: return TOP.self - case 949: return TRY.self - case 780: return TTD.self - case 901: return TWD.self - case 834: return TZS.self - case 980: return UAH.self - case 800: return UGX.self - case 840: return USD.self - case 858: return UYU.self - case 927: return UYW.self - case 860: return UZS.self - case 928: return VES.self - case 704: return VND.self - case 548: return VUV.self - case 882: return WST.self - case 950: return XAF.self - case 951: return XCD.self - case 952: return XOF.self - case 953: return XPF.self - case 886: return YER.self - case 710: return ZAR.self - case 967: return ZMW.self - case 932: return ZWL.self - case 963: return XTS.self - case 999: return XXX.self - default: return nil + + private func lookup(_ identifier: CurrencyIdentifier) -> AnyCurrency.Type? { + var typeFound: AnyCurrency.Type? = nil + switch identifier { + case let .alphaCode(value): typeFound = CurrencyMint.lookup(byAlphaCode: value) + case let .numericCode(value): typeFound = CurrencyMint.lookup(byNumCode: value) } + return typeFound ?? self.fallbackLookup(identifier) } } - diff --git a/Sources/Currency/CurrencyMint.swift.gyb b/Sources/Currency/CurrencyMint.swift.gyb deleted file mode 100644 index 6903d0e..0000000 --- a/Sources/Currency/CurrencyMint.swift.gyb +++ /dev/null @@ -1,116 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftCurrency open source project -// -// Copyright (c) 2020 SwiftCurrency project authors -// Licensed under MIT License -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftCurrency project authors -// -// SPDX-License-Identifier: MIT -// -//===----------------------------------------------------------------------===// - -import struct Foundation.Decimal - -/// A generator class that supports lookup of ISO 4217 currencies by their alphabetic and numeric codes. -public final class CurrencyMint { - public init() { } - - /// Attempts to find the appropriate currency type that matches the provided alphabetic code and intialize it. - /// - Paramter code: The alphabetic ISO 4217 code to search for. - /// - Returns: An instance of a currency that matches the provided `code`, with a `.zero` amount. Otherwise `nil`. - public func make(alphabeticCode code: String) -> AnyCurrency? { - return self.make(alphabeticCode: code, minorUnits: .zero) - } - - /// Attempts to find the appropriate currency type that matches the provided alphabetic code and initialize it. - /// - Parameters: - /// - code: The alphabetic ISO 4217 code to search for. - /// - amount: The amount the instance should be set to. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(alphabeticCode code: String, amount: Decimal) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byAlphaCode: code) else { return nil } - return currencyType.init(amount: amount) - } - - /// Attempts to find the appropriate currency type that matches the provided alphabetic code and initialize it. - /// - /// See `CurrencyMetadata.minorUnits`. - /// - Parameters: - /// - code: The alphabetic ISO 4217 code to search for. - /// - minorUnits: The amount the instance should be set to, in the scale of it's smallest unit. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(alphabeticCode code: String, minorUnits: Int64) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byAlphaCode: code) else { return nil } - return currencyType.init(minorUnits: minorUnits) - } - - /// Attempts to find the appropriate currency type that matches the provided numeric code and initialize it. - /// - Parameter code: The numeric ISO 4217 code to search for. - /// - Returns: An instance of a currency that matches the provided `code`, with a `.zero` amount. Otherwise `nil`. - public func make(numericCode code: UInt16) -> AnyCurrency? { - return self.make(numericCode: code, minorUnits: .zero) - } - - /// Attempts to find the appropriate currency type that matches the provided numeric code and initialize it. - /// - Parameters: - /// - code: The numeric ISO 4217 code to search for. - /// - amount: The amount the instance should be set to. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(numericCode code: UInt16, amount: Decimal) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byNumCode: code) else { return nil } - return currencyType.init(amount: amount) - } - - /// Attempts to find the appropriate currency type that matches the provided numeric code and initialize it. - /// - /// See `CurrencyMetadata.minorUnits`. - /// - Parameters: - /// - code: The numeric ISO 4217 code to search for. - /// - minorUnits: The amount the instance should be set to, in the scale of it's smallest unit. - /// - Returns: An instance of a currency that matches the provided `code`, with the appropriate value. Otherwise `nil`. - public func make(numericCode code: UInt16, minorUnits: Int64) -> AnyCurrency? { - guard let currencyType = CurrencyMint.lookup(byNumCode: code) else { return nil } - return currencyType.init(minorUnits: minorUnits) - } -} - -% warning = "Contents following this line are automatically generated, and should not be edited." -// ${warning} - -%{ import csv }% -% with open('../../Resources/ISO4217.csv') as file: - % reader = csv.DictReader(file) - -extension CurrencyMint { - public static func lookup(byAlphaCode code: String) -> AnyCurrency.Type? { - switch code { - % for row in reader: - % alphaCode = row["Ccy"] - % if alphaCode: - case "${alphaCode}": return ${alphaCode}.self - %end - %end - default: return nil - } - } - - public static func lookup(byNumCode code: UInt16) -> AnyCurrency.Type? { - switch code { - % file.seek(0) - % next(reader) - % for row in reader: - %{ - alphaCode = row["Ccy"] - numCode = row["CcyNbr"] - }% - case ${numCode}: return ${alphaCode}.self - %end - default: return nil - } - } -} - -%end diff --git a/Tests/CurrencyTests/CurrencyMintTests.swift b/Tests/CurrencyTests/CurrencyMintTests.swift index 03a2b78..9083abf 100644 --- a/Tests/CurrencyTests/CurrencyMintTests.swift +++ b/Tests/CurrencyTests/CurrencyMintTests.swift @@ -17,46 +17,80 @@ import XCTest final class CurrencyMintTests: XCTestCase { func testLookupByString_passes() { - let mint = CurrencyMint() - let pounds = mint.make(alphabeticCode: "GBP") + let pounds = CurrencyMint.standard.make(identifier: "GBP") + XCTAssertTrue(pounds is GBP) XCTAssertEqual(pounds?.amount, .zero) } func testLookupByString_withAmount() { - let mint = CurrencyMint() + let mint = CurrencyMint.standard - let yen = mint.make(alphabeticCode: "JPY", amount: 302.98) + let yen = mint.make(identifier: "JPY", amount: 302.98) + XCTAssertTrue(yen is JPY) XCTAssertEqual(yen?.amount, 303) - let usd = mint.make(alphabeticCode: "USD", minorUnits: 549) + let usd = mint.make(identifier: "USD", minorUnits: 549) + XCTAssertTrue(usd is USD) XCTAssertEqual(usd?.amount, 5.49) } func testLookupByString_fails() { - let mint = CurrencyMint() - let talons = mint.make(alphabeticCode: "KLT") - XCTAssertNil(talons) + let darseks = CurrencyMint.standard.make(identifier: "KED", amount: 30.23) + XCTAssertNil(darseks) } func testLookupByNum_passes() { - let mint = CurrencyMint() - let euros = mint.make(numericCode: 978) + let euros = CurrencyMint.standard.make(identifier: 978) + XCTAssertTrue(euros is EUR) XCTAssertEqual(euros?.amount, .zero) } func testLookupByNum_withAmount() { - let mint = CurrencyMint() + let mint = CurrencyMint.standard - let pesos = mint.make(numericCode: 484, amount: 3098.9823) + let pesos = mint.make(identifier: 484, amount: 3098.9823) + XCTAssertTrue(pesos is MXN) XCTAssertEqual(pesos?.amount, 3098.98) - let omanis = mint.make(numericCode: 512, minorUnits: 198239) + let omanis = mint.make(identifier: 512, minorUnits: 198239) + XCTAssertTrue(omanis is OMR) XCTAssertEqual(omanis?.amount, 198.239) } func testLookupByNum_fails() { - let mint = CurrencyMint() - let talons = mint.make(numericCode: 666) - XCTAssertNil(talons) + let darseks = CurrencyMint.standard.make(identifier: 666) + XCTAssertNil(darseks) + } + + func testDefaultCurrency() { + let mint = CurrencyMint(defaultCurrency: USD.self) + let money = mint.make(identifier: "KED") + XCTAssertTrue(money is USD) + } + + func testFallbackLookup() { + struct KED: CurrencyProtocol, CurrencyMetadata { + static var name: String { return "Klingon Darseks" } + static var alphabeticCode: String { return "KED" } + static var numericCode: UInt16 { return 666 } + static var minorUnits: UInt8 { return 3 } + + var minorUnits: Int64 + + public init(minorUnits: T) { self.minorUnits = .init(minorUnits) } + } + + let mint = CurrencyMint(fallbackLookup: { identifier in + guard identifier == .alphaCode("KED") || identifier == .numericCode(666) else { return nil } + return KED.self + }) + + let d1 = mint.make(identifier: "KED", amount: 30.239) + XCTAssertTrue(d1 is KED) + XCTAssertEqual(d1 as? KED, 30.239) + + let d2 = mint.make(identifier: 666, minorUnits: d1?.minorUnits ?? .zero) + XCTAssertTrue(d2 is KED) + XCTAssertEqual(d2?.minorUnits, 30239) } } diff --git a/Tests/CurrencyTests/XCTestManifests.swift b/Tests/CurrencyTests/XCTestManifests.swift index a2b75b2..d891989 100644 --- a/Tests/CurrencyTests/XCTestManifests.swift +++ b/Tests/CurrencyTests/XCTestManifests.swift @@ -50,6 +50,8 @@ extension CurrencyMintTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__CurrencyMintTests = [ + ("testDefaultCurrency", testDefaultCurrency), + ("testFallbackLookup", testFallbackLookup), ("testLookupByNum_fails", testLookupByNum_fails), ("testLookupByNum_passes", testLookupByNum_passes), ("testLookupByNum_withAmount", testLookupByNum_withAmount), diff --git a/swift-currency.xcodeproj/project.pbxproj b/swift-currency.xcodeproj/project.pbxproj index 9746346..28315ea 100644 --- a/swift-currency.xcodeproj/project.pbxproj +++ b/swift-currency.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 0318F9AB2409DD130017B010 /* CurrencyMint+ISOCurrencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0318F9AA2409DD120017B010 /* CurrencyMint+ISOCurrencies.swift */; }; 03AE0FB82401E5BA00A76C38 /* Decimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AE0FB72401E5BA00A76C38 /* Decimal.swift */; }; D814C41723D68385007D4037 /* CurrencyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D814C41623D68385007D4037 /* CurrencyProtocol.swift */; }; D838378B23CEBF2D0017B4D2 /* AnyCurrency+Sequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D838378A23CEBF2D0017B4D2 /* AnyCurrency+Sequence.swift */; }; @@ -55,12 +56,13 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0318F9AA2409DD120017B010 /* CurrencyMint+ISOCurrencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CurrencyMint+ISOCurrencies.swift"; sourceTree = ""; }; 03AE0FB72401E5BA00A76C38 /* Decimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decimal.swift; sourceTree = ""; }; D814C41623D68385007D4037 /* CurrencyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyProtocol.swift; sourceTree = ""; }; D838378A23CEBF2D0017B4D2 /* AnyCurrency+Sequence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AnyCurrency+Sequence.swift"; sourceTree = ""; }; D893657C23D2944B0006FAE1 /* AnyCurrencyAlgorithmsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCurrencyAlgorithmsTests.swift; sourceTree = ""; }; D893657E23D294850006FAE1 /* AnyCurrency+Algorithms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyCurrency+Algorithms.swift"; sourceTree = ""; }; - OBJ_10 /* CurrencyMint.swift.gyb */ = {isa = PBXFileReference; lastKnownFileType = text; path = CurrencyMint.swift.gyb; sourceTree = ""; }; + OBJ_10 /* CurrencyMint+ISOCurrencies.swift.gyb */ = {isa = PBXFileReference; lastKnownFileType = text; path = "CurrencyMint+ISOCurrencies.swift.gyb"; sourceTree = ""; }; OBJ_11 /* AnyCurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCurrency.swift; sourceTree = ""; }; OBJ_12 /* CurrencyMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyMetadata.swift; sourceTree = ""; }; OBJ_13 /* CurrencyMint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyMint.swift; sourceTree = ""; }; @@ -164,7 +166,8 @@ D893657E23D294850006FAE1 /* AnyCurrency+Algorithms.swift */, D838378A23CEBF2D0017B4D2 /* AnyCurrency+Sequence.swift */, OBJ_12 /* CurrencyMetadata.swift */, - OBJ_10 /* CurrencyMint.swift.gyb */, + OBJ_10 /* CurrencyMint+ISOCurrencies.swift.gyb */, + 0318F9AA2409DD120017B010 /* CurrencyMint+ISOCurrencies.swift */, OBJ_13 /* CurrencyMint.swift */, D814C41623D68385007D4037 /* CurrencyProtocol.swift */, OBJ_9 /* ISOCurrencies.swift.gyb */, @@ -283,6 +286,7 @@ D814C41723D68385007D4037 /* CurrencyProtocol.swift in Sources */, OBJ_31 /* AnyCurrency.swift in Sources */, OBJ_32 /* CurrencyMetadata.swift in Sources */, + 0318F9AB2409DD130017B010 /* CurrencyMint+ISOCurrencies.swift in Sources */, D838378B23CEBF2D0017B4D2 /* AnyCurrency+Sequence.swift in Sources */, D893657F23D294850006FAE1 /* AnyCurrency+Algorithms.swift in Sources */, OBJ_33 /* CurrencyMint.swift in Sources */,