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?
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'