Making NSDecimalNumber Codable

Viewed 5849

Is it possible to extend NSDecimalNumber to conform Encodable & Decodable protocols?

3 Answers

It is not possible to extend NSDecimalNumber to conform to Encodable & Decodable protocols. Jordan Rose explains it in the following swift evolution email thread.

If you need NSDecimalValue type in your API you can build computed property around Decimal.

struct YourType: Codable {
    var decimalNumber: NSDecimalNumber {
        get { return NSDecimalNumber(decimal: decimalValue) }
        set { decimalValue = newValue.decimalValue }
    }
    private var decimalValue: Decimal
}

Btw. If you are using NSNumberFormatter for parsing, beware of a known bug that causes precision loss in some cases.

let f = NumberFormatter()
f.generatesDecimalNumbers = true
f.locale = Locale(identifier: "en_US_POSIX")
let z = f.number(from: "8.3")!
// z.decimalValue._exponent is not -1
// z.decimalValue._mantissa is not (83, 0, 0, 0, 0, 0, 0, 0)

Parse strings this way instead:

NSDecimalNumber(string: "8.3", locale: Locale(identifier: "en_US_POSIX"))

With Swift 5.1 you can use property wrappers to avoid the boilerplate of writing a custom init(from decoder: Decoder) / encode(to encoder: Encoder).

@propertyWrapper
struct NumberString {
    private let value: String
    var wrappedValue: NSDecimalNumber

    init(wrappedValue: NSDecimalNumber) {
        self.wrappedValue = wrappedValue
        value = wrappedValue.stringValue
    }
}

extension NumberString: Decodable {
    init(from decoder: Decoder) throws {
        value = try String(from: decoder)
        wrappedValue = NSDecimalNumber(string: value)
    }
}

extension NumberString: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue.stringValue)
    }
}

extension NumberString: Equatable {}

Usage:

struct Foo: Codable {
    @NumberString var value: NSDecimalNumber
}
Related