Swift: ¿cuál es la forma correcta de dividir un dando como resultado un ] con un tamaño de subcampo dado?

Comenzando con un gran [String] y un tamaño de subcampo dado, ¿cuál es la mejor manera de dividir esta matriz en matrices más pequeñas? (La última matriz será más pequeña que el tamaño de subcampo dado).

Ejemplo concreto:

Dividir [“1”, “2”, “3”, “4”, “5”, “6”, “7”] con un tamaño máximo de división 2

El código produciría [[“1”, “2”], [“3”, “4”], [“5”, “6”], [“7”]]

Obviamente, podría hacerlo un poco más manualmente, pero siento que algo así como map () o reducir () puede hacer lo que quiero realmente bien.

No lo llamaría hermoso, pero aquí hay un método que usa el map :

 let numbers = ["1","2","3","4","5","6","7"] let splitSize = 2 let chunks = numbers.startIndex.stride(to: numbers.count, by: splitSize).map { numbers[$0 ..< $0.advancedBy(splitSize, limit: numbers.endIndex)] } 

El método stride(to:by:) le proporciona los índices para el primer elemento de cada fragmento, por lo que puede asignar esos índices a un sector del conjunto de origen utilizando advancedBy(distance:limit:) .

Un enfoque más "funcional" sería simplemente recurrir a través de la matriz, así:

 func chunkArray(s: [T], splitSize: Int) -> [[T]] { if countElements(s) < = splitSize { return [s] } else { return [Array(s[0..(s[splitSize.. 

En Swift 3/4 esto se vería así:

 let numbers = ["1","2","3","4","5","6","7"] let chunkSize = 2 let chunks = stride(from: 0, to: numbers.count, by: chunkSize).map { Array(numbers[$0.. 

Como una extensión de Array:

 extension Array { func chunked(by chunkSize: Int) -> [[Element]] { return stride(from: 0, to: self.count, by: chunkSize).map { Array(self[$0.. 

O un poco más detallado, pero más general:

 let numbers = ["1","2","3","4","5","6","7"] let chunkSize = 2 let chunks: [[String]] = stride(from: 0, to: numbers.count, by: chunkSize).map { let end = numbers.endIndex let chunkEnd = numbers.index($0, offsetBy: chunkSize, limitedBy: end) ?? end return Array(numbers[$0.. 

Esto es más general porque estoy haciendo menos suposiciones sobre el tipo de índice en la colección. En la implementación anterior asumí que podrían ser comparados y agregados.

Tenga en cuenta que en Swift 3 la funcionalidad de los índices de avance ha sido transferida de los índices a la colección.

Con Swift 4.1, según sus necesidades, puede elegir una de las cinco formas siguientes para resolver su problema.


1. Usando AnyIterator en un método de extensión de Collection

AnyIterator es un buen candidato para iterar sobre los índices de un objeto que se ajusta al protocolo de Collection para devolver subsecuencias de este objeto. En una extensión de protocolo de chunked(by:) , puede declarar un chunked(by:) con la siguiente implementación:

 extension Collection { func chunked(by distance: Int) -> [[Element]] { precondition(distance > 0, "distance must be greater than 0") // prevents infinite loop var index = startIndex let iterator: AnyIterator> = AnyIterator({ let newIndex = self.index(index, offsetBy: distance, limitedBy: self.endIndex) ?? self.endIndex defer { index = newIndex } let range = index ..< newIndex return index != self.endIndex ? Array(self[range]) : nil }) return Array(iterator) } } 

Uso:

 let array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] let newArray = array.chunked(by: 2) print(newArray) // prints: [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]] 

2. Usando la función stride(from:to:by:) en un método de extensión Array

Array índices de Array son del tipo Int y se ajustan al protocolo Strideable . Por lo tanto, puede usar stride(from:to:by:) y advanced(by:) con ellos. En una extensión Array , puede declarar un chunked(by:) con la siguiente implementación:

 extension Array { func chunked(by distance: Int) -> [[Element]] { let indicesSequence = stride(from: startIndex, to: endIndex, by: distance) let array: [[Element]] = indicesSequence.map { let newIndex = $0.advanced(by: distance) > endIndex ? endIndex : $0.advanced(by: distance) //let newIndex = self.index($0, offsetBy: distance, limitedBy: self.endIndex) ?? self.endIndex // also works return Array(self[$0 ..< newIndex]) } return array } } 

Uso:

 let array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] let newArray = array.chunked(by: 2) print(newArray) // prints: [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]] 

3. Usar un enfoque recursivo en un método de extensión Array

Basado en el código recursivo de Nate Cook, puede declarar un chunked(by:) en una extensión Array con la siguiente implementación:

 extension Array { func chunked(by distance: Int) -> [[Element]] { precondition(distance > 0, "distance must be greater than 0") // prevents infinite loop if self.count < = distance { return [self] } else { let head = [Array(self[0 ..< distance])] let tail = Array(self[distance ..< self.count]) return head + tail.chunked(by: distance) } } } 

Uso:

 let array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] let newArray = array.chunked(by: 2) print(newArray) // prints: [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]] 

4. Uso de un bucle for y lotes en un método de extensión de Sequence

Chris Eidhof y Florian Kugler muestran en Swift Talk # 33 - Sequence & Iterator (Collections # 2) video cómo usar un bucle for para llenar lotes de elementos de secuencia y anexarlos al completarlos en una matriz. En una extensión Sequence , puede declarar un chunked(by:) con la siguiente implementación:

 extension Collection { func chunked(by distance: Int) -> [[Element]] { var result: [[Element]] = [] var batch: [Element] = [] for element in self { batch.append(element) if batch.count == distance { result.append(batch) batch = [] } } if !batch.isEmpty { result.append(batch) } return result } } 

Uso:

 let array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] let newArray = array.chunked(by: 2) print(newArray) // prints: [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]] 

5. Usar una struct personalizada que se ajuste a los protocolos Sequence y IteratorProtocol

Si no desea crear extensiones de Sequence , Collection o Array , puede crear una struct personalizada que se ajuste a los protocolos Sequence y IteratorProtocol . Esta struct debe tener la siguiente implementación:

 struct BatchSequence: Sequence, IteratorProtocol { private let array: [T] private let distance: Int private var index = 0 init(array: [T], distance: Int) { precondition(distance > 0, "distance must be greater than 0") // prevents infinite loop self.array = array self.distance = distance } mutating func next() -> [T]? { guard index < array.endIndex else { return nil } let newIndex = index.advanced(by: distance) > array.endIndex ? array.endIndex : index.advanced(by: distance) defer { index = newIndex } return Array(array[index ..< newIndex]) } } 

Uso:

 let array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] let batchSequence = BatchSequence(array: array, distance: 2) let newArray = Array(batchSequence) print(newArray) // prints: [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]] 

Me gusta la respuesta de Nate Cook, parece que Swift ha avanzado desde que se escribió, esta es mi opinión sobre esto como una extensión de Array:

 extension Array { func chunk(chunkSize : Int) -> Array> { return 0.stride(to: self.count, by: chunkSize) .map { Array(self[$0..< $0.advancedBy(chunkSize, limit: self.count)]) } } } 

Tenga en cuenta que devuelve [] para números negativos y dará como resultado un error fatal como se indica arriba. Tendrás que poner un guardia si quieres prevenir eso.

 func testChunkByTwo() { let input = [1,2,3,4,5,6,7] let output = input.chunk(2) let expectedOutput = [[1,2], [3,4], [5,6], [7]] XCTAssertEqual(expectedOutput, output) } func testByOne() { let input = [1,2,3,4,5,6,7] let output = input.chunk(1) let expectedOutput = [[1],[2],[3],[4],[5],[6],[7]] XCTAssertEqual(expectedOutput, output) } func testNegative() { let input = [1,2,3,4,5,6,7] let output = input.chunk(-2) let expectedOutput = [] XCTAssertEqual(expectedOutput, output) } 

No creo que quieras usar mapa o reducir. El mapa es para aplicar una función en cada elemento individual en una matriz, mientras que reducir es para aplanar una matriz. Lo que quiere hacer es dividir la matriz en submapas de cierto tamaño. Este fragmento usa rebanadas.

 var arr = ["1","2","3","4","5","6","7"] var splitSize = 2 var newArr = [[String]]() var i = 0 while i < arr.count { var slice: Slice! if i + splitSize >= arr.count { slice = arr[i.. 

Sería bueno express la formulación de Tyler Cloutier como una extensión en Array:

 extension Array { func chunked(by chunkSize:Int) -> [[Element]] { let groups = stride(from: 0, to: self.count, by: chunkSize).map { Array(self[$0..< [$0 + chunkSize, self.count].min()!]) } return groups } } 

Esto nos da una forma general de dividir una matriz en fragmentos.

Lo de arriba es muy cortante, pero me duele la cabeza. Tuve que volver a un enfoque menos rápido.

Para Swift 2.0

 var chunks = [[Int]]() var temp = [Int]() var splitSize = 3 var x = [1,2,3,4,5,6,7] for (i, element) in x.enumerate() { if temp.count < splitSize { temp.append(element) } if temp.count == splitSize { chunks.append(temp) temp.removeAll() } } if !temp.isEmpty { chunks.append(temp) } 

Playground Result [[1, 2, 3], [4, 5, 6], [7]]

Voy a arrojar mi sombrero en el ring aquí con otra implementación basada en AnyGenerator.

 extension Array { func chunks(_ size: Int) -> AnyIterator< [Element]> { if size == 0 { return AnyIterator { return nil } } let indices = stride(from: startIndex, to: count, by: size) var generator = indices.makeIterator() return AnyIterator { guard let i = generator.next() else { return nil } var j = self.index(i, offsetBy: size) repeat { j = self.index(before: j) } while j >= self.endIndex return self[i...j].lazy.map { $0 } } } } 

Prefiero este método ya que depende exclusivamente de generadores que pueden tener un impacto de memoria positivo no despreciable cuando se trata de matrices de gran tamaño.

Para su ejemplo específico, así es como funcionaría:

 let chunks = Array(["1","2","3","4","5","6","7"].chunks(2)) 

Resultado:

 [["1", "2"], ["3", "4"], ["5", "6"], ["7"]] 

Nuevo en Swift 4, puede hacerlo de manera eficiente con reduce(into:) . Aquí hay una extensión en Sequence:

 extension Sequence { func eachSlice(_ clump:Int) -> [[Self.Element]] { return self.reduce(into:[]) { memo, cur in if memo.count == 0 { return memo.append([cur]) } if memo.last!.count < clump { memo.append(memo.removeLast() + [cur]) } else { memo.append([cur]) } } } } 

Uso:

 let result = [1,2,3,4,5,6,7,8,9].eachSlice(2) // [[1, 2], [3, 4], [5, 6], [7, 8], [9]] 

¿Sabes que cualquier solución con estilo rápido [a … b] funciona 10 veces más despacio que lo normal?

 for y in 0.. 

Pruébalo y verás, aquí está mi código en bruto para la prueba:

 let count = 1000000 let cols = 1000 let rows = count / cols var stream = [Double].init(repeating: 0.5, count: count) // Regular var mat = [[Double]]() let t1 = Date() for y in 0.. 

y fuera:

regular: 0.0449600219726562

rápido: 0.49255496263504