43

I have an iOS application that will be performing a lot of basic arithmetic on numbers representing USD currency (eg 25.00 representing $25.00).

I have gotten into a lot of trouble using the datatype Double in other languages like Java and Javascript so I would like to know the best datatype to use for currency in Swift.

PeonProgrammer
  • 1,445
  • 2
  • 15
  • 27
  • 1
    Just curious, what kind of problems did you run into while using double as a datatype for currency ? – userx Feb 09 '15 at 23:04
  • 8
    @AbhishekMukherjee I ran into problems comparing values. When I thought 5.20 == 5.20 it was actually 5.200000001 == 5.20 – PeonProgrammer Feb 10 '15 at 16:03
  • 3
    That's been discussed quite a lot. The correct type to use in Java is BigDecimal. – Rob Dec 03 '15 at 18:11
  • For my project, I was originally storing currency amounts as `Int` (multiplying the dollar amount by 100 to store, and then reversing it and casting as a `Double` when accessing it.) However, after implementing a multi-currency system where currencies have different numbers of decimal places, keeping track of whether it was the stored `Int` or accessed `Double` amount and whether it was in the original currency or base currency, the `Int` approach had too much overhead. Math was easier when using `Double`, but in the end I also recommend `NSDecimalNumber`. – blwinters Apr 27 '16 at 15:48
  • @userx The problem I found using Doubles is that just subtracting leads to REALLY long floating values, I don't know why. For example, something as easy as 650.50 - 300.50 would result in 350.0038420489380933, and I don't know why. – Hedylove Apr 12 '17 at 08:30
  • 1
    @JozemiteApps read http://www.toves.org/books/float/ and you'll learn why. Floating point values are inherently imprecise for many values within their range. – Matt Kantor Sep 28 '17 at 22:53

6 Answers6

41

Use Decimal, and make sure you initialize it properly!

CORRECT


// Initialising a Decimal from a Double:
let monetaryAmountAsDouble = 32.111
let decimal: Decimal = NSNumber(floatLiteral: 32.111).decimalValue
print(decimal) // 32.111  
let result = decimal / 2
print(result) // 16.0555 


// Initialising a Decimal from a String:
let monetaryAmountAsString = "32,111.01"

let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.numberStyle = .decimal

if let number = formatter.number(from: monetaryAmountAsString) {
    let decimal = number.decimalValue
    print(decimal) // 32111.01 
    let result = decimal / 2.1
    print(result) // 15290.9571428571428571428571428571428571 
}

INCORRECT

let monetaryAmountAsDouble = 32.111
let decimal = Decimal(monetaryAmountAsDouble) 
print(decimal) // 32.11099999999999488  

let monetaryAmountAsString = "32,111.01"
if let decimal = Decimal(string: monetaryAmountAsString, locale: Locale(identifier: "en_US")) {
    print(decimal) // 32  
}

Performing arithmetic operations on Doubles or Floats representing currency amounts will produce inaccurate results. This is because the Double and Float types cannot accurately represent most decimal numbers. More information here.

Bottom line: Perform arithmetic operations on currency amounts using Decimals or Int

Eric
  • 16,003
  • 15
  • 87
  • 139
  • Thanks for showing proper way to initialize it. Can you also show proper way to convert ```Decimal``` back to ```String```? I am not sure if other answers on StackOverflow are showing that correctly or not. – zeeshan May 18 '20 at 09:11
  • Would using Decimal(floatLiteral:) be equal to using NSNumber(floatLiteral:).decimalValue? – IloneSP Feb 10 '21 at 10:58
  • @Roberto for whatever reason, Decimal(floatLiteral:) produces an incorrect (32.11099999999999488) result. – Shengchalover Feb 16 '21 at 09:44
  • 1
    Using `NSNumber(floatLiteral: 32.111).decimalValue` is not correct either. The problem si that there is a conversion from a decadic number (floating point literal) to a lossy binary number (`double`) and then to a a decimal number (`Decimal`). If you want precision, initialize always from a String. – Sulthan Feb 21 '22 at 20:38
  • `Decimal(string:locale:)` does not recognize grouping separators. With "32111.01" the result will be correct. – Roman Aliyev Jan 21 '23 at 01:59
32

I suggest you start with a typealias for Decimal. Example:

