Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift generic array function to find all indexes of elements not matching item

Swift 3

Trying to write a generic array extension that gets all indexes of items that DON'T equal value

example

 let arr: [String] = ["Empty", "Empty", "Full", "Empty", "Full"]
 let result: [Int] = arr.indexes(ofItemsNotEqualTo item: "Empty")
 //returns [2, 4]

I tried to make a generic function:

extension Array {

    func indexes<T: Equatable>(ofItemsNotEqualTo item: T) -> [Int]?  {
        var result: [Int] = []
        for (n, elem) in self.enumerated() {
            if elem  != item {
                result.append(n)
            }
        }
        return result.isEmpty ? nil : result
    }
}

But that gives a warning: Binary operator cannot be applied to operands of type "Element" and "T".

So then I did this where I cast the element (note the as?)

extension Array {

    func indexes<T: Equatable>(ofItemsNotEqualTo item: T) -> [Int]?  {
        var result: [Int] = []
        for (n, elem) in self.enumerated() {
            if elem as? T != item {
                result.append(n)
            }
        }
        return result.isEmpty ? nil : result
    }
}

But now it seems the type checking has gone out the window, because if I pass in an integer I get the wrong result

 let arr: [String] = ["Empty", "Empty", "Full", "Empty", "Full"]
 let result: [Int] = arr.indexes(ofItemsNotEqualTo item: 100)
 //returns [0, 1, 2, 3, 4]

Help would be greatly appreciated.

Is there a better way to do this with the reduce function?

like image 574
MH175 Avatar asked Dec 21 '16 05:12

MH175


1 Answers

You have defined a generic method

func indexes<T: Equatable>(ofItemsNotEqualTo item: T) -> [Int]?

which takes an argument of type T which is required to be Equatable, but is unrelated to the Element type of the array.

Therefore

let arr = ["Empty", "Empty", "Full", "Empty", "Full"]
let result = arr.indexes(ofItemsNotEqualTo: 100)

compiles, but elem as? T gives nil (which is != item) for all array elements.

What you want is a method which is defined only for arrays of Equatable elements. This can be achieved with a constrained extension:

extension Array where Element: Equatable {
    func indexes(ofItemsNotEqualTo item: Element) -> [Int]?  {
        var result: [Int] = []
        for (n, elem) in enumerated() {
            if elem != item {
                result.append(n)
            }
        }
        return result.isEmpty ? nil : result
    }
}

Actually I would not make the return value an optional. If all elements are equal to the given item, then the logical return value would be the empty array.

Is there a better way to do this with the reduce function?

Well, you could use reduce(), but that is not very efficient because intermediate arrays are created in each iteration step:

extension Array where Element: Equatable {
    func indexes(ofItemsNotEqualTo item: Element) -> [Int]  {
        return enumerated().reduce([]) {
            $1.element == item ? $0 : $0 + [$1.offset]
        }
    }
}

What you actually have is a "filter + map" operation:

extension Array where Element: Equatable {
    func indexes(ofItemsNotEqualTo item: Element) -> [Int]  {
        return enumerated().filter { $0.element != item }.map { $0.offset }
    }
}

which can be simplified using flatMap():

extension Array where Element: Equatable {

    func indexes(ofItemsNotEqualTo item: Element) -> [Int]  {
        return enumerated().flatMap { $0.element != item ? $0.offset : nil }
    }
}

Examples:

let arr = ["Empty", "Empty", "Full", "Empty", "Full"]
arr.indexes(ofItemsNotEqualTo: "Full") // [0, 1, 3]

[1, 1, 1].indexes(ofItemsNotEqualTo: 1) // []

arr.indexes(ofItemsNotEqualTo: 100)
// error: cannot convert value of type 'Int' to expected argument type 'String'
like image 76
Martin R Avatar answered Sep 18 '22 13:09

Martin R