Search code examples
swiftnumbersformatter

NumberFormatter return the wrong number


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

Solution

  • 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