Swift: Decode imprecise decimal correctly
Asked Answered
S

2

5

I need to decode (Decodable protocol) an imprecise decimal value correctly, from this question I understand how to properly handle the Decimal instantiation, but how can I do this when decoding?

If trying to init any number as a String

if let value = try! container.decode(String.self, forKey: .d) {
    self.taxAmount = Decimal(string: value)
}

I get Fatal Error: "Expected to decode String but found a number instead."

And if try to init 130.43 as a Decimal

if let value = try! container.decode(Decimal.self, forKey: .d) {
    //value.description is 130.43000000000002048
    self.d = Decimal(string: value.description)
    //making subtotal to be also 130.43000000000002048 and not 130.43
}

Is there any way to use either of this constructors when decoding?

  • NSDecimalNumber(string: "1.66")
  • NSDecimalNumber(value: 166).dividing(by: 100)
  • Decimal(166)/Decimal(100)
  • Decimal(sign: .plus, exponent: -2, significand: 166)

Here is a simplified version of the JSON I receive from the external service:

{
   "priceAfterTax": 150.00,
   "priceBeforeTax": 130.43,
   "tax": 15.00,
   "taxAmount": 19.57
}

Note: I can't change what is being received to be decoded, I'm stuck working with decimal numbers.

Sailer answered 12/3, 2019 at 22:10 Comment(9)
This is actually a big problem which is not handled correctly in most languages. The usual solution is to parse Double, store it into Decimal and then round it to a given number of Decimal digits.Reisfield
Sadly you cannot. JSONDecoder uses NSJSONSerialization internally, which decodes to Double. So even if you decode a Decimal, it is first internally decoded to a Double and hence precision is lost. As Sulthan pointed out, there's a workaround, but there's no real solution due to this implementation issue.Entomostracan
You can simply encode and decode it as a String and create a computed property that returns a DecimalWolfgang
Hi @LeoDabus I can't, I always receive a number, if I could edit the data received believe me that would've been my first try.Sailer
You can convert the received data to string using number formatterWolfgang
So you are not encoding the object? If the value you receive is a Double there is nothing you can do other than rounding it yourselfWolfgang
@LeoDabus I'm not encoding the object, I'm receiving it from an external service, I guess I could write some really complicated code to go an edit the json as a string to change the numbers to strings and then decode it, but I'm not sure how would I identify the numbers and enclose them in quotesSailer
So all you want is to set the maximum fraction digits to 2 ?Wolfgang
Hi @LeoDabus, I've added a JSON example. I always receive numbers with scale 2, I need to have the exact value in Swift without loosing precisionSailer
W
10

You can implement your own decoding method, convert your double to string and use it to initialize your decimal properties:


extension LosslessStringConvertible {
    var string: String { .init(self) }
}

extension FloatingPoint where Self: LosslessStringConvertible {
    var decimal: Decimal? { Decimal(string: string) }
}

struct Root: Codable {
    let priceAfterTax, priceBeforeTax, tax, taxAmount: Decimal
}

extension Root {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.priceAfterTax = try container.decode(Double.self, forKey: .priceAfterTax).decimal ?? .zero
        self.priceBeforeTax = try container.decode(Double.self, forKey: .priceBeforeTax).decimal ?? .zero
        self.tax = try container.decode(Double.self, forKey: .tax).decimal ?? .zero
        self.taxAmount = try container.decode(Double.self, forKey: .taxAmount).decimal ?? .zero
    }
}

let data = Data("""
{
"priceAfterTax": 150.00,
"priceBeforeTax": 130.43,
"tax": 15.00,
"taxAmount": 19.57
}
""".utf8)

let decodedObj = try! JSONDecoder().decode(Root.self, from: data)
decodedObj.priceAfterTax   // 150.00
decodedObj.priceBeforeTax  // 130.43
decodedObj.tax             // 15.00
decodedObj.taxAmount       // 19.57
Wolfgang answered 12/3, 2019 at 22:58 Comment(0)
C
-1

Try this:

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let tmpDouble = try container.decode(Double.self, forKey: .yourKey)
    decimalValue = Decimal(string: tmpDouble.description) ?? .zero
}
Counterbalance answered 10/1, 2023 at 12:18 Comment(1)
decoding this way you will get decimal with precision error of decoding DoubleParonomasia

© 2022 - 2024 — McMap. All rights reserved.