1

I'm trying to grab a batch of 3 from an array within a loop. I feel there must be a more elegant way with Swift.

Here's what I have so far:

for (index, item) in allItems.enumerate() {
    var batch: [MyType] = []

    if index < allItems.endIndex {
        batch.append(allItems[index])
    }

    if index + 1 < allItems.endIndex {
        batch.append(allItems[index + 1])
    }

    if index + 2 < allItems.endIndex {
        batch.append(allItems[index + 2])
    }

    sendBatchSomewhere(batch)
}

Any better and safer way to achieve grabbing a batch? The middle is easy, but of course handling the beginning and end gets a little tricky. Any Swifty ideas?

Update:

Thanks, this works beautifully! Here's the playground version:

import Foundation

typealias MyType = (a: String, b: Int, c: Int)

let allItems1: [MyType] = []

let allItems2 = [
    (a: "Item 1", b: 2, c: 3)
]

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

let testItems = allItems3 // Change to allItems1, allItems2, allItems3, etc
let batchSize = 3

let output = testItems.indices.map { fromIndex -> [MyType] in
    let toIndex = fromIndex.advancedBy(batchSize, limit: testItems.endIndex)
    return Array(testItems[fromIndex ..< toIndex])
}

print(output) =>

    [
      [("Item 1", 2, 3), ("Item 2", 4, 5), ("Item 3", 6, 7)], 
      [("Item 2", 4, 5), ("Item 3", 6, 7), ("Item 4", 8, 9)], 
      [("Item 3", 6, 7), ("Item 4", 8, 9), ("Item 5", 10, 11)], 
      [("Item 4", 8, 9), ("Item 5", 10, 11), ("Item 6", 12, 13)], 
      [("Item 5", 10, 11), ("Item 6", 12, 13), ("Item 7", 14, 15)], 
      [("Item 6", 12, 13), ("Item 7", 14, 15), ("Item 8", 16, 17)], 
      [("Item 7", 14, 15), ("Item 8", 16, 17), ("Item 9", 18, 19)], 
      [("Item 8", 16, 17), ("Item 9", 18, 19), ("Item 10", 20, 21)], 
      [("Item 9", 18, 19), ("Item 10", 20, 21), ("Item 11", 22, 23)], 
      [("Item 10", 20, 21), ("Item 11", 22, 23)], 
      [("Item 11", 22, 23)]
    ]
TruMan1
  • 33,665
  • 59
  • 184
  • 335
  • 1
    Is it intentional that your "batches" overlap? – Martin R Mar 16 '16 at 15:49
  • Good point, yes actually. Building for watch complication so the first item in the batch is the "parent". Then the parent+1 will be the parent for the next batch. – TruMan1 Mar 16 '16 at 15:50

4 Answers4

4

You can use slices and the three-parameter form of advancedBy() which takes a limiting argument. Example:

let allItems = [1, 2, 3, 4, 5]
let batchSize = 3

allItems.indices.forEach { fromIndex in
    let toIndex = fromIndex.advancedBy(batchSize, limit: allItems.endIndex)
    let batch = allItems[fromIndex ..< toIndex]

    // Or, if you need a "real" array:
    // let batch = Array(allItems[fromIndex ..< toIndex])

    print(batch)
}

Output:

[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
[4, 5]
[5]

If you want an array with all batches then you can use map() instead of forEach():

let output = allItems.indices.map { fromIndex -> [Int] in
    let toIndex = fromIndex.advancedBy(batchSize, limit: allItems.endIndex)
    return Array(allItems[fromIndex ..< toIndex])
}
print(output)

Output:

[[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5], [5]]
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Indeed elegant, thanks again Martin!! I updated my post with a playground version from your answer. – TruMan1 Mar 16 '16 at 16:14
2

Here's an extension approach (should be safe as well) that should be re-usable anywhere:

extension Array {

    func getSubArray(startIndex: Int, maxNumber: Int) -> Array? {

        guard startIndex < count else{
            return nil
        }
        let desiredLastIndex = startIndex + maxNumber
        let endNumber = (desiredLastIndex >= endIndex) ? endIndex : desiredLastIndex

        return Array(self[startIndex..<endNumber])
    }
}
GetSwifty
  • 7,568
  • 1
  • 29
  • 46
0

Hope you will looking for this one

    var batch = allItems[0..<allItems.endIndex] + allItems[0..<allItems.endIndex-1] + allItems[0..<allItems.endIndex-2]
    sendBatchSomewhere(batch)
Muzahid
  • 5,072
  • 2
  • 24
  • 42
0

Here is a custom ArrayIterator, that has worked for our case. Maybe it will be helpful for anyone coming here in the future

class ArrayIterator<T>{

private var array : [T] = []
private var stepSize: Int = 10
private var head: Int = 0

var hasNext: Bool {
    get {
        return head < array.count
    }
}
class func from(array: [T], stepSize size: Int = 10, startingAt startIndex: Int = 0) -> ArrayIterator<T>{
    
    let a = ArrayIterator<T>()
    a.array = array
    a.stepSize = size
    a.head = startIndex
    return a
}

func next() -> Array<T>? {
    guard head < array.count else {
        return nil
    }
    
    defer {
        head = head + stepSize
    }
    
    guard stepSize < array.count else {
        return array
    }
    
    if let _ = array[safe: (head + stepSize - 1)] {
        return Array(array[head..<head + stepSize])
        
    } else {
        let remaider = (head + stepSize - 1) % array.count
        return Array(array[head..<(head + stepSize - 1 - remaider)])
    }
}

}

Victor Benetatos
  • 396
  • 3
  • 12