typealias Dollars = Decimal
let a = Dollars(123456)
let b = Dollars(1000)
let c = a / b
print(c)

Output:

123.456

If you are trying to parse a monetary value from a string, use a NumberFormatter and set its generatesDecimalNumbers property to true.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Does this approach maintain the number of digits after the decimal? Or will it create an approximation of the decimal that could stretch many digits passed the decimal point? – PeonProgrammer Feb 10 '15 at 16:08
  • I like this approach the most because it is feels the easiest to use. – PeonProgrammer Feb 10 '15 at 16:14
  • 1
    Why not just make a Money class? – Rob Dec 03 '15 at 18:56
  • @Rob Why make a class when you can just use `typealias`. – Hedylove Apr 12 '17 at 08:31
  • 4
    This is a *terrible* solution for _currency_, because initialising a `Decimal` with a `Double` can easily lead to rounding errors. For example: `Decimal(2.13) == 2.129999999999999488` – Eric Sep 09 '20 at 15:02
  • 2
    My answer doesn't suggest converting from a `Double` to a `Decimal`. My answer shows how to accurately create a `Decimal` with fractional digits by dividing by a power of 10. – rob mayoff Sep 09 '20 at 15:10
  • nope, this will lose precision because there's no way to represent 0.2 in binary – OMGPOP Feb 21 '22 at 19:40
  • I'm not sure what you're responding to, @OMGPOP, but in case it's me: `Decimal` represents a number as an integer (the ‘significand’ or ‘mantissa’) times a (possibly negative) power of ten (the ‘exponent’). A `Decimal` can represent `0.2` exactly (by storing 2 as the significand and -1 as the exponent), and `Decimal(2) / Decimal(10)` produces such a representation. You can check it for yourself by printing `(Decimal(2) / Decimal(10))._mantissa` and `(Decimal(2) / Decimal(10))._exponent`. – rob mayoff Feb 21 '22 at 19:55
4

There's a really nice lib called Money:

let money: Money = 100
let moreMoney = money + 50 //150

There a lot of nice features besides that, such as type-safe currencies:

let euros: EUR = 100
let dollars: USD = 1500
euros + dollars //Error

Binary operator '+' cannot be applied to operands of type 'EUR' (aka '_Money') and 'USD' (aka '_Money')

fpg1503
  • 7,492
  • 6
  • 29
  • 49
  • 3
    Warning with this class: It does not conform to NSCoding, meaning you will not be able to save values of type Money using NSCoder. Xcode will throw an error. – Hedylove Apr 12 '17 at 08:31
1

The Flight-School/Money is probably the best choice for a project full of monies.

In the ending of README @mattt provides a nice solution for a simple Money type:

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

    var amount: Decimal
    var currency: Currency
}

Chapter 3 in 'Flight School Guide to Swift Numbers' is provides excellent intro into the topic.

Shengchalover
  • 614
  • 5
  • 10
0

We use this approach with Currency being a huge enum that has been generated in advance (don't forget a monetary value is just a number without its currency - they both belong together):

struct Money: Hashable, Equatable {

    let value: Decimal
    let currency: Currency
}

extension Money {

    var string: String {
        CurrencyFormatter.shared.string(from: self)
    }
}
blackjacx
  • 9,011
  • 7
  • 45
  • 56
0

You need the Decimal and its initializer:

Decimal(string: "12345.67890", locale: Locale(identifier: "en-US"))

When considering the other answers, please note that Float and Double literals affect the accuracy of the Decimal result. The same applies to the NumberFormatter.

Decimal(5.01) // 5.009999999999998976 ❌

Decimal(floatLiteral: 5.01) // 5.009999999999998976 ❌

NSNumber(floatLiteral: 0.9999999).decimalValue // 0.9999999000000001 ❌

let decimalFormatter = NumberFormatter()
decimalFormatter.numberStyle = .decimal
decimalFormatter.number(from: "0.9999999")?.decimalValue // 0.9999999000000001 ❌

Decimal(string: "5.01", locale: Locale(identifier: "en-US")) // 5.01 ✅
Decimal(string: "0.9999999", locale: Locale(identifier: "en-US")) // 0.9999999 ✅

Roman Aliyev
  • 224
  • 2
  • 7