Skip to content

💰 A precise, flexible representation of monetary values in any given currency.

License

Notifications You must be signed in to change notification settings

connor-ricks/swift-money

Repository files navigation

💰 Money

Build Codecov License

A precise, flexible representation of monetary values in any given currency.

Requirements

Platform Version
iOS v13
macOS v10_15
macCatalyst v13
tvOS v13
watchOS v6
visionOS v1

Installation

// Package Dependency
.package(url: "https://github.com/connor-ricks/swift-money", from: "0.1.0")

// Target Dependency
.product(name: "Money", package: "swift-money")

About

Rather than using a concrete generic type to enforce type safety and help prevent user error when dealing with monetary values across different currencies, Money introduces various concepts and guard rails to prevent abuse, while still allowing for the flexibility of dealing with dynamic values of various currencies.

A common way of defining a monetary value would be something similar to Money<USD>. At the surface level this seems like a great solution. It allows you to utilize the compiler prevent abuse. A perfect example would be attempting to add Money<USD> and Money<GBP>. You cannot simply add a monetary value of two currencies, you must convert one of the currenices to the currency of the other value before adding their amounts together. Using generics, you are able to restrict these operations and prevent yourself from making mistakes.

This strategy works great when you are dealing with one currency throughout your entire application, but it falls apart quickly when you introduce support for multiple currencies.

Take the following example...

Say you are writing an app that keeps track of expenses for employees, and you need to display a list of receipts, as well as their total value. Your API may return an array of monetary values of various currencies. (Imagine an employee on a business trip abroad)

{
  "receipts": [
    {
      "name": "Airport Dinner",
      "amount": "38.50",
      "currency": "USD"
    }, {
      "name": "Hotel",
      "amount": "200",
      "currency": "GBP"
    }, {
      "name": "Public Transportation",
      "amount": "5",
      "currency": "GBP"
    }
  ]
}

Given that the list of receipts contains different currencies, decoding the receipts into a generic typed Money<USD> would not be feasible. The app would either have to convert all money to USD using a custom decoder, or know exactly what type of money the API will send ahead of time.

Even if all the receipts for a given request used the same currency, that would mean that you would have to define your receipt using generics too, as other request could return recipets with different currencies.

struct Receipt<C: Currency> {
    let amount: Money<C>
}

Sadly, this doesn't scale well, as anything that interacts with a Receipt will also need to be generic over a monetary value, bubbling up entire your entire code base is generic of your currency type.

Usage

Creating Money

The Money type contains a currency, and an amount, representing the amount of the given currency that the monetary value contains. Predefined currencies are named using their ISO 4217 currency code.

let dollars = Money(amount: 10, currency: .USD) // $10

Some currencies specify a minor unit, for example, USD amounts utilize cents, with each cent being worth 1/100 of a dollar. You can initialize monetary values from a quantity of minor units. For currencies that have no minor units, like JPY, the value would be the same as if the standard amount initializer would have been used.

let dollars = Money(minorUnits: 50, currency: .USD) // $0.50

let foo = Money(amount: 2000, currency: .JPY) // ¥2000
let bar = Money(minorUnits: 2000, currency: .JPY) // ¥2000

Arithmetic

Performing simple arithmetic operations on Money is as easy as using operators like +, +=, -, -=, *, *=, /, /= with either Int or Decimal values.

var dollars = Money(amount: 10, currency: .USD)
dollars = dollars + 3 // Money(amount:  13, currency: .USD)
dollars += 2          // Money(amount:  15, currency: .USD)

dollars = dollars - 5 // Money(amount:  10, currency: .USD)
dollars -= -10        // Money(amount:  20, currency: .USD)

dollars = dollars * 2 // Money(amount:  40, currency: .USD)
dollars *= -1         // Money(amount: -40, currency: .USD)

dollars = dollars / 2 // Money(amount: -20, currency: .USD)
dollars /= -1         // Money(amount:  20, currency: .USD)

dollars = -dollars    // Money(amount: -20, currency: .USD)

Performing arithmetic operations on two Money objects is a little more nuanced than simply operating on their amounts. Two monetary values could have different currencies, which means they would need to be converted to the same currency before performing operations on their amounts. Rather than requiring that all operations between monetary values provide an exchange rate, we introduce the concept of AggregatedMoney

AggregatedMoney is backed by a collection of monetary values, effectively delaying the need to convert currencies until you need to.

let dollars =  Money(amount: 10, currency: .USD)
let sterling = Money(amount:  5, currency: .GBP)

var aggregatedMoney = dollars + sterling             // AggregatedMoney([.USD: 10, .GBP:  5])
aggregatedMoney += Money(amount: 20, currency: .GBP) // AggregatedMoney([.USD: 10, .GBP: 25])
aggregatedMoney -= Money(amount: 10, currency: .JPY) // AggregatedMoney([.USD: 10, .GBP: 25, .JPY: -10])

