0

I test the NumberFormatter to get the number from priceWithCurrency.

If price bigger than $70 NumberFormatter converted the wrong number.

lazy var currencyFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.currencyCode = "USD"
        return formatter
}()


let price = "$71.9"

currencyFormatter.number(price)
//71.90000000000001
Shawn Baek
  • 1,928
  • 3
  • 20
  • 34

1 Answers1

3

If you do the same thing with 71.8, it will work. This is just because 71.9 can't be represented precisely in floating point numbers ( due to the finitude of bits )

Use integers ( price in cents ) or decimal numbers is the only issue to deal correctly with prices. Check the Decimal and NSDecimalNumber classes to see what you can do with this.

It is specially recommended if you do some computations on prices, ( If you pay 10$ with two friends in cash, two will pay 3.33, and one will pay 3.34 - so depending of your context, divide by 3 might not be enough)

let number = currencyFormatter.number(from: price) ?? NSNumber(value: 0)
var value =  number.decimalValue
var roundedValue: Decimal = 0

// Right way: ( choose your precisions - 3 decimals here):
NSDecimalRound(&roundedValue, &value, 3, .bankers)

print(number)
print(value)
print(roundedValue)
71.90000000000001
71.90000000000001
71.9

If you just need to log your prices, simply use Int(price*100)/100 It will do the job

If you need more... Good luck :)

Edit

Following the excellent remark of @Nicholas Rees, I add this variation:

currencyFormatter.generatesDecimalNumbers = true

let number = (currencyFormatter.number(from: price) as? NSDecimalNumber) ?? NSDecimalNumber(value: 0)
var value =  number.decimalValue
var roundedValue: Decimal = 0

// Right way: ( choose your precisions - 3 decimals here):
NSDecimalRound(&roundedValue, &value, 3, .bankers)

print(number)
print(value)
print(roundedValue)

There, the result in the same when logged, but I suppose the internal format of the 'value' is correct

Another approach is to remove currency and create decimal from string:

print(Decimal(string: "71.9") ?? 0)

71.9
Moose
  • 2,607
  • 24
  • 23
  • 1
    Won't parsing it as a float first sometimes represent it as the wrong number? I would have thought parsing it directly into a Decimal was the only safe way to go. – Nicholas Rees Feb 28 '21 at 15:43
  • You are right. I have added more cases. However, don't forget the real error in double value is around exponent -1000 - if I'm right.  The error when logged is due to rounding before display, but internally, it is insignificant. So computations are right unless you have billions of billions of dollars to count :) - You got a point anyway. Cheers – Moose Mar 01 '21 at 09:13
  • Sorry - precision is 2^-53 for common numbers ( according to https://en.wikipedia.org/wiki/Double-precision_floating-point_format ) – Moose Mar 01 '21 at 11:25
  • Thanks! I'm dumbfounded that Apple provides a currency formatter where the default is currencyFormatter.generatesDecimalNumbers = false. – Nicholas Rees Mar 01 '21 at 14:54