A precise, type-safe representation of a monetary amount in a given currency

Overview

Money

Build Status License Swift Version Cocoapods platforms Cocoapods compatible Carthage compatible

A precise, type-safe representation of monetary amounts in a given currency.

This functionality is discussed in Chapter 3 of Flight School Guide to Swift Numbers.

Requirements

  • Swift 4.0+

Installation

Swift Package Manager

Add the Money package to your target dependencies in Package.swift:

import PackageDescription

let package = Package(
  name: "YourProject",
  dependencies: [
    .package(
        url: "https://github.com/Flight-School/Money",
        from: "1.3.0"
    ),
  ]
)

Then run the swift build command to build your project.

CocoaPods

You can install Money via CocoaPods, by adding the following line to your Podfile:

pod 'Money-FlightSchool', '~> 1.3.0'

Run the pod install command to download the library and integrate it into your Xcode project.

Note The module name for this library is "Money" --- that is, to use it, you add import Money to the top of your Swift code just as you would by any other installation method. The pod is called "Money-FlightSchool" because there's an existing pod with the name "Money".

Carthage

To use Money in your Xcode project using Carthage, specify it in Cartfile:

github "Flight-School/Money" ~> 1.3.0

Then run the carthage update command to build the framework, and drag the built Money.framework into your Xcode project.

Usage

Creating Monetary Amounts

The Money type has a required associated Currency type. These currency types are named according to their three letter ISO 4701 currency codes. You can initialize a monetary using a Decimal value:

let amount = Decimal(12)
let monetaryAmount = Money<USD>(amount)

Some currencies specify a minor unit. For example, USD amounts are often expressed in cents, each worth 1/100 of a dollar. You can initialize monetary amounts from a quantity of minor units. For currencies that don't have a minor unit, such as JPY, this is equivalent to the standard initializer.

let twoCents = Money<USD>(minorUnits: 2)
twoCents.amount // 0.02

let ichimonEn = Money<JPY>(minorUnits: 10_000)
ichimonEn.amount // 10000

You can also create monetary amounts using integer, floating-point, and string literals.

12 as Money<USD>
12.00 as Money<USD>
"12.00" as Money<USD>

Important: Swift floating-point literals are currently initialized using binary floating-point number type, which cannot precisely express certain values. As a workaround, monetary amounts initialized from a floating-point literal are rounded to the number of places of the minor currency unit. If you want to express a smaller fractional monetary amount, initialize from a string literal or Decimal value instead.

let preciseAmount: Money<USD> = "123.4567"
let roundedAmount: Money<USD> = 123.4567

preciseAmount.amount // 123.4567
roundedAmount.amount // 123.46

For more information, see https://bugs.swift.org/browse/SR-920.

Comparing Monetary Amounts

You can compare two monetary amounts with the same currency:

let amountInWallet: Money<USD> = 60.00
let price: Money<USD> = 19.99

amountInWallet >= price // true

Attempting to compare monetary amounts with different currencies results in a compiler error:

let dollarAmount: Money<USD> = 123.45
let euroAmount: Money<EUR> = 4567.89

dollarAmount == euroAmount // Error: Binary operator '==' cannot be applied

Adding, Subtracting, and Multiplying Monetary Amounts

Monetary amounts can be added, subtracted, and multiplied using the standard binary arithmetic operators (+, -, *):

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"

Important: Multiplying a monetary amount by a floating-point number results in an amount rounded to the number of places of the minor currency unit. If you want to produce a smaller fractional monetary amount, multiply by a Decimal value instead.

Formatting Monetary Amounts

You can create a localized representation of a monetary amount using NumberFormatter. Set the currencyCode property of the formatter to the currency.code property of the Money value and pass the amount property to the formatter string(for:) method.

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"

Encoding and Decoding Monetary Amounts

Encoding

By default, Money values are encoded as keyed containers, with amount encoded as a number value.

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"}"#

To configure encoding behavior, set either the JSONEncoder.moneyEncodingOptions property or the CodingUserInfoKey.moneyEncodingOptions key in the encoder's userInfo property.

var encoder = JSONEncoder()
encoder.moneyEncodingOptions = [.omitCurrency, .encodeAmountAsString]

let data = try encoder.encode([value])
String(data: data, encoding: .utf8) // #"["123.45"]"#

Decoding

The default decoding behavior is flexible, supporting both keyed and single value containers, with string or number values for 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"

