Dictionary comparison fails, but key/value comparison passes?

UPDATE: with some great suggestions, I finally tracked down the issue - of course the premise was impossible but the cause was very hard to track down.


I have two dictionaries of type [String: [String: Set]];

If I compare them using "a == b" it always returns false. But this succeeds:

var isEqual = a.count == b.count // true
for key in a.keys {
  guard
    let value1 = a[key],
    let value2 = b[key]
  else { fatalError() }
 
  guard value1 == value2 else { isEqual = false; break }
 }
 // isEqual always true

Is this expected? I'm using real data now not contrived test data (which I guess I could try to generate).

David

This will take the else path and set isEqual to false whenever value1 and value2 are equal, no?

That wasn't my real code - tried to make it generic and obviously failed. Fixing code now (that second guard was an if where I printed out the two values if they were different, but then, they never were.

You may need to provide a reduced reproduction for further help. If the keys and values of two dictionaries are equal then the two dictionaries should compare equal.

Without knowing too much about what's going on, this misbehavior usually occurs when a custom type's Equatable and Hashable conformances are not in sync. I.e. something like the following:

struct IntPair {
  let x: Int
  let y: Int
}

extension IntPair: Equatable {
  static func ==(lhs: IntPair, rhs: IntPair) -> Bool {
    lhs.x == rhs.x && lhs.y == rhs.y
  }
}

extension IntPair: Hashable {
  func hash(into hasher: inout Hasher) {
    hasher.combine(x)
  }
}

You said the dictionaries are of type [String: [String: Set]], but what is the element type for this Set I wonder.

2 Likes

No, that’s should not happen.

To find the culprit I’d remove the keys one by one (along with printing out the removed key to the console) until the result of == and your manual eq both give true. The last removed key is the culprit then. You may further verify that fact by starting over and removing all keys but that one and see if you have a disagreement between == and your eq. Then look closely at that key and its value - at that point you’d probably figure out what’s going on and where to go from there.

1 Like

I also wonder if there could be a custom == implementation somewhere which is getting picked up during the per-value comparison but which isn't used for Equatable conformance and so doesn't get called during the aggregate comparison (or vice versa).

There are two types of sets: one is an array of NSStrings from a Parse object mapped into a Swift set, and the other an array of NSStrings from a Core Data ManagedObject converted to a Swift Set. Both are of type Set<String>.

What's maddening is that all the code is in Swift. The keys are obviously the same. When I loop through all values and compare them one by one (without typing them as in the sample code), they compare exactly.

I am working on trying to get an example. My simple example works just fine (I tried reversing object orders in the set, etc - to no avail).

I could actually dump the real data as strings that would compile again, but I'm 100% sure if I do that it will work just fine. In any case I'll do it just to see what happens.

That's an interesting thought - this is a huge app and someone might have overridden equitable on dictionaries. Hmmm.

That said, there are another type of dictionary used in comparisons, [String: Date], that are build from Parse and Core Data, and they are comparing perfectly.

In fact, some of this code was recently converted to Swift. When those failing-to-compare dictionaries were created earlier, one was in Objective-C, the other in Swift, and the Objective-C comparison worked fine:

[a isEqualToDictionary:b]

I wonder if this is this gotcha with Dates (or any similar floating point value):

let a = NSDate(timeIntervalSince1970: .nan)
let b = NSDate(timeIntervalSince1970: .nan)
print(a == b) // true
print(a as Date == b as Date) // false

OK - making some real progress. The issue is the set of IDs I get from Core Data Managed objects is changing to an empty set. There is some very bizarre memory issue going on. All code is running on the mail thread. But the Managed Object Context was saved then reset (to free memory).

The set contains string properties from the objects that are getting faulted. Somehow when that data goes to nil it's zeroing out the set.

I now believe this is a race condition - for some reason iterating through the dictionary is faster and so it succeeds.

I'm going to try a few things to see if I can work around this - will update when I get more.

PS: it probably succeeds in Objective-C since the Swift dictionary is converted to an Objective-C dictionary immediately after its created.

PSS: this is absolutely maddening. The dictionary is a let property. At some point one set in an inner dictionary reverts to an empty dictionary (not nil, it's just empty - its two strings just disappear). It's there for one test, then later on, its empty.

The actual code (object names changed) that produces the disappearing Set:

class func getItemsAndAccessories(KPGOs: String, kACCs: String) -> [String: [String: Set<String>]] {
    let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "MSI")
    let appDelegate = AppDelegate.appDelegate

    do {
        let _results = try appDelegate.managedObjectContext.fetch(request)
        guard
            let array: [MSI] = _results as? [msi]
        else { return [:] }

        var retDict: [String: [String: Set<String>]] = [:]
        array.forEach { item in
            guard case let id = item.serverObjectId, !id.isEmpty else { return }
            
            let pgo: [PPP] = item.pgoArray
            let upc: [UUU] = item.upcArray

            let items: [String] = pgo.compactMap { String($0.serverObjectId) }
            let upCharges: [String] = upc.compactMap { String($0.serverObjectId) }
            retDict[id] = [
                KPGOs: Set<String>(items),
                kACCs: Set<String>(upCharges) // THIS IS THE SET THAT GETS EMPTIED
            ]
        }
        return retDict
    }  catch {
        Log("Query getUpdatedValues Error: \(error)")
        return [:]
    }
}