aggregatedMoney *=  5 // AggregatedMoney([.USD: 50, .GBP: 125, .JPY: -50])
aggregatedMoney /= 25 // AggregatedMoney([.USD:  2, .GBP:   5, .JPY:  -2])

let otherAggregatedMoney = AggregatedMoney([Money(amount: 5, currency: .GBP), Money(amount: 10, currency: .EUR)])

aggregatedMoney += otherAggregatedMoney // AggregatedMoney([.USD: 2, .GBP: 10, .JPY: -2, .EUR: 10])

aggregatedMoney = -aggregatedMoney // AggregatedMoney([.USD: -2, .GBP: -10, .JPY: 2, .EUR: -10])

Conversion

In order to convert one monetary value to another, we make use either an Exchange or an AsyncExchange

Both exchanges are protocols that require the implementation of a rate function that provides an exchange rate for a given base currency and quote currency. The rate function is a throwing function as your exchange may perform some failable work, such as a network request, or it may not support converting between all possible currencies.

How you choose to implement an exchange is up to you. You could choose an Exchange with a set number of exchange rates at initialization, or you could utilize AsyncExchange to fetch the latest exchange rate for a given currency pair upon request. This framework does not provide any concrete exchange implementations as the requirements for the consumer depend on their specific use case.

Once you have an exchange, utilizing it for Money is very simple.

let exchange = ForexExchange(...) // GBP/USD = 1.25
let dollars = Money(amount: 10, currency: .USD) // $10
let sterling = try await exchange.trade(dollars, for: .GBP) // £8

For AggregatedMoney, you are exchanging each currency in the collection to the desired currency and summing their values.

let exchange = ForexExchange(...) // GBP/USD = 1.25 GBP/EUR 1.15
let dollars = Money(amount: 10, currency: .USD) // $10
let euros = Money(amount: 100, currency: .EUR) // €23
let aggregatedMoney = AggregatedMoney(money: dollars, euros)
let sterling = try await exchange.trade(aggregatedMoney, for: .GBP) // £28

Alternatively, for quick simple exchanges, Money exposes a convert and converted function that simply takes a rate and performs the conversion directly.

let dollars = Money(amount: 10, currency: .USD) // $10
let sterling = dollars.converted(to: .GBP, rate: 1.25) // $8

Rounding

Money can be rounded using its currency to determine the level of prevision.

var dollars = Money(amount: 5.5555, currency: .USD)
dollars.round() // $5.56

AggregatedMoney Subscripting

Sometimes, you may want to interact directly with the amount of a specific monetary value within an AggregatedMoney. To do so, you can utilize subscripting to get and set amounts for a given currency.

var aggregatedMoney = AggregatedMoney(money: Money(amount: 10, currency: .USD), Money(amount: 15, currency: .GBP))
let dollarsAmount = aggregatedMoney[.USD] // 10
let yenAmount = aggregatedMoney[.JPY] // nil

aggregatedMoney[.USD] = 15 // AggregatedMoney(money: Money(amount: 15, currency: .USD), Money(amount: 15, currency: .GBP))

Supported Currencies

Out of the box, this framework has predefined currencies for most ISO 4126 currencies. You can reference them using their alphabetic code as static properties on Currency

let dollars = Money(amount: 10, currency: .USD)
let sterling = Money(amount: 10, currency: .GBP)

In addition, you can attempt to initialize an ISO4127Currency using either an alphabetic code or numeric code.

let dollar = ISO4127Currency(alphabeticCode: "USD") // Optional<ISO4127Currency>(.GBP)
let sterling = ISO4127Currency(numericCode: "826")  // Optional<ISO4127Currency>(.GBP)

These initializers will only work for currencies defined within the framework. If you extend ISO4127Currency beyond the predefined currencies, they will not successfully be decoded by the initializers.

Custom Currencies

While this framework comes prepopulated with a fair number of currencies, there are times in which you many need to add your own, either new ISO 4127 currencies or perhaps even your own custom currencies.

Creating a new ISO4127Currency is as easy as extending the static properties that already exist on Currency.

extension Currency where Self == ISO4127Currency {
    static var XXX: ISO4127Currency { .init(alphabeticCode: "XXX", numericCode: "000", minorUnits: 2, name: "Some Currency") }
}

let money = Money(amount: 10, currency: .XXX)

If you have your own custom currency, for example maybe an in-app currency such as 💎s, you could create your own currency by conforming to Currency.

struct InAppCurrency: Currency {
    let id: String
    let minorUnits: Int = 0
}

extension Currency where Self == InAppCurrency {
    static var diamonds: InAppCurrency { .init(id: "💎", minorUnits: 0) }
    static var hearts: InAppCurrency { .init(id: "❤️", minorUnits: 1) }
}

About

💰 A precise, flexible representation of monetary values in any given currency.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

Languages