Best way to loop through array and group consecutive numbers in another array SWIFT 4?

Viewed 1886

I'm trying to figure out the most efficient way using SWIFT 4 to loop through an array of numbers, get the range of any consecutive numbers and add those to a new array. I could do the standard loop checks but I believe I can use a map filter? -- can someone point me in the right direction?

Beginning:

myNumbersArray:[Int] = [1,2,3,4,10,11,15,20,21,22,23]

Wanted result:

newNumbersArray = [
   [1,2,3,4],
   [10,11],
   [15],
   [20,21,22,23]
]

I will post my solution if I figure it out...

4 Answers

My suggestion is IndexSet where consecutive items are stored as ranges.

  • Create the index set from the array.
  • Get the rangeView.
  • Map the ranges to arrays.

let myNumbersArray = [1,2,3,4,10,11,15,20,21,22,23]
let indexSet = IndexSet(myNumbersArray)
let rangeView = indexSet.rangeView
let newNumbersArray = rangeView.map { Array($0.indices) }

This would work also with negative integers:

extension BidirectionalCollection where Element: BinaryInteger, Index == Int {
    var consecutivelyGrouped: [[Element]] { 
        return reduce(into: []) {
            $0.last?.last?.advanced(by: 1) == $1 ? 
            $0[index(before: $0.endIndex)].append($1) :
            $0.append([$1]) 
        }
    }
}

let numbers = [-5,-4,-2,0,1,3,4,10,11,15,20,21,22,23]
let grouped = numbers.consecutivelyGrouped  // [[-5, -4], [-2], [0, 1], [3, 4], [10, 11], [15], [20, 21, 22, 23]]

While using IndexSet according to @vadian is a good idea, it works only in your case (consecutive and positive integers), and it may be not your purpose to use indices. It's a little hacky to use a similar kind of objects for that, but does nicely the job.

A possible way is to use reduce():

let reduced = myNumbersArray.reduce([[Int]]()) { (current, next) -> [[Int]] in
    var result = current
    //Retrieve the last sequence, check if the current - last item of sequence is 1 to know if they are consecutive or not
    if var lastSequence = result.last, let last = lastSequence.last, next-last == 1 {
        lastSequence.append(next)
        result[result.endIndex-1] = lastSequence
        return result
    } else { //It's not => New array of its own
        result.append([next])
        return result
    }
}
print("reduced: \(reduced)")

Output:

$>reduced: [[1, 2, 3, 4], [10, 11], [15], [20, 21, 22, 23]]

As suggested by @Leo Dabus, with reduce(into:):

let reducedInto = myNumbersArray.reduce(into: [[Int]]()) { (result, next) in
    //Retrieve the last sequence, check if the current - last item of sequence is 1
    if var lastSequence = result.last, let last = lastSequence.last, next-last == 1 {
        lastSequence.append(next)
        result[result.endIndex-1] = lastSequence
    } else { //It's not => New array of its own
        result.append([next])
    }
}
print("reducedInto: \(reducedInto)")

Output:

$>reducedInto: [[1, 2, 3, 4], [10, 11], [15], [20, 21, 22, 23]]

For those who are not familiar with map and indexset check this:

var arr = [0,1,2,4,6,7,9]
var previousIndex = 0
var count = 0
var arrayDict : [Int:[Int]] = [:]
for (index,data) in arr.enumerated() {
  if index == (arr.count - 1) {
     arrayDict[count] = Array(arr[previousIndex ..< index + 1])
     break
  }
  if arr[index + 1] - data != 1 {
    arrayDict[count] = Array(arr[previousIndex ..< index + 1])
    previousIndex = index + 1
    count += 1
  }
}
print(arrayDict)

The logic is that i'm iterating over the elements of the given array and each time checking whether the value at that index is not less than value at index + 1 by 1, if it is then I am splitting the array using Array slice and adding it in a dictionary having a key which is given by the variable 'count'. the variable previous Index holds the end index of previous split so that next split starts from previousIndex to the current index

Related