Nicolai Henriksen
Nicolai Henriksen

Reputation: 1504

Swift: loop over array elements and access previous and next elements

In Swift, I want to loop over an array and compare each element to the previous and/or next. For each comparison I will either produce a new element or nothing. Is there "functional" way of doing this?

An example could be that I have an array of Int and want to find all "local minimums.

I could do it sequencially like this

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]
var i = 1
var r: [Int] = []

while i < a.count - 1 {
    if a[i] < a[i+1] && a[i] < a[i-1] {
        r.append(i)
    }
    i += 1
}

print(r)
// [6, 11]

I wonder if there is a more simple or direct way to do it.

Upvotes: 14

Views: 10275

Answers (7)

chasew
chasew

Reputation: 8828

I was looking for a variation of the original Q that I hope might help someone else. I needed to map every item in the array while considering the previous and next values:

extension Sequence {
    var withPreviousAndNext: [(Element?, Element, Element?)] {
        let optionalSelf = self.map(Optional.some)
        let next = optionalSelf.dropFirst() + [nil]
        let prev = [nil] + optionalSelf.dropLast()
        return zip(self, zip(prev, next)).map {
            ($1.0, $0, $1.1)
        }
    }
}

And the not so pretty way to use that with original Q:

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]
let indices = a.enumerated().withPreviousAndNext.compactMap { values -> Int? in
    let (prev, cur, next) = values
    return (cur.1 < (prev?.1 ?? Int.min) && cur.1 < (next?.1 ?? Int.min)) ? cur.0 : nil
}
indices // [6,11]

Upvotes: 4

r.izumita
r.izumita

Reputation: 450

I think Martin R's answer is smart, though I try answering using the other way.

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]

func withPrevAndNext<T, U>(`default`: U, _ f: @escaping (T, T, T) -> (U)) -> (T) -> (U) {
    var previous: T?
    var current: T?

    return { next in
        defer { (previous, current) = (current, next) }
        guard let prev = previous, let curt = current else { return `default` }
        return f(prev, curt, next)
    }
}

let r = a.enumerated().compactMap(withPrevAndNext(default: .none) { prev, curt, next -> Int? in
    curt.1 < prev.1 && curt.1 < next.1 ? curt.0 : .none
})

print(r)
// [6, 11]

Upvotes: 0

Brian Arnold
Brian Arnold

Reputation: 373

Would a simple for-loop over the range be sufficiently readable and maintainable? You could just cache the intermediate values as you iterate, so that you only access one element of the array in each iteration. If you wanted to generalize this for any comparable type, you could implement it as an extension of Array:

extension Array where Element: Comparable {

    func localMinimums() -> [Int] {
        var minimums = [Int]()

        var currentValue = self[0]
        var nextValue = self[1]
        for index in 1..<(self.count - 1) {
            let previousValue = currentValue
            currentValue = nextValue
            nextValue = self[index + 1]
            if currentValue < nextValue && currentValue < previousValue {
                minimums.append(index)
            }
        }

        return minimums
    }
}

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]
let r = a.localMinimums()
print(r)
// [6, 11]

Upvotes: 0

Martin R
Martin R

Reputation: 539965

Generally, one can use dropFirst() and zip() to traverse over adjacent array elements in parallel. Here is a simple example which produces the array of increments between the array elements:

let a = [ 1, 2, 2, 3, 5, 4, 2, 5, 7, 9, 5, 3, 8, 10 ]

let diffs = zip(a.dropFirst(), a).map(-)
print(diffs)
// [1, 0, 1, 2, -1, -2, 3, 2, 2, -4, -2, 5, 2]

To compute the indices of local minima we can iterate over a, a.dropFirst() and a.dropFirst(2) in parallel. enumerated() is used to keep track of the array offsets, and flatMap() (renamed to compactMap() in Swift 4.1) is used to pick only those indices which correspond to a local minimum:

let a = [ 1, 2, 2, 3, 5, 4, 2, 5, 7, 9, 5, 3, 8, 10 ]

let localMins = zip(a.enumerated().dropFirst(), zip(a, a.dropFirst(2))).flatMap {
    $0.element < $1.0 && $0.element < $1.1 ? $0.offset : nil
}
print(localMins) // [6, 11]

Upvotes: 13

Sandeep
Sandeep

Reputation: 21154

You could also iterate over indices and compare like this,

for i in a.indices.dropFirst().dropLast()
{
    if a[i] < a[a.index(after: i)],
            a[i] < a[a.index(before: i)] {
        r.append(i)
    }
}
print(r)
// [6, 11]

Or, something like this,

let result = a.indices.dropLast().dropFirst().filter { i in
    return a[i] < a[a.index(after: i)] &&
            a[i] < a[a.index(before: i)]
}
print(r)
// [6, 11]

Or, short,

let result = a.indices.dropLast()
                      .dropFirst()
                      .filter { a[$0] < a[$0 + 1] &&
                                a[$0] < a[$0 - 1] }
 print(result)

Upvotes: 1

dmorrow
dmorrow

Reputation: 5694

Using flatMap

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]
let r = a.enumerated().flatMap { (_ offset: Int, _ element: Int) -> Int? in
    guard offset > 0 else { return nil }
    if element < a[offset-1] && element < a[offset+1] {
        return offset
    }
    return nil
}

Upvotes: 0

rmaddy
rmaddy

Reputation: 318884

You can replace the while loop and i with a for loop and stride.

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]
var r: [Int] = []

for i in stride(from: 1, to: a.count - 1, by: 1) {
    if a[i] < a[i+1] && a[i] < a[i-1] {
        r.append(i)
    }
}

print(r)
// [6, 11]

You can get real fancy with a filter but this isn't nearly as readable as the above code:

let a = [ 1,2,2,3,5,4,2,5,7,9,5,3,8,10 ]
let r = a.enumerated().dropFirst().dropLast().filter { $0.1 < a[$0.0 + 1] && $0.1 < a[$0.0 - 1] }.map { $0.0 }
print(r)
// [6, 11]

Upvotes: 3

Related Questions