0

From the open-source project github.com/Flight-School/Money currencies are declared like the following:

Currency.swift

public protocol CurrencyType {
    /// The three letter ISO 4217 currency code.
    static var code: String { get }

    /// The name of the currency.
    static var name: String { get }

    static var minorUnit: Int { get }
}

public enum EUR: CurrencyType {
    public static var code: String {
        return "EUR"
    }

    public static var name: String {
        return "Euro"
    }

    public static var minorUnit: Int {
        return 2
    }
}

public enum GBP: CurrencyType {
    public static var code: String {
        return "GBP"
    }

    public static var name: String {
        return "Pound Sterling"
    }

    public static var minorUnit: Int {
        return 2
    }
}

public enum USD: CurrencyType {
    public static var code: String {
        return "USD"
    }

    public static var name: String {
        return "US Dollar"
    }

    public static var minorUnit: Int {
        return 2
    }
}
// ^^^More than 150 more like this in the file...

And a Money struct in Money.swift like so:

public struct Money<Currency: CurrencyType>: Equatable, Hashable {
    /// The amount of money.
    public var amount: Decimal

    /// Creates an amount of money with a given decimal number.
    public init(_ amount: Decimal) {
        self.amount = amount
    }

    /// The currency type.
    public var currency: CurrencyType.Type {
        return Currency.self
    }
    
    /**
        A monetary amount rounded to
        the number of places of the minor currency unit.
     */
    public var rounded: Money<Currency> {
        return Money<Currency>(amount.rounded(for: Currency.self))
    }
}

// MARK: - Comparable

extension Money: Comparable {
    public static func < (lhs: Money<Currency>, rhs: Money<Currency>) -> Bool {
        return lhs.amount < rhs.amount
    }
}

Usage example:

let amount = Decimal(12)
let monetaryAmount = Money<USD>(amount) // Works with any hard-coded currency code

My Problematic Code:

What I'm trying to achieve is to construct a money object with the CurrencyType of the choice of user via their string input:

let userCurrencyCodeInput = "USD"
let userAmountInput = 39.95

let currency = CurrencyType(code: userCurrencyCodeInput ) // 'CurrencyType' cannot be constructed because it has no accessible initializers
let priceForUser = Money<currency>(userAmountInput) // Use of undeclared type 'currency'

I know that in order to get the corresponding CurrencyType enum item, I could use switch case statement but there are more than 150 currencies defined the above way (which means I would have to write more than 150 cases statically), so if there is a dynamic way of mapping a string code to the code property of the enumeration item and accessing it, I'd better learn and use it, otherwise just drop the entire library and start over the implementation in a more generic way.

GOs
  • 16
  • 1
  • 5
  • In this example enums are not used as enums at all, they don't have any `case` definitions, they are just used in place of structures (`struct`) to store some `public static var`. Why? I think whoever developed it confused the case of using enums for constants instead of structs (e.g. dicussion here: https://stackoverflow.com/questions/38585344/swift-constants-struct-or-enum). – timbre timbre Sep 28 '20 at 14:41
  • Although I would disagree with @KirilS. about the author's understanding ability (Mattt is one of the most celebrated iOS devs out there), I would agree that the choice of separate enums is _really_ weird. If you still want to use the library (this is a well thought project otherwise) you could fork it and change a bit the [.gyb file](https://nshipster.com/swift-gyb/) (the one that parses the csv with the currencies and produces the boilerplate code) – Alladinian Sep 28 '20 at 15:04
  • I have also studied and tried to use this project but had similar issues because I used Core Data and was to much trouble in converting persisted data to Money. I ended up using (almost) the same implementation for Money but created a struct for Currency instead. – Joakim Danielson Sep 28 '20 at 16:42
  • @JoakimDanielson my thinking points me to the same direction you described. – GOs Sep 29 '20 at 19:37

1 Answers1

0

There's no need for so many enums. I think what you're getting at is whether or not you can receive data from a CurrencyType based upon a String input. The following is an attempt to greatly reduce your code size and complexity.


Currency Types

Here, I've created an enum called CurrencyTypes. Note that I've conformed it to String and CaseIterable.

For each currency type case, I am assigning a String raw value.

For example, I created the USD case. Note how I've put all its data into 1 String: "USD.US Dollar.2". I've separated each piece of information by a period. Code, Name, and Minor Unit. If you ever was to extract this data, use the raw method, it will return (code:String, name:String, minorUnit: Int).

Finally, I've created a new initialization method. It allows you to initialize a currency type based upon its code or its name. If I wanted to make a USD, you can do any of the following:

  • let currencyType = CurrencyType("USD")!
  • let currencyType = CurrencyType("US Dollar")!

This allows us to make many new currency objects easily. If you want to introduce another kind of currency, all you have to do is add another case (Instead of creating a whole new enum).

enum CurrencyType: String, CaseIterable {
    case EUR = "EUR.Euro.2"
    case GBP = "GBP.Pound Sterling.2"
    case USD = "USD.US Dollar.2"
    // Add hundreds of cases based upon your liking
    
    /// Retrieve the **CODE**, **NAME**, and **MINOR UNIT** of a specified currency type
    var raw: (code: String, name: String, minorUnit: Int) {
        let values = self.rawValue.split(separator: ".").map { String($0) }
        return (values[0], values[1], Int(values[2])!)
    }

    /// Find currency based upon **CODE** or **NAME**
    init?(_ from: String) {
        for i in CurrencyType.allCases {
            let (code,name,_) = i.raw
            if code == from || name == from { self = i; return }
        }
        return nil
    }
}

How to save money

With the Money struct, there is no need for Generics. All you have to do is give your CurrencyType and amount. You're money struct is still Equatable, Comparable, and Hashable. You can even round your money to its nearest minor unit.

struct Money: Equatable, Comparable, Hashable {
    var currency: CurrencyType, amount: Double
    init(_ currency: CurrencyType,_ amount: Double) { self.currency = currency; self.amount = amount }
    static func < (lhs: Money, rhs: Money) -> Bool { return lhs.amount < rhs.amount }
    public var rounded: Money {
        let roundTo = pow(10.0, Double(currency.raw.minorUnit))
        let roundedAmount = Double(Int(amount * roundTo)) / roundTo
        return Money(currency, roundedAmount)
    }
}

Try it out!

// Example 1 - Currency Code
let userCurrencyCodeInput = "USD"
let userAmountInput = 39.951

if let findCurrency = CurrencyType("USD") {
    let currency = Money(findCurrency, userAmountInput)
    print(currency.rounded.amount) // prints 39.95
}

// Example 2 - Currency Name
let userCurrencyNameInput = "US Dollar"
if let findCurrency = CurrencyType("US Dollar") {
    let currency = Money(findCurrency, userAmountInput)
    print(currency.rounded.amount) // prints 39.95
}
0-1
  • 702
  • 1
  • 10
  • 30