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.)