J. Doe
J. Doe

Reputation: 13043

Custom comparator for Swift

This is my code (simplified code):

struct SomeStruct {
    let id: Int
    let age: Int
}

extension SomeStruct: Hashable {
    var hashValue: Int {
        return id.hashValue * age.hashValue
    }

    static func ==(lhs: SomeStruct, rhs: SomeStruct) -> Bool {
        return lhs.id == rhs.id && lhs.age == rhs.age
    }
}

struct Calculator {
    let struct1: [SomeStruct]
    let struct2: [SomeStruct]

    func uniqueById() {
        let struct3 = Set(struct2).union(Set(struct1))

        // I want to union it by property 'id' only.
        // If the property 'id' is equal for both objects,
        // the object in struct2 should be used (since that can have a different age property)
    }
}

SomeStruct is a generated struct which I do not want to edit. I want to create a Set for SomeStruct that is based on 1 property: id. For that, I think I need a custom Comparator, just as Java has. Is there any Swifty way? This is the only thing I can come up with, but I am wondering if there is a better way:

struct SomeStructComparatorById: Hashable {
    let someStruct: SomeStruct

    var hashValue: Int {
        return someStruct.id.hashValue
    }

    static func ==(lhs: SomeStructComparatorById, rhs: SomeStructComparatorById) -> Bool {
        return lhs.someStruct.id == rhs.someStruct.id
    }
}

Upvotes: 1

Views: 2057

Answers (3)

Rob Napier
Rob Napier

Reputation: 299345

First, I don't think this would work in Java. addAll() doesn't take a Comparator (nor does contains, etc.) Comparators are for sorting, not equality. Conceptually this is breaking how Set works in any language. Two items are not "equal" unless they can be swapped in all cases.

That tells us that we don't want a Set here. What you want here is uniqueness based on some key. That's a Dictionary (as Daniel discusses).

You could either just have a "id -> age" dictionary or "id -> struct-of-other-properties" dictionary as your primary data type (rather than using Array). Or you can turn your Array into a temporary Dictionary like this:

extension Dictionary {
    init<S>(_ values: S, uniquelyKeyedBy keyPath: KeyPath<S.Element, Key>)
        where S : Sequence, S.Element == Value {
        let keys = values.map { $0[keyPath: keyPath] }
        self.init(uniqueKeysWithValues: zip(keys, values))
    }
}

And merge them like this:

let dict1 = Dictionary(struct1, uniquelyKeyedBy: \.id)
let dict2 = Dictionary(struct2, uniquelyKeyedBy: \.id)
let merged = dict1.merging(dict2, uniquingKeysWith: { old, new in old }).values

This leaves merged as [SomeStruct].

Note that this Dictionary(uniquelyKeyedBy:) has the same preconditions as Dictionary(uniqueKeysWithValues:). If there are duplicate keys, it's a programming error and will raise precondition failure.

Upvotes: 4

Daniel T.
Daniel T.

Reputation: 33967

The short answer is no. Swift sets do not have any way to accept a custom comparator and if you absolutely must have a Set, then your wrapper idea is the only way to do it. I question the requirement for a set though.

Instead of using Set in your calculator, I recommend using dictionary.

You can use a Dictionary to produce an array where each item has a unique ID...

let struct3 = Dictionary(grouping: struct1 + struct2, by: { $0.id })
        .compactMap { $0.value.max(by: { $0.age < $1.age })}

Or you can keep the elements in a [Int: SomeStruct] dictionary:

let keysAndValues = (struct1 + struct2).map { ($0.id, $0) }
let dictionary = Dictionary(keysAndValues, uniquingKeysWith: { lhs, rhs in 
    lhs.age > rhs.age ? lhs : rhs 
})

Upvotes: 1

ielyamani
ielyamani

Reputation: 18591

You could do something like this:

var setOfIds: Set<Int> = []
var struct3 = struct2.filter { setOfIds.insert($0.id).inserted }
struct3 += struct1.filter { setOfIds.insert($0.id).inserted }

The result would be an array of SomeStruct, with all elements with unique ids.

You could define this as a custom operator :

infix operator *>

func *> (lhs: [SomeStruct], rhs: [SomeStruct]) -> [SomeStruct] {
    var setOfIds: Set<Int> = []
    var union = lhs.filter { setOfIds.insert($0.id).inserted }
    union += rhs.filter { setOfIds.insert($0.id).inserted }
    return union
}

Your code would then look like this:

func uniqueById() {
    let struct3 = struct2 *> struct1
    //use struct3
}

Upvotes: 1

Related Questions