'How to check if two [String: Any] are identical?

Is there any way to check if two [String: Any] are identical ?

let actual: [[String: Any]] = [
    ["id": 12345, "name": "Rahul Katariya"],
    ["id": 12346, "name": "Aar Kay"]
]
var expected: [[String: Any]]!

if actual == expected {
    print("Equal")
}

Basically i want Dictionary to conform to Equatable protocol in Swift 3.



Solution 1:[1]

Conformance to Equatable aside; for the exercise you could write your own isEqual function to compare two [T: Any] dictionaries for a subset of (Equatable) types that you know the value wrapped by Any is limited to. By attempted conversion to these types (e.g. in a switch statement, as below), you could compare the dictionary's values (for each given key) one by one after their conversion to these given types. E.g.

// Usable if the 'Any' values in your dict only wraps
// a few different types _that are known to you_.
// Return false also in case value cannot be successfully 
// converted to some known type. This might yield a false negative.
extension Dictionary where Value: Any {
    func isEqual(to otherDict: [Key: Any], 
                 allPossibleValueTypesAreKnown: Bool = false) -> Bool {
        guard allPossibleValueTypesAreKnown && 
            self.count == otherDict.count else { return false }
        for (k1,v1) in self {
            guard let v2 = otherDict[k1] else { return false }
            switch (v1, v2) {
                case (let v1 as Double, let v2 as Double) : if !(v1.isEqual(to: v2)) { return false }
                case (let v1 as Int, let v2 as Int) : if !(v1==v2) { return false } 
                case (let v1 as String, let v2 as String): if !(v1==v2) { return false }
                // ... fill in with types that are known to you to be 
                // wrapped by the 'Any' in the dictionaries
                default: return false
            }
        }
    return true
    } 
}

Usage:

/* example setup */
var dict1: [String: Any] = ["id": 12345, "name": "Rahul Katariya", "weight": 70.7]
var dict2: [String: Any] = ["id": 12346, "name": "Aar Kay", "weight": 83.1]

/* example usage */
print(dict1.isEqual(to: dict2, allPossibleValueTypesAreKnown: true))
    // false

dict2["name"] = "Rahul Katariya"
dict2["weight"] = 70.7

print(dict1.isEqual(to: dict2, allPossibleValueTypesAreKnown: true)) 
    // false

dict2["id"] = 12345

print(dict1.isEqual(to: dict2, allPossibleValueTypesAreKnown: true)) 
    // true

class Foo {}
dict1["id"] = Foo()
dict2["id"] = Foo()

print(dict1.isEqual(to: dict2, allPossibleValueTypesAreKnown: true))  
    // false! (we haven't implemented this attempted conversion!)

// incompatable keys cause error as expected an intended    
let dict3: [Int: Any] = [1:2]
dict1.isEqual(to: dict3)
    /* error: cannot convert value of type '[Int : Any]' 
              to expected argument type '[String : Any]' */

Just note the danger that the as conversion may yield a false positive (true) as it can allow mapping from two different types to a common other type, e.g. slicing away derived class differences when casting two derived class instances to their common parent type:

class Base: Equatable {}
func ==(lhs: Base, rhs: Base) -> Bool { return true }

class DerivedA : Base {
    let foo = "foo"
}

class DerivedB : Base {
    let bar = 4.2
}

let a = DerivedA()
let b = DerivedB()

switch (a, b) {
    case (let a as Base, let b as Base): print(a == b) 
    default: ()
} // sliced by conversion! prints "true"

If you'd rather like a failed "known types conversion" to return nil (whereas successful conversions will always yield true/false, based on subsequent equality testing), you could extend the above to (the even messier)

// a 'nil' return here would correspond to an invalid call
extension Dictionary where Value: Any {
    func isEqual(to otherDict: [Key: Any], 
                 allPossibleValueTypesAreKnown: Bool = false) -> Bool? {
        guard allPossibleValueTypesAreKnown else { return nil } 
        guard self.count == otherDict.count else { return false }
        for (k1,v1) in self {
            guard let v2 = otherDict[k1] else { return false }
            switch (v1, v2) {
                case (let v1 as Double, let v2 as Double) : if !(v1.isEqual(to: v2)) { return false }
                case (let v1 as Int, let v2 as Int) : if !(v1==v2) { return false } 
                case (let v1 as String, let v2 as String): if !(v1==v2) { return false }
                // ... 
                case (_ as Double, let v2): if !(v2 is Double) { return false }
                case (_, _ as Double): return false
                case (_ as Int, let v2): if !(v2 is Int) { return false }
                case (_, _ as Int): return false
                case (_ as String, let v2): if !(v2 is String) { return false }
                case (_, _ as String): return false 
                default: return nil
            }
        }
    return true
    } 
}

/* Example as per above will yield (printout):

       Optional(false)
       Optional(false)
       Optional(true)
       nil                           */

Note however that the value by value equality testing above is short-circuited in case of a false hit, which mean that depending on the random order of the non-ordered dictionaries (non-ordered collection), a special case may return nil as well as false, given two non-equal dictionaries. This special case occurs for two dictionary of non-equal values (non-equality for a known type value-value pair) which also hold an value type not included in the attempted casting: if the non-equality of known types is hit first, false will be returned, whereas if a failed conversion is hit first, nil will be returned. Either way, a nil return means the call should be considered invalid, as caller stated that allPossibleValueTypesAreKnown was true (which a failed conversion implies is false).

Solution 2:[2]

The type Any is not Equatable in Swift, so any collection types including Any cannot be Equatable.

You can write something like this in Swift 3/Xcode 8 beta 6:

if actual as NSArray == expected as NSArray {
    print("Equal")
}

But, as importing id as Any is just introduced in beta 6, so this behaviour may change in the near future.

Solution 3:[3]

With Swift 5.5 you can easily cast it to NSDictionary, as it always succeeds:

XCTAssertEqual(actual as NSDictionary, expected as NSDictionary)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 OOPer
Solution 3 toupper