To configure decoding behavior, set either the JSONDecoder.moneyDecodingOptions property or the CodingUserInfoKey.moneyDecodingOptions key in the decoder's userInfo property.

var decoder = JSONDecoder()
decoder.moneyDecodingOptions = [.requireExplicitCurrency]

Important: Foundation decoders currently decode number values using a binary floating-point number type, which cannot precisely express certain values. As a workaround, you can specify the requireStringAmount decoding option to require monetary amounts to be decoded precisely from a string representation.

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

Alternatively, you can the roundFloatingPointAmount decoding option to round decoded floating-point values to the number of places of the minor currency unit.

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

For more information, see https://bugs.swift.org/browse/SR-7054.

Supporting Multiple Currencies

Consider a Product structure with a price property. If you only support a single currency, such as US Dollars, you would define price to be of type Money<USD>:

struct Product {
    var price: Money<USD>
}

If you want to support multiple currencies, however, you can't specify an explicit currency type in the property declaration. Instead, the Product would have to be defined as a generic type:

struct Product<Currency: CurrencyType> {
    var price: Money<Currency>
}

Unfortunately, this approach is unwieldy, as each type that interacts with Product would also need to be generic, and so on, until the entire code base is generic over the currency type.

class ViewController<Currency: CurrencyType> : UIViewController { ... } // 😭

A better solution would be to define a new Price protocol with requirements that match the Money type:

protocol Price {
    var amount: Decimal { get }
    var currency: CurrencyType.Type { get }
}

extension Money: Price {}

Doing this allows prices to be defined in multiple currencies without making Product generic over the currency type:

struct Product {
    var price: Price
}

let product = Product(price: 12.00 as Money<USD>)
product.price // "$12.00"

If you want to support only certain currencies, such as US Dollars and Euros, you can define a SupportedCurrency protocol and add conformance to each currency type through an extension:

protocol SupportedCurrency: CurrencyType {}
extension USD: SupportedCurrency {}
extension EUR: SupportedCurrency {}

extension Money: Price where Currency: SupportedCurrency {}

Now, attempting to create a Product with a price in an unsupported currency results in a compiler error:

Product(price: 100.00 as Money<EUR>)
Product(price: 100.00 as Money<GBP>) // Error

Supported Currencies

This package provides a Currency type for each of the currencies defined by the ISO 4217 standard with the exception of special codes, such as USN (US Dollar, Next day) and XBC (Bond Markets Unit European Unit of Account 9).

The source file defining the available currencies is generated from a CSV file using GYB. This data source is up-to-date with ISO 4217 Amendment Number 169, published on August 17, 2018.

You can regenerate Sources/Money/Currency.swift from Resources/iso4217.csv by installing GYB and running the make command from the terminal:

$ make

We don't currently have a mechanism to automatically update this data source. Please open an issue if you're aware of any new amendments made to ISO 4217.

You can lookup any built-in currency types by its three-letter code using the iso4217Currency(for:) function.

iso4217Currency(for: "USD")?.name // "US Dollar"
iso4217Currency(for: "invalid") // nil

Adding Custom Currencies

You can create your own custom currency types by defining an enumeration that conforms to the CurrencyType protocol. For example, here's how you might represent Bitcoin (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 only supports currencies defined by ISO 4217, so you'll have to configure the symbol, currency code, and any other necessary parameters:

let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.currencySymbol = "â‚¿"
formatter.currencyCode = "BTC"
formatter.maximumFractionDigits = 8

formatter.string(for: satoshi.amount) // â‚¿0.00000001

