14

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

Lorenzo B
  • 33,216
  • 24
  • 116
  • 190
Arda Keskiner
  • 772
  • 7
  • 23
  • What have you tried so far? What are your requirements? What are you trying to encode / decode? – Lorenzo B Sep 19 '17 at 07:45
  • @LorenzoB I checked Apple's documentation https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types for encoding and decoding custom types. I'm trying to parse responses from server which I'd loose precision if I used Double. – Arda Keskiner Sep 19 '17 at 07:54

4 Answers4

13

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"))
Wojciech Nagrodzki
  • 2,788
  • 15
  • 13
3

In swift you should use Decimal type. This type confirms to protocols Encodable & Decodable from the box.

If you have NSDecimalNumber type in your code it's easy to cast it to Decimal

let objcDecimal = NSDecimalNumber(decimal: 10)
let swiftDecimal = (objcDecimal as Decimal)
Nick Rybalko
  • 202
  • 2
  • 5
2

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
}
boa_in_samoa
  • 587
  • 7
  • 16
0

In my case, We are maintaining legacy code which is Objective-C and Swift One of the modules we needed to have a property of type NSNumber (internal API reason) which is not supported by Codable So We use Codable for almost all supported data types and NSCoding with a help of NSKeyedUnarchiver for unsupported types

I am sharing here a sample of the code, as a reference that might help someone who has a such scenario.

class Branch: NSObject, Codable {

    @objc var discountMaxLimit: NSNumber?

    private enum CodingKeys: String, CodingKey {
          case discountInfoKeys
    }

    private enum CorporateDiscountInfoKeys: String, CodingKey {
          case discountMaxLimit = "discount_max_limit"
    }

    func encode(to encoder: Encoder) throws {
         var container = encoder.container(keyedBy: CodingKeys.self)
         var discountInfoK = container.nestedContainer(keyedBy: discountInfoKeys.self, forKey: .discountInfoKeys)
         if let value = discountMaxLimit {
            let data = try NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: false)
            try discountInfoK.encode(data, forKey: .discountMaxLimit)
          }
     }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let discountInfo = try container.nestedContainer(keyedBy: discountInfoKeys.self, forKey: .discountInfoKeys)
        let discountMaxLimitData = try discountInfo.decode(Data.self, forKey: .discountMaxLimit)
        discountMaxLimit = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(discountMaxLimitData) as? NSNumber
    }

}
Amr Angry
  • 3,711
  • 1
  • 45
  • 37