Parsing Decimal values from JSON

I think that the real problem here is that the Decimal initializer that takes a Double does not do a very good job of converting the Double to a Decimal. This does not only affect JSON decoding, but also simple statements such as:

let myDecimal: Decimal = 3.133

I would expect that to give me the exact Decimal of 3.133, but since this will pass through a Double (through the ExpressibleByFloatLiteral protocol), I will get an inexact value of 3.132999999999999488.

The problem of converting a binary floating point number to a decimal floating point representation might not be easy, but it seems to me that there are better ways of doing it, and that such an algorithm can be found when we convert Double to String.

Consider these two functions that create a Decimal from a Double.

func decimalThroughString(from double: Double) -> Decimal {
    return Decimal(string: "\(double)")!
}

func decimalDirectly(from double: Double) -> Decimal {
    return Decimal(double)
}

For all numbers I've been able to find – I did a little round trip test with random decimal numbers – the decimalThroughString method works perfectly while the decimalDirectly often gives unwanted results. I'm wondering if there's any situation I'm not seeing where the behavior of the Decimal(_: Double) initializer is preferrable.


Here's my random test:

func randomDecimal() -> Decimal {
    let double = Double(arc4random()) / Double(UInt32.max) * 100
    let formatter = NumberFormatter()
    formatter.decimalSeparator = "."
    formatter.maximumFractionDigits = Int(arc4random() % 20)
    let string = formatter.string(from: double as NSNumber)!
    return Decimal(string: string)!
}

var totalCorrectDirectly = 0, totalCorrectThroughString = 0
for _ in 1...1000 {
    let decimal = randomDecimal()
    let double = (decimal as NSNumber).doubleValue
    let correctDirectly = decimalDirectly(from: double) == decimal
    let correctThroughString = decimalThroughString(from: double) == decimal
    totalCorrectDirectly += correctDirectly ? 1 : 0
    totalCorrectThroughString += correctThroughString ? 1 : 0
}
totalCorrectDirectly // ~378
totalCorrectThroughString // 1000

(I also wrote a blog post about this problem.)

2 Likes