Search code examples
iosswiftnskeyedarchivernskeyedunarchiver

NSKeyedArchiver, NSKeyedUnarchiver and TimeInterval rounding more than 7 decimal places


I encountered a problem when working with NSKeyedArchiver, NSKeyedUnarchiver.

I need to archive the dictionary ["updated": time, "isFavorite": true], where time is the time Interval since 1970.

My code looks like this:

import Foundation

extension Data {
    /** Decode data and returns Dictionary<String,Any>, use NSKeyedUnarchiver decoder */
    var decode: [String:Any]?  {
        return NSKeyedUnarchiver.unarchiveObject(with: self) as? [String:Any]
    }
}

extension Dictionary where Key: ExpressibleByStringLiteral, Value: Any {
    /** Encode  Dictionary<String,Any> to the data, use NSKeyedUnarchiver encoder */
    var encode: Data? {
        return NSKeyedArchiver.archivedData(withRootObject: self)
    }
}

/** The current time since 1970 */
var time: Double {
    return Date().timeIntervalSince1970   // example 1491800604.362141
}

//////TEST
let payload: Dictionary<String,Any> = ["updated": time, "isFavorite": true]

print("Data before archiving: \(payload)")

let encodePayload = payload.encode
let decodePayload = encodePayload?.decode

print("Data after unarchive: \(decodePayload!)")

All works well if the time variable contains <= 6 decimal places, but i get >= 7 decimal places digit and is rounded.

Example Correctly

  • Data before archiving: ["updated": 1522537700.689399, "isFavorite": true]
  • Data after unarchive: ["updated": 1522537700.689399, "isFavorite": 1]

Example Not correct

  • Data before archiving: ["updated": 1522536585.2104979, "isFavorite": true]
  • Data after unarchive: ["updated": 1522536585.210498, "isFavorite": 1]

Solution

  • As @rmaddy pointed out, this is a limitation of the precision of the Double type:

    let payload: Dictionary<String,Any> = ["updated": 1522536585.2104979 as Double, "isFavorite": true]
    
    print("Data before archiving: \(payload)")
    
    let encodePayload = payload.encode
    let decodePayload = encodePayload?.decode
    
    print("Data after unarchive: \(decodePayload!)")
    

    outputs:

    Data before archiving: ["updated": 1522536585.2104979, "isFavorite": true]
    Data after unarchive: ["updated": 1522536585.210498, "isFavorite": 1]
    

    However, you can archive things with greater precision using NSDecimalNumber:

    let decimalNumber = NSDecimalNumber(mantissa: 15225365852104979, exponent: -7, isNegative: false)
    
    let payload: Dictionary<String,Any> = ["updated": decimalNumber, "isFavorite": true]
    
    print("Data before archiving: \(payload.description)")
    
    let encodePayload = payload.encode
    let decodePayload = encodePayload?.decode
    
    print("Data after unarchive: \(decodePayload!.description)")
    

    outputs:

    Data before archiving: ["updated": 1522536585.2104979, "isFavorite": true]
    Data after unarchive: ["updated": 1522536585.2104979, "isFavorite": 1]
    

    You can also use the Swift-native Decimal instead of NSDecimalNumber, but its initializer is, for some reason, very poorly documented and much more awkward to use:

    // If you ever end up compiling for a big-endian architecture,
    // the byte ordering here may need to be reversed.
    // Of course it's not possible to test whether that's actually true at present.
    let decimal = Decimal(_exponent: -7, _length: 56, _isNegative: 0, _isCompact: 0, _reserved: 0, _mantissa: (0xa913, 0xbb30, 0x1763, 0x0036, 0, 0, 0, 0))
    
    let payload: Dictionary<String,Any> = ["updated": decimal, "isFavorite": true]
    
    print("Data before archiving: \(payload.description)")
    
    let encodePayload = payload.encode
    let decodePayload = encodePayload?.decode
    
    print("Data after unarchive: \(decodePayload!.description)")
    

    outputs:

    Data before archiving: ["updated": 1522536585.2104979, "isFavorite": true]
    Data after unarchive: ["updated": 1522536585.2104979, "isFavorite": 1]