Vina
Vina

Reputation: 1029

Set's symmetric difference in mutable value creates duplicate

I want the user to rank an array of options into their liking. I have 1 array holding original values for Options, 1 array holding updated values from the original. How can I get the remaining options? I'm on Swift 4.2

My idea is to convert both into Sets and use symmetric difference.

options array provides the original choice
ranked array has value property of user's rank (1,2,3, or 4)
remainingOptions should contains fruits that are not ranked yet

However, I get duplicates.

Feel free to suggest other ways.

struct Option: Hashable {
    var title: String
    var key: String
    var value: Int
}
var apple = Option(title: "Apple", key: "apple", value: 0)
var grape = Option(title: "Grape", key: "grape", value: 0)
var banana = Option(title: "Banana", key: "banana", value: 0)
var papaya = Option(title: "Papaya", key: "papaya", value: 0)

var options = [apple, grape, banana, papaya]

apple.value = 1
grape.value = 2
var ranked = [apple, grape]

let originalSet: Set<Option> = Set(options)
var rankedSet: Set<Option> = Set(ranked)

let remainingOptions = originalSet.symmetricDifference(rankedSet)

Result:

{title "Grape", key "grape", value 1}
{title "Apple", key "apple", value 0}
{title "Grape", key "grape", value 2}
{title "Banana", key "banana", value 0}
{title "Apple", key "apple", value 1}
{title "Papaya", key "papaya", value 0}

Wanted result:

{title "Banana", key "banana", value 0}
{title "Papaya", key "papaya", value 0}

Upvotes: 0

Views: 647

Answers (1)

vacawama
vacawama

Reputation: 154671

The issue is that you are expecting two Options to be equal if they have the same title and key, but by default Swift is also checking the value. Therefore, two Options with different values are considered different.

Symmetric difference returns a new set with the elements that are either in this set or in the given sequence, but not in both. Because you have changed the values, you end up with a union of your two sets because they have nothing in common.

You can fix this by explicitly implementing the hash(into:) function and == functions to ignore the value when checking for equality:

struct Option: Hashable, CustomStringConvertible {
    var title: String
    var key: String
    var value: Int
    var description: String { return "{title: \"\(title)\", key: \"\(key)\", value: \(value)}" }

    func hash(into hasher: inout Hasher) {
        hasher.combine(title)
        hasher.combine(key)
    }

    static func ==(lhs: Option, rhs: Option) -> Bool {
        return lhs.title == rhs.title && lhs.key == rhs.key
    }
}

var apple = Option(title: "Apple", key: "apple", value: 0)
var grape = Option(title: "Grape", key: "grape", value: 0)
var banana = Option(title: "Banana", key: "banana", value: 0)
var papaya = Option(title: "Papaya", key: "papaya", value: 0)

var options = [apple, grape, banana, papaya]

apple.value = 1
grape.value = 2
var ranked = [apple, grape]

let originalSet: Set<Option> = Set(options)
var rankedSet: Set<Option> = Set(ranked)

let remainingOptions = originalSet.symmetricDifference(rankedSet)
print(remainingOptions)
[{title: "Papaya", key: "papaya", value: 0}, {title: "Banana", key: "banana", value: 0}]

Note: symmetricDifference takes a sequence, so it isn't necessary to convert ranked into a Set, you can just use the array:

let remainingOptions = originalSet.symmetricDifference(ranked)

Another Option: Use Filter

Instead of using Sets and symmetricDifference, you can use map to get an array of keys from the ranked array, and then use filter on the options array to get the remaining options that don't match those keys:

let rankedKeys = ranked.map { $0.key }
let remaining = options.filter { !rankedKeys.contains($0.key) }

This doesn't require you to change the Option struct from your original definition.

Upvotes: 1

Related Questions