Swift iOS -Compare elements in 1 array by property

Refresh

January 2019

Views

223 time

1

I have an array of objects and I want to compare the objects based on property to find out if the properties are all the same. Right now I loop through all the objects, place all values of the properties in a separate array, and then use filterArr.allSatisfy { $0 == filterArr.last } to detemermine wether the properties are all the same or not.

This method works but I know there has to be a more elegant way then what I'm doing.

I actually went looking for an answer to this but every single thing I came across was comparing the elements of 2 different arrays instead of 1.

class IceCream {
    var flavor: String?
    var price: Double?
}

let iceCream1 = IceCream()
iceCream1.flavor = "vanilla"
iceCream1.price = 1.5

let iceCream2 = IceCream()
iceCream2.flavor = "chocolate"
iceCream2.price = 2.0

let iceCream3 = IceCream()
iceCream3.flavor = "vanilla"
iceCream3.price = 1.5

let iceCream4 = IceCream()
iceCream4.flavor = "strawberry"
iceCream4.price = 2.5

let iceCreams = [iceCream1, iceCream2, iceCream3, iceCream4]

var filterArr = [String]()

for iceCream in iceCreams {
    filterArr.append(iceCream.flavor ?? "")
}

let areItemsEqual = filterArr.allSatisfy { $0 == filterArr.last }

print(areItemsEqual) // prints false

3 answers

1

You can avoid having to initialize and then assign the properties on separate lines with a struct.

struct IceCream {
    let flavor: String?
    let price: Double?
}

let iceCreams: [IceCream] = [
    IceCream(flavor: "vanilla", price: 1.5),
    IceCream(flavor: "chocolate", price: 2.0),
    IceCream(flavor: "vanilla", price: 1.5),
    IceCream(flavor: "strawberry", price: 2.5)
]

Using the generics sugar provided by @Alexander and @matt, we have a nice looking extension.

extension Collection {
    func allEqual<T: Equatable>(by key: KeyPath<Element, T>) -> Bool {
        return allSatisfy { first?[keyPath:key] == $0[keyPath:key] }
    }
}

print(iceCreams.allEqual(by: \.flavor))

Alternatively, you could specify an IceCream be equal to one another by flavor.

extension IceCream: Equatable {
    static func == (lhs: IceCream, rhs: IceCream) -> Bool {
        return lhs.flavor == rhs.flavor
    }
}

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        return allSatisfy { first == $0 }
    }
}

print(iceCreams.allEqual())
1

Here's a pretty Swifty way to do it. I define an extension on Collection that checks for equality among the collection's items, according to a given predicate:

extension Collection {
    func allEqual<T: Equatable>(by deriveKey: (Element) -> T) -> Bool {
        guard let firstElement = self.first else {
            return true // empty lists are all-equal
        }
        let sampleKey = deriveKey(firstElement)
        return self.dropFirst().allSatisfy{ deriveKey($0) == sampleKey }
    }
}

struct IceCream {
    let flavor: String
    let price: Double
}

let iceCreams = [
    IceCream(flavor:    "vanilla", price: 1.5),
    IceCream(flavor:  "chocolate", price: 2.0),
    IceCream(flavor:    "vanilla", price: 1.5),
    IceCream(flavor: "strawberry", price: 2.5)
]

let allItemsEqualByFlavour = iceCreams.allEqual(by: { $0.flavor})
print(allItemsEqualByFlavour) // false

let vanillaOnlyIceCreams = iceCreams.filter{ $0.flavor == "vanilla" }
print(vanillaOnlyIceCreams.allEqual(by: { $0.flavor})) // true
1

Here's an elegant way to make sure your ice creams are the same along any arbitrary axis, i.e. either flavor or price or any other equatable property you may later inject:

extension Array {
    func allSameForProperty<T:Equatable> (_ p:KeyPath<Element,T>) -> Bool {
        return self.isEmpty || self.allSatisfy{
            self.first![keyPath:p] == $0[keyPath:p]
        }
    }
}

Let's test it. First, some initial conditions:

struct Icecream {
    let flavor : String
    let price : Double
}
let arr = [
    Icecream(flavor: "vanilla", price: 1.5),
    Icecream(flavor: "vanilla", price: 1.75)
]

And now the actual test:

arr.allSameForProperty(\Icecream.flavor) // true
arr.allSameForProperty(\Icecream.price) // false