Assign elements at index, index+1, index+2 into array

Hi,

Note: This is not school homework, I have made an attempt, but wasn't sure if that was a reasonable approach, or if there is a better way to do it.

Overview

I have an array of numbers
I would like to group them into an array of arrays in the following way:

[[numbers[0], numbers[1], numbers[2],
 [numbers[1], numbers[2], numbers[3],
 ...
 ...
 [numbers[x], numbers[x+1], numbers[x+2],
 [numbers[x+1], [numbers[x+2],
 [numbers[x+2]]

Example:

If numbers = [23, 42, 19, 7, 150, 8, 20, 41]
Output would be as follows:

[[23, 42, 19], [42, 19, 7], [19, 7, 150], [7, 150, 8], [150, 8, 20], [8, 20, 41], [20, 41], [41]]

My implementation

let numbers = [23, 42, 19, 7, 150, 8, 20, 41]

let count = numbers.count
var groups = [[Int]]()
for index in 0 ..< count {
    
    var group = [Int]()
    group.append(numbers[index])
    if index + 1 < count {
        group.append(numbers[index + 1])
        
        if index + 2 < count {
            group.append(numbers[index + 2])
        }
    }
    
    groups.append(group)
}

print(groups)

Questions:

  • Is there a better way to achieve this?
  • I thought my implementation was verbose and was hoping that there was a better way or is my approach reasonable?

This is not school homework,

:+1:

It's nice you include some code.

First thing that comes to mind: is there a way to avoid those if statements. They run on each loop.
If we go beyond the last element we get an error. So stop sooner.

let count = numbers.count
var groups = [[Int]]()
for index in 0 ..< count -2
{
    var group = [Int]()
    group.append(numbers[index])
    group.append(numbers[index + 1])    
    group.append(numbers[index + 2])
    groups.append(group)
}

We can now drop the count variable and will account for arrays smaller than 2.

var groups = [[Int]]()
for index in 0 ..< max(0, numbers.count -2)
{
    var group = [Int]()
    group.append(numbers[index])
    group.append(numbers[index + 1])    
    group.append(numbers[index + 2])
    groups.append(group)
}

So we are actually just extracting three values from an array. :thinking:

var groups = [[Int]]()
for index in 0 ..< max(0, numbers.count -2)
{
    var group = numbers[index..<index+2]
    groups.append(group)
}

Might as well make it a one liner.

var groups = [[Int]]()
for index in 0 ..< max(0, numbers.count -2)
{
    groups.append(numbers[index..<index+2])
}

Oh, that doesn't compile because it's a slice. So:

    groups.append(Array(numbers[index..<index+2]))

Unfortunately we don't have the last two groups in your output:

groups.append(numbers.suffix(2))
groups.append(numbers.suffix(1))

Which all together gives:

	var groups = [[Int]]()
	for index in 0 ..< max(0, numbers.count - 2)
	{
		groups.append(Array(numbers[index..<index+2]))
	}

	groups.append(numbers.suffix(2))
	groups.append(numbers.suffix(1))

Though this will add empty arrays if the number of elements is not a multiple of 3. An if statement can be added on these two lines. At least it's not checking the index on each loop anymore.

There might be shorter/better ways to do this. Maybe somebody else will chime in.

3 Likes

Your code is nice and simple, which is good. But some things could be improved a bit.

I would also recommend using slices, and to avoid addition for indexing operations - Collection already has all of the indexing operations that we need. It's also good to reserve the array capacity when we know it.

let numbers = [23, 42, 19, 7, 150, 8, 20, 41]

var groups = [[Int]]()
groups.reserveCapacity(numbers.count)
for index in numbers.startIndex ..< numbers.endIndex {
    let groupEnd = numbers.index(index, offsetBy: 3, limitedBy: numbers.endIndex) ?? numbers.endIndex
    groups.append(Array(numbers[index..<groupEnd]))
}

print(groups)
// [[23, 42, 19], [42, 19, 7], [19, 7, 150], [7, 150, 8], [150, 8, 20], [8, 20, 41], [20, 41], [41]]

Note that it becomes much easier to make adjustments, such as changing the length of the groups.

However, we're also allocating an array for each group, and there will be a lot of groups - if numbers has n values, you will have n groups, meaning n Array allocations. If the rest of your application allows it, I would also recommend changing groups to be an array of slices, in other words:

var groups = [ArraySlice<Int>]()
...
groups.append(numbers[index..<rowEnd])

This means all of the groups will share the same underlying array storage.

5 Likes

May be harder to follow, but you could do this by iterating over successive triplets with a nested zip.

let numbers = [23, 42, 19, 7, 150, 8, 20, 41]
let paddedNumbers = (numbers + [nil, nil])
let groups = zip(zip(paddedNumbers, paddedNumbers.dropFirst()), paddedNumbers.dropFirst(2))
  .map { [$0.0.0, $0.0.1, $0.1].compactMap { $0 } }
print(groups)

Padding numbers with two nils is required to get the 2-element and single-element triplets at the end of the iteration, with the nils filtered out using a compactMap.

Using the index type as Karl suggests is way better. Especially if you want to turn this into a generic function.

func group<C>(collection: C, by length: Int) 
where C: Collection, C.Element == Int, C.Index == Int
{
 ...
}

The collection might not start at 0 so startIndex is required.

I should have done code completion on the index type for inspiration. :joy:

If you're fine with the return type of [ArraySlice<Int>] instead of [[Int]], then this one-liner should also do what you need:

numbers.indices.map { numbers[$0...].prefix(3) }

otherwise, wrap that closure output in Array.init like so:

numbers.indices.map { Array(numbers[$0...].prefix(3)) }
6 Likes

Since it’s not homework, you don’t need to roll your own:

import Algorithms

let numbers = …
let groups = numbers.windows(ofCount: 3)
8 Likes

to avoid any confusion: the Collection.windows(ofCount:) method is from the swift-algorithms package. it does not come with the toolchain.

it is also available on Array, though there is no way of knowing this via the docs, because Collection.windows(ofCount:) is dark API.

3 Likes

windows(ofCount:) is very useful but fails to handle the tail in the way the OP specified it to be.

1 Like

While we are bikeshedding, I’ll make my usual suggestion to never use indexes when counts are more appropriate:

for i in numbers.indices {
  groups.append(Array(numbers[i...].prefix(3)))
}
7 Likes

Ah yes, failed to note the tail behavior; if that’s essential then some extra finagling will be required.

Thank you so much guys!!!!

Amazing to see the different approaches to the same problem.

Been a great learning for me, thank you so much!!

4 Likes