Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant way to split an array in swift

Tags:

arrays

swift

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
    }
}
like image 968
Manel Avatar asked Nov 20 '20 15:11

Manel


People also ask

How do you separate characters in a string in Swift?

let str = "Hello! Swift String." ["Hello!", "Swift", "String."] In the above example, we use a space "" as a separator, but a separator doesn't need to be a single character. We can use a string as a separator with components (separatedBy:). You can also use a set of characters ( CharacterSet) as separators.

What is an array in Swift?

An array can hold multiple elements of a given type. We can use them to store numbers, strings, classes, but in general elements can be anything. With the Any type you can actually express this and you can put anything into this random access collection. There are quite many ways to create an array in Swift.

How to split a string into an array using a separator string?

To split a string into an array by using a separator string, call the String.components (separatedBy:) function. Here the argument separatedBy can be either one of the following:

Is there a limit on the number of splits in Swift?

If not provided, there is no limit on the number of splits. omittingEmptySubsequences (optional) - specifies whether to omit empty string elements or to include them Note: If maxSplits is specified, the array will have a maximum of maxSplits + 1 items. var text = "Swift is awesome. Swift is fun." // split at period "."


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], []]
like image 138
Martin R Avatar answered Oct 16 '22 11:10

Martin R


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"]
like image 6
Leo Dabus Avatar answered Oct 16 '22 11:10

Leo Dabus


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.

like image 6
Fattie Avatar answered Oct 16 '22 12:10

Fattie


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].

like image 5
Cristik Avatar answered Oct 16 '22 12:10

Cristik


'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)
  }
like image 5
Jessy Avatar answered Oct 16 '22 10:10

Jessy