swiftnscountedset

Subtracting NSCountedSets filled with Swift structs


I have two NSCountedSets and need to subtract them. For the sake of clarity, I am using a simple struct with one property as the objects:

struct MyStruct {
    let name: String
    
    init(_ name: String) {
        self.name = name
    }
}

func subtract(_ set1: NSCountedSet, _ set2: NSCountedSet) -> NSCountedSet {
    var result = NSCountedSet()

    for e in set1 {
        let count1 = set1.count(for: e)
        let count2 = set2.count(for: e)
        let newCount = count1 - count2

        if newCount > 0 {
            for _ in 1 ... newCount {
                result.add(e)
            }
        }
    }

    return result
}

let set1 = NSCountedSet(array: [MyStruct("A"), MyStruct("A"), MyStruct("A"), MyStruct("B"), MyStruct("C"), MyStruct("C")])
let set2 = NSCountedSet(array: [MyStruct("A"), MyStruct("B"), MyStruct("C")])

let set3 = subtract(set1, set2) // expected result: ["A", "A", "C"]`

print(set3.count(for: MyStruct("A"))) // observed result: 0 (expected 2)
print(set3.count(for: MyStruct("B"))) // observed result: 0 (expected 0)
print(set3.count(for: MyStruct("C"))) // observed result: 0 (expected 1)

But if I for instance use:

let set1 = NSCountedSet(array: ["A", "A", "A", "B", "C", "C"])
let set2 = NSCountedSet(array: ["A", "B", "C"])

I get the expected result.

I suspect that each MyStruct("A") etc is actually not equal to the other ones since they are recreated. Which is what I see when I add a print statement, count1 is always 1 and count2 is always 0

So maybe there is a way that I can count the number of MyStruct by checking the name property, is that possible?

If not, is there maybe any other solution?


Solution

  • You need to make your type conform to Hashable to make NSCountedSet compare its elements based on their properties.

    struct MyStruct: Hashable {
      let name: String
    
      init(_ name: String) {
        self.name = name
      }
    }
    

    There's also no need to define a subtract method, NSCountedSet already inherits minus from its NSMutableSet superclass

    set1.minus(Set(_immutableCocoaSet: set2))
    

    Be aware that minus mutates the set it's called on in place, rather than returning a new instance.

    If you want to declare a non-mutating variant of minus without having to reimplement its logic, you can simply do that by copying the set, the calling minus on the copy.

    extension NSMutableSet {
      func subtracting(_ other: NSMutableSet) -> NSMutableSet {
        let copy = self.mutableCopy() as! NSMutableSet
        copy.minus(.init(_immutableCocoaSet: other))
        return copy
      }
    }
    
    let subtracted = set1.subtracting(set2)