Elegant way to split an array in swift

Viewed 3225

Given an array of any kind and the wanted number of subarray, i need this output :

print([0, 1, 2, 3, 4, 5, 6].splitInSubArrays(into: 3))
// [[0, 3, 6], [1, 4], [2, 5]]

Output must contain the correct number of subarrays even if there is not "enough" elements to fill those :

print([0, 1, 2].splitInSubArrays(into: 4))
// [[0], [1], [2], []]

I have this working implementation for now but is there a better (more elegant) way of achieving this output :

extension Array {

    func splitInSubArrays(into size: Int) -> [[Element]] {

        var output: [[Element]] = []

        (0..<size).forEach {

            var subArray: [Element] = []

            for elem in stride(from: $0, to: count, by: size) {
                subArray.append(self[elem])
            }

            output.append(subArray)
        }

        return output
    }
}
5 Answers

You can replace both loops with a map() operation:

extension Array {
    func splitInSubArrays(into size: Int) -> [[Element]] {
        return (0..<size).map {
            stride(from: $0, to: count, by: size).map { self[$0] }
        }
    }
}

The outer map() maps each offset to the corresponding array, and the inner map() maps the indices to the array elements.

Examples:

print([0, 1, 2, 3, 4, 5, 6].splitInSubArrays(into: 3))
// [[0, 3, 6], [1, 4], [2, 5]]

print([0, 1, 2].splitInSubArrays(into: 4))
// [[0], [1], [2], []]

Just for fun a generic implementation that would work with strings as well:

extension Collection {
    func every(n: Int, start: Int = 0) -> UnfoldSequence<Element,Index> {
        sequence(state: dropFirst(start).startIndex) { index in
            guard index < endIndex else { return nil }
            defer { index = self.index(index, offsetBy: n, limitedBy: endIndex) ?? endIndex }
            return self[index]
        }
    }
}

extension RangeReplaceableCollection {
    func splitIn(subSequences n: Int) -> [SubSequence] {
        (0..<n).map { .init(every(n: n, start: $0)) }
    }
}

[0, 1, 2, 3, 4, 5, 6].splitIn(subSequences: 3)   // [[0, 3, 6], [1, 4], [2, 5]]
[0, 1, 2].splitIn(subSequences: 4)               // [[0], [1], [2], []]
"0123456".splitIn(subSequences: 3)               // ["036", "14", "25"]

KISS, algorithm-matching approach:

The most intuitive way to do this is dead simple:

  • for each index
  • get the remainder when dividing by three
  • put the number in that array

So it's really nothing more than this:

arrays[i%n].append(item i)

Example code per @LeoDabus comment below

extension RangeReplaceableCollection {
    func moduloishtrancheization(n: Int) -> [SubSequence] {
        var r: [SubSequence] = .init(repeating: .init(), count: n)
        var i = 0
        forEach {
            r[i%n].append($0)
            i += 1
        }
        return r
    }
}

That's the whole thing.

'Twould be good to allow it to be used on all sequences.

stride(from: 0, through: 6, by: 1).splitInSubArrays(into: 3)

(Put this into a public extension too, if it's useful across many apps, like the one below is.)

extension Sequence {
  func splitInSubArrays(into size: Int) -> [[Element]] {
    enumerated()
      .grouped { $0.offset % size }
      .map { $0.map(\.element) }
  }
}
  /// Group the elements by a transformation into an `Equatable`.
  /// - Note: Similar to `Dictionary(grouping values:)`,
  /// but preserves "key" ordering, and doesn't require hashability.
  func grouped<Equatable: Swift.Equatable>(
    by equatable: (Element) throws -> Equatable
  ) rethrows -> [[Element]] {
    try reduce(into: [(equatable: Equatable, elements: [Element])]()) {
      let equatable = try equatable($1)

      if let index = ( $0.firstIndex { $0.equatable == equatable } ) {
        $0[index].elements.append($1)
      } else {
        $0.append((equatable, [$1]))
      }
    }.map(\.elements)
  }

For completeness, here's a reduce-based solution that works on all Collection types:

extension Collection {
    func splitInSubArrays(_ size: Int) -> [[Element]] {
        enumerated().reduce(into: [[Element]](repeating: [], count: size)) {
            $0[$1.offset % size].append($1.element)
        }
    }
}

How the function works: it creates a an empty array of [Element] entries, and appends each element of the original array to the corresponding sub-array. We're using here of reduce just to carry the result array, to avoid explicitly creating a local variable (though internally reduce is doing that for us).

Usage:

print([0, 1, 2, 3, 4, 5, 6].splitInSubArrays(3)) // [[0, 3, 6], [1, 4], [2, 5]]
print([0, 1, 2].splitInSubArrays(4))             // [[0], [1], [2], []]
print("ABCDEF".splitInSubArrays(3))              // ["A", "D"], ["B", "E"], ["C", "F"]]

Note that, as Leo Dabus pointed out, in the last example above the 2-D array is not a string-based one, it's a 2-D character array [[Character]]. To generate a array of substrings instead, RangeReplaceableCollection can be extended, and the result type can be changed to [SubSequence].

Related