Important: The iso4217Currency(for:) returns only built-in currencies, so calling iso4217Currency(for: "BTC")would returnnil`.

Showing Off with Emoji

If you're the type of person who enjoys putting clip art in your source code, here's a trick that'll really impress your teammates:

typealias 💵 = Money<USD>
typealias 💴 = Money<JPY>
typealias 💶 = Money<EUR>
typealias 💷 = Money<GBP>

let tubeFare: 💷 = 2.40 // "£2.40"

Alternatives to Consider

A type-safe Money structure like the one provided by this package can reduce the likelihood of certain kinds of programming errors. However, you may find the cost of using this abstraction to outweigh the benefits it can provide in your code base.

If that's the case, you might consider implementing your own simple Money type with a nested Currency enumeration like this:

struct Money {
   enum Currency: String {
      case USD, EUR, GBP, CNY // supported currencies here
   }

   var amount: Decimal
   var currency: Currency
}

It's ultimately up to you to decide what kind of abstraction is best for your particular use case. Whatever you choose, just make sure to represent monetary amounts using a Decimal type with an explicit currency.

License

MIT

Contact

Mattt (@mattt)

Comments
  • Potential rounding issues decoding from json

    Potential rounding issues decoding from json

    This is a fantastic piece of work by the way @mattt and has saved us from having to roll our own solution.

    I did discover what could be a potential rounding issue when decoding Decimal's from json. I'm seeing the following test fail

    Screen Shot 2020-04-30 at 3 47 53 PM

    Our solution to this was to perform the same rounding correction you already perform elsewhere in the code and add it to the decode function.

    E.g.

    self.amount = Money<Currency>(amount).rounded.amount
    

    So the decode method becomes

    Screen Shot 2020-04-30 at 3 59 13 PM

    Just thought I'd mention this incase I'm doing something dumb or it isn't dumb and could be useful to someone else.

    I have one other question. We'd like to override the behaviour of the encode and decode functions to support decoding from a string and encoding to a single value and not the amount + currency (currently we're only using a single currency, but that could change down the line)

    How can I extend or override these methods without forking the repo or plain copying your class? It seems as though you can't inherit from a generic class and override in the usual way.

    Anyway thanks again for making this :+1:

    bug 
    opened by craigrushforth 10
  • Instantiate based on currencyCode

    Instantiate based on currencyCode

    Hi,

    I'm not sure if this is the best place to ask this question so bear with me in case this is more of a feature request or just not understanding the library clearly.

    Some Background

    I am building an app and all currency values are stored and passed in API's using Minor Values (e.g. cents or pence as needed by the currency). I'm using similar libraries for JavaScript and the Backend to be able to manage this.

    I was looking for a similar library to understand and use this on Swift.

    Usage

    I have two steps in what I'm trying to do.

    (1) Creating a Money value based on a currencyCode which is passed as a string.

    I sense this is something I'm not fully understanding in Swift, but how do I instantiate the Money with the "USD" actually being a string?

    (2) Creating the value from a Minor Value

    I considered creating this as an extension of your class, which would need to do the following:

    • Understanding which currency is being converted using the currencyCode, then finding the CurrencyType enum which matches this currencyCode
    • Using the CurrencyType. minorUnit to understand how many decimal places there are for this currency and split the integer into a decimal with the correct values (maybe via a string concatenation). However, in this case the factory will have to know if it's returning a USD or GBP ... in the return type which causes a compilation error.

    Again, apologies if there is something I'm not understanding, but any help or guidance would be much appreciated.

    Thanks

    Moe

    opened by moetanahy 7
  • use currency from locale

    use currency from locale

    Hi,

    like the effort here, thx!

    One issue that comes up immediately is localization. Is there an easy way to use the currency defined from the current locale?

    opened by deeje 7
  • Make it easier to specify custom coding keys

    Make it easier to specify custom coding keys

    Resolves #18

    By default, Money values are encoded and decoded with the string keys "amount" and "currencyCode", which correspond to their respective properties.

    This PR makes the private Money.CodingKeys enumeration a public type, MoneyCodingKeys, which allows consumers to customize the key names for decoding Money values like this:

    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
    
    opened by mattt 4
  • Update money negation docstring

    Update money negation docstring

    It looks like this was copied directly from

        /// The difference between two monetary amounts.
        public static func - (lhs: Money<Currency>, rhs: Money<Currency>) -> Money<Currency> {
            return Money<Currency>(lhs.amount - rhs.amount)
        }
    

    I think this updated phrasing reflects the behavior correctly, but I'm only using the library as a reference so I might be misunderstanding this.

    opened by sberrevoets 3
  • Fix crash and improve empty string case

    Fix crash and improve empty string case

    • When string is empty or not a number. For example Money<USD>("") and Money<USD>("a") will crash. This PR fix this issue. By default, it will return 0
    • When string is empty the decode will fail. In this empty string case, it will return Money<USD>("0") by default.
    opened by remlostime 2
  • CocoaPods Support

    CocoaPods Support

    It would be nice to use this from CocoaPods! Here’s a rough draft of a Podspec that seems to work, albeit with a warning that the summary and description are identical:

    Pod::Spec.new do |s|
      
      s.name         = "Money"
      s.version      = "1.0.0"
      s.summary      = "A precise, type-safe representation of a monetary amount in a given currency."
    
      s.description  = <<-DESC
    A precise, type-safe representation of a monetary amount in a given currency.
                       DESC
    
      s.homepage     = "https://gumroad.com/l/swift-numbers"
      s.license      = { :type => "MIT", :file => "LICENSE.md" }
    
    
      s.author             = "Mattt"
      s.social_media_url   = "http://twitter.com/mattt"
    
      s.swift_version = '4.0'
      s.ios.deployment_target = "8.0"
      s.osx.deployment_target = "10.10"
      s.watchos.deployment_target = "2.0"
      s.tvos.deployment_target = "9.0"
    
      s.source       = { :git => "https://github.com/Flight-School/Money.git", :tag => "#{s.version}" }
    
      s.source_files  = "Sources/Money/*.swift"
    
    end
    
    opened by SlaunchaMan 2
  • Change

    Change "currencyCode" JSON coding key to "currency"

    Please make the "currencyCode" JSON key configurable to "currency", or please change this key to "currency" for better compatibility between libraries.

    The JSON serialization/deserialization of a Money object does not seem to be configurable to another key besides "currencyCode". Whereas, "currency" seems to be the preferred default JSON key for money libraries in other languages (i.e. jackson-datatype-money & dinero). Also, not every library can be configured to use "currencyCode" as the JSON key, and "currency" is a shorter key that offers minor savings in bandwidth and storage costs.

    opened by EthanLozano 1
  • CocoaPods operations reset SWIFT_VERSION to 4.2

    CocoaPods operations reset SWIFT_VERSION to 4.2

    I have a project that includes Money version 1.2.0. The project has 4 targets, 1 run and 3 test targets. All 4 targets have set the Swift Language Version to Swift 5.

    I recently started updating CocoaPods library dependencies. I got a build error after updating an unrelated CocoaPods library saying we couldn't add/subtract Money values. Turns out that when I had run pod update <CocoaPods Library Name>, the updated pods project changed the SWIFT_VERSION from 5.0 to 4.2. If I reset the pods project SWIFT_VERSION back to 5.0, the project compiles without errors.

    CocoaPods 1.7.0 adds support for multiple Swift versions in the Podspec. It also allows the Podfile to specify a swift version for a pod, using supports_swift_versions '>= 5.0'. We're currently using CocoaPods 1.9.1, so I added that to our podfile and ran pod update. CocoaPods reported the following error:

    • Money-FlightSchool does not specify a Swift version (4.2) that is satisfied by any of targets (<AppTargetName>) integrating it.

    It doesn't appear there are any code changes needed to run Money on Swift 5. Can you update the podspec to allow running on the latest Swift versions?

    Thank you for considering this!!

    opened by markpokornycos 1
  • Added earlier deployments

    Added earlier deployments

    When installing via Carthage for iOS apps that support iOS versions earlier than the 'Latest', the built framework wouldn't be supported. This PR updates the project settings to add Carthage support for earlier versions of iOS.

    opened by shaps80 1
  • Add top-level iso4217Currency(for:) function

    Add top-level iso4217Currency(for:) function

    As discussed in https://github.com/Flight-School/Money/issues/14#issuecomment-812541152

    You can lookup any built-in currency types by its three-letter code using the iso4217Currency(for:) function.

    iso4217Currency(for: "USD")?.name // "US Dollar"
    iso4217Currency(for: "invalid") // nil
    
    opened by mattt 0
Owner
Flight School
A book series for advanced Swift developers written by @mattt
Flight School
Inspired by Fabric - Answers animation. Allows to "build" given view with pieces. Allows to "destroy" given view into pieces

ADPuzzleAnimation Whats inside Custom animation for UIView inspired by Fabric - Answers animation. Easy to use To create your first animation you need

Anton 126 Dec 25, 2022
Type-safe networking abstraction layer that associates request type with response type.

APIKit APIKit is a type-safe networking abstraction layer that associates request type with response type. // SearchRepositoriesRequest conforms to Re

Yosuke Ishikawa 1.9k Dec 30, 2022
MemoryCache - type-safe, thread-safe memory cache class in Swift

MemoryCache is a memory cache class in swift. The MemoryCache class incorporates LRU policies, which ensure that a cache doesn’t

Yusuke Morishita 74 Nov 24, 2022
Modern thread-safe and type-safe key-value observing for Swift and Objective-C

Now Archived and Forked PMKVObserver will not be maintained in this repository going forward. Please use, create issues on, and make PRs to the fork o

Postmates Inc. 708 Jun 29, 2022
A Protocol-Oriented NotificationCenter which is type safe, thread safe and with memory safety

A Protocol-Oriented NotificationCenter which is type safe, thread safe and with memory safety. Type Safe No more userInfo dictionary and Downcasting,

null 632 Dec 7, 2022
Type-safe CAAnimation wrapper. It makes preventing to set wrong type values.

TheAnimation TheAnimation is Type-safe CAAnimation wrapper. Introduction For example, if you want to animate backgroundColor with CABasicAnimation, yo

Taiki Suzuki 222 Dec 6, 2022
Swipe between pages with an interactive title navigation control. Configure horizontal or vertical chains for unlimited pages amount.

SlideController is a simple and flexible UI component fully written in Swift. Built using power of generic types, it is a nice alternative to UIPageVi

Touchlane 409 Dec 6, 2022
A Credit Amount and EMI User Interface build in Swift and SwiftUI

App Usage An iPhone application build in swift . Overview Supported on All iPhone Screen Sizes Dynamic Data following MVVM Design Pattern Used Transit

Mohammad Yasir 4 Apr 20, 2022
Stopwatch is a Swift App that measures amount of time elapsed from a particular time.

Stopwatch Stopwatch is a Swift App that measures amount of time elapsed from a particular time. It highly mocks the stopwatch of Apple's offical App c

Kushal Shingote 3 Feb 20, 2022
Swipe between pages with an interactive title navigation control. Configure horizontal or vertical chains for unlimited pages amount.

SlideController is a simple and flexible UI component fully written in Swift. Built using power of generic types, it is a nice alternative to UIPageVi

Touchlane 411 Jan 5, 2023
A phantom type is a custom type that has one or more unused type parameters.

PhantomTypes A phantom type is a custom type that has one or more unused type parameters. Phantom types allow you to enforce type-safety without sacri

null 3 Nov 4, 2022
:octocat:💧 A slider widget with a popup bubble displaying the precise value selected. Swift UI library made by @Ramotion

FLUID SLIDER A slider widget with a popup bubble displaying the precise value selected written on Swift. We specialize in the designing and coding of

Ramotion 1.9k Dec 23, 2022
A slider widget with a popup bubble displaying the precise value selected written on Swift.

A slider widget with a popup bubble displaying the precise value selected written on Swift. We specialize in the designing and coding of

Ramotion 1.9k Dec 23, 2022
Precise JSON decimal parsing for Swift 🧮

PreciseDecimal Introduction Swift has long suffered a problem with its Decimal type: unapparent loss of precision. This happens with all common ways o

David Roman 22 Dec 15, 2022
Tools for making SwiftUI navigation simpler, more ergonomic and more precise.

SwiftUI Navigation Tools for making SwiftUI navigation simpler, more ergonomic and more precise. Motivation Tools Navigation overloads Navigation view

Point-Free 1.1k Jan 1, 2023
💧 A slider widget with a popup bubble displaying the precise value selected. Swift UI library made by @Ramotion

FLUID SLIDER A slider widget with a popup bubble displaying the precise value selected written on Swift. We specialize in the designing and coding of

Ramotion 1.9k Dec 23, 2022
JSEN (JSON Swift Enum Notation) is a lightweight enum representation of a JSON, written in Swift.

JSEN /ˈdʒeɪsən/ JAY-sən JSEN (JSON Swift Enum Notation) is a lightweight enum representation of a JSON, written in Swift. A JSON, as defined in the EC

Roger Oba 8 Nov 22, 2022
Mobile Text-to-Image search powered by multimodal semantic representation models(e.g., OpenAI's CLIP)

A Mobile Text-to-Image Search Powered by AI A minimal demo demonstrating semantic multimodal text-to-image search using pretrained vision-language mod

null 66 Jan 5, 2023
Eat fit is a component for attractive data representation inspired by Google Fit

EatFit Check this article on our blog. Purpose Eat fit is a component for attractive data representation inspired by Google Fit. It is based on PageVi

Yalantis 657 Jan 5, 2023
Mobile(iOS) Text-to-Image search powered by multimodal semantic representation models(e.g., OpenAI's CLIP)

Mobile Text-to-Image Search(MoTIS) MoTIS is a minimal demo demonstrating semantic multimodal text-to-image search using pretrained vision-language mod

Roy 66 Dec 2, 2022