TruMan1
TruMan1

Reputation: 36118

Find closest item in array that matches criteria in Swift?

When I have a reference to an item in an array, I'd like to find another item closest to it that matches a certain criteria (forward or backwards).

For example, I have this array:

let items = [
    (a: "Item 1", b: "F", c: 3),
    (a: "Item 2", b: "S", c: 5),
    (a: "Item 3", b: "D", c: 7),
    (a: "Item 4", b: "A", c: 9),
    (a: "Item 5", b: "M", c: 11),
    (a: "Item 6", b: "I", c: 13),
    (a: "Item 7", b: "F", c: 15),
    (a: "Item 8", b: "S", c: 17),
    (a: "Item 9", b: "D", c: 19),
    (a: "Item 10", b: "A", c: 21),
    (a: "Item 11", b: "M", c: 23),
    (a: "Item 12", b: "I", c: 13),
    (a: "Item 13", b: "F", c: 15),
    (a: "Item 14", b: "S", c: 17),
    (a: "Item 15", b: "D", c: 19),
    (a: "Item 16", b: "A", c: 21),
    (a: "Item 17", b: "M", c: 23),
    (a: "Item 18", b: "I", c: 13),
    (a: "Item 19", b: "F", c: 15),
    (a: "Item 20", b: "S", c: 17),
    (a: "Item 21", b: "D", c: 19),
    (a: "Item 22", b: "A", c: 21),
    (a: "Item 23", b: "M", c: 23),
    (a: "Item 24", b: "I", c: 13)
]

Now say I have item[7], how can I find the closest item that has b = "I"? I can only think of a few nested for loops but sounds messy and not good on performance. Also keeping in mind I don't want an out of range issue when searching. Any Swift-like ideas on how to handle this?

Upvotes: 1

Views: 4913

Answers (6)

David Berry
David Berry

Reputation: 41236

The following is a generic on Array, which should do what you're looking for. It returns a tuple containing the index and value of the closest match:

extension Array {
    func closestMatch(_ index:Index, predicate:(Element)->Bool) -> (Int, Element)? {
        
        if predicate(self[index]) {
            return (index, self[index])
        }
        
        var delta = 1

        while(true) {
            guard index + delta < count || index - delta >= 0 else {
                return nil
            }

            if index + delta < count && predicate(self[index + delta]) {
                return (index + delta, self[index + delta])
            }
            
            if index - delta >= 0 && predicate(self[index - delta]) {
                return (index - delta, self[index - delta])
            }
            
            delta = delta + 1
        }
    }
}

print(items.closestMatch(7) { $0.1 == "I" })

Upvotes: 2

Abhijeet Ravi
Abhijeet Ravi

Reputation: 295

Optimised solution using higher order functions:

 func closestMatch(values: [Int64], inputValue: Int64) -> Int64? {
    return (values.reduce(values[0]) { abs($0-inputValue) < abs($1-inputValue) ? $0 : $1 })
}

Swift is advancing with every version to optimise the performance and efficiency. With higher order functions finding the closest match in an array of values is much easier with this implementation. Change the type of value as per your need.

Upvotes: 1

Eendje
Eendje

Reputation: 8883

In addition to the other solutions, a "one-liner":

let index = 7
let searchString = "I"

let result = items.enumerate()
    .filter { $0.1.b == searchString }
    .map { (abs(index - $0.0), $0.1) }
    .minElement { $0.0 < $1.0 }
    .map { $0.1 }

print(result) // Optional(("Item 6", "I", 13))

Based on the answer of David Berry:

extension Array {
    func closestMatch(index: Index, predicate: (Element) -> Bool) -> Element? {
        return enumerate().filter { predicate($0.1) }.map { (abs(index - $0.0), $0.1) }.minElement { $0.0 < $1.0 }.map { $0.1 }
    }
}

print(items.closestMatch(7) { $0.1 == "I" }) // Optional(("Item 6", "I", 13))

NOTE: Performance wise, the answer of David Berry is better.

Upvotes: 1

user887210
user887210

Reputation:

Simplest would be:

let startIndex = 7
if startIndex < items.count,
  let nextIndex = items[startIndex+1..<items.endIndex].indexOf({ $0.b == items[startIndex].b }) {
    print(items[nextIndex]) // => "("Item 14", "S", 17)\n"
}

This isn't a complete answer to the question, only an example of a way to do a forward search. I realized it didn't match what was asked after I posted it but I'll leave it up as a partial example.

Upvotes: 0

GetSwifty
GetSwifty

Reputation: 7756

This function should work:

func findClosestItem(index: Int, items: [(a: String, b: String, c: Int)]) -> (a: String, b: String, c: Int)? {

    guard index < items.count && index > -1 else{
        return nil
    }
    var foundItem: (String, String, Int)? = nil
    var closestBefore = Int(INT16_MAX)
    for i in (index - 1).stride(to: 0, by: -1) {
        if items[index].b == items[i].b {
            closestBefore = index - i
            foundItem = items[i]
            break
        }
    }

    var closestAfter = Int(INT16_MAX)
    for i in index + 1 ..< items.count {
        if items[index].b == items[i].b {
            closestAfter = i - index
            if closestAfter < closestBefore {
                foundItem = items[i]
            }
            break
        }
    }

    return foundItem
}

It will return nil if there is no other matching item or if it's an invalid index, otherwise it will return the item.

It simply searches back until it find a matching item, then searches forward till it finds a matching item, and will return the one that's closest.

Upvotes: 0

LHA
LHA

Reputation: 9655

Fake code:

int index = 7; 

int delta = 0;
while (true)
{
    delta = delta + 1;

    if ( index - delta >= 0 && matches(items[ index - delta]) {

         // Found closest index on Left :  index-delta

         break;
    }

    if ( index + delta < items.length && matches(items[ index + delta]) {

         // Found closest index on Right:  index + delta

         break;
    }
}

// Time complexity: O(N)

You can easily convert the fake code to Swift code.

Upvotes: 1

Related Questions