web-dev-qa-db-fra.com

Swift: quelle est la bonne façon de diviser une [chaîne] donnant une [[chaîne]] avec une taille de sous-matrice donnée?

En commençant par un grand [String] et une taille de sous-tableau donnée, quelle est la meilleure façon de diviser ce tableau en tableaux plus petits? (Le dernier tableau sera plus petit que la taille de sous-tableau donnée).

Exemple concret:

Fractionner ["1", "2", "3", "4", "5", "6", "7"] avec la taille de fractionnement maximale 2

Le code produirait [["1", "2"], ["3", "4"], ["5", "6"], ["7"]

Évidemment, je pourrais le faire un peu plus manuellement, mais je sens que quelque chose comme map () ou reduction () peut faire ce que je veux vraiment.

36
Jordan Smith

Je n'appellerais pas cela beau, mais voici une méthode utilisant 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)]
}

La méthode stride(to:by:) vous donne les indices pour le premier élément de chaque bloc. Vous pouvez ainsi les mapper sur une tranche du tableau source à l'aide de advancedBy(distance:limit:).

Une approche plus "fonctionnelle" serait simplement de récidiver sur le tableau, comme ceci:

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

Dans Swift 3/4, ceci ressemblerait à ceci:

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..<min($0 + chunkSize, numbers.count)])
}
// prints as [["1", "2"], ["3", "4"], ["5", "6"], ["7"]]

En tant qu'extension de Array:

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

Ou le légèrement plus verbeux, mais plus général:

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..<chunkEnd])
}

Ceci est plus général car je fais moins de suppositions sur le type de l'index dans la collection. Dans l’implémentation précédente, j’imaginais qu’ils pouvaient être comparés et ajoutés.

Notez que dans Swift 3, la fonctionnalité de progression des index a été transférée des index eux-mêmes à la collection.

46
Tyler Cloutier

Avec Swift 4.2 et Swift 5, selon vos besoins, vous pouvez choisir l’un des cinq moyens suivants pour résoudre votre problème.


1. Utilisation de AnyIterator dans une méthode d'extension Collection

AnyIterator est un bon candidat pour parcourir les index d'un objet conforme au protocole Collection afin de renvoyer des sous-séquences de cet objet. Dans une extension de protocole Collection, vous pouvez déclarer une méthode chunked(by:) avec l'implémentation suivante:

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<Array<Element>> = 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)
    }

}

Usage:

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. Utilisation de la fonction stride(from:to:by:) dans une méthode d'extension Array

Les indices Array sont de type Int et sont conformes au protocole Strideable. Par conséquent, vous pouvez utiliser stride(from:to:by:) et advanced(by:) avec eux. Dans une extension Array, vous pouvez déclarer une méthode chunked(by:) avec l'implémentation suivante:

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
    }

}

Usage:

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. Utilisation d'une approche récursive dans une méthode d'extension Array

Sur la base de Nate Cook code récursif , vous pouvez déclarer une méthode chunked(by:) dans une extension Array avec l'implémentation suivante:

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)
        }
    }

}

Usage:

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. Utilisation d'une boucle for et de lots dans une méthode d'extension Collection

Chris Eidhof et Florian Kugler montrent dans Swift Talk n ° 33 - Séquence et itérateur (Collections n ° 2) video comment utiliser une simple boucle for pour remplir des lots d'éléments de séquence et les ajouter ensuite à un tableau. Dans une extension Sequence, vous pouvez déclarer une méthode chunked(by:) avec l'implémentation suivante:

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
    }

}

Usage:

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. Utilisation d'une struct personnalisée conforme aux protocoles Sequence et IteratorProtocol

Si vous ne souhaitez pas créer d'extensions de Sequence, Collection ou Array, vous pouvez créer une struct personnalisée conforme aux protocoles Sequence et IteratorProtocol. Cette struct devrait avoir l'implémentation suivante:

struct BatchSequence<T>: 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])
    }

}

Usage:

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"]]
32
Imanou Petit

J'aime la réponse de Nate Cook, on dirait que Swift a évolué depuis sa rédaction, voici mon interprétation de ceci comme une extension de Array:

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

Remarque, il retourne [] pour les nombres négatifs et entraînera une erreur fatale comme écrit ci-dessus. Vous devrez mettre un garde si vous voulez empêcher cela.

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)
}
8
Julian

Je ne pense pas que vous voudrez utiliser la carte ou réduire. La carte sert à appliquer une fonction à chaque élément individuel d'un tableau, tandis que réduire sert à aplatir un tableau. Ce que vous voulez faire, c'est diviser le tableau en sous-tableaux d'une certaine taille. Cet extrait utilise des tranches.

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<String>!
    if i + splitSize >= arr.count {
        slice = arr[i..<arr.count]
    }
    else {
        slice = arr[i..<i+splitSize]
    }
    newArr.append(Array(slice))
    i += slice.count
}
println(newArr)
4
Connor

Serait agréable à exprimer la formulation de Tyler Cloutier en tant qu'extension sur 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
    }
}

Cela nous donne un moyen général de partitionner un tableau en morceaux.

3
matt

Nouveau dans Swift 4, vous pouvez le faire efficacement avec reduce(into:). Voici une extension sur la séquence:

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])
            }
        }
    }
}

Usage:

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

Je vais juste lancer mon chapeau ici avec une autre implémentation basée sur 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 }
        }
    }
}

Je préfère cette méthode car elle s’appuie exclusivement sur des générateurs pouvant avoir un impact positif non négligeable sur la mémoire lorsqu’il s’agit de tableaux de grande taille.

Pour votre exemple spécifique, voici comment cela fonctionnerait:

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

Résultat:

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

Ce qui précède est très coupé, mais ma tête me fait mal. J'ai dû revenir à une approche moins radicale.

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

2
DogCoffee

Savez-vous qu’une solution avec [a ... b] le style Swift fonctionne 10 fois plus lentement que la normale?

for y in 0..<rows {
    var row = [Double]()
    for x in 0..<cols {
        row.append(stream[y * cols + x])
    }
    mat.append(row)
}

Essayez et vous verrez, voici mon code brut pour le test:

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..<rows {
    var row = [Double]()
    for x in 0..<cols {
        row.append(stream[y * cols + x])
    }
    mat.append(row)
}

print("regular: \(Date().timeIntervalSince(t1))")


//Swift
let t2 = Date()

var mat2: [[Double]] = stride(from: 0, to: stream.count, by: cols).map {
    let end = stream.endIndex
    let chunkEnd = stream.index($0, offsetBy: cols, limitedBy: end) ?? end
    return Array(stream[$0..<chunkEnd])
}

print("Swift: \(Date().timeIntervalSince(t2))")

et dehors:

normal: 0.0449600219726562

Swift: 0.49255496263504

Dans Swift 4 ou version ultérieure, vous pouvez également étendre Collection et renvoyer une collection de SubSequence afin de pouvoir l'utiliser également avec une String. De cette façon, il retournera une collection de sous-chaînes au lieu d'une collection de plusieurs personnages:

Xcode 10.1 • Swift 4.2.1 ou version ultérieure

extension Collection {
    func subSequences(limitedTo maxLength: Int) -> [SubSequence] {
        precondition(maxLength > 0, "groups must be greater than zero")
        var start = startIndex
        return stride(from: 0, to: count, by: maxLength).map { _ in
            let end = index(start, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex
            defer { start = end }
            return self[start..<end]
        }
    }
}

Usage

let array = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
let slices = array.subSequences(limitedTo: 2)  // [ArraySlice(["1", "2"]), ArraySlice(["3", "4"]), ArraySlice(["5", "6"]), ArraySlice(["7", "8"]), ArraySlice(["9"])]
for slice in slices {
    print(slice) // prints: [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]]
}
// To convert from ArraySlice<Element> to Array<element>
let arrays = slices.map(Array.init)  // [["1", "2"], ["3", "4"], ["5", "6"], ["7", "8"], ["9"]]


extension Collection {
    var singles: [SubSequence] { return subSequences(limitedTo: 1) }
    var pairs:   [SubSequence] { return subSequences(limitedTo: 2) }
    var triples: [SubSequence] { return subSequences(limitedTo: 3) }
    var quads:   [SubSequence] { return subSequences(limitedTo: 4) }
}

Tableau ou tableau de caractères 

let chars = ["a","b","c","d","e","f","g","h","i"]
chars.singles  // [["a"], ["b"], ["c"], ["d"], ["e"], ["f"], ["g"], ["h"], ["i"]]
chars.pairs    // [["a", "b"], ["c", "d"], ["e", "f"], ["g", "h"], ["i"]]
chars.triples  // [["a", "b", "c"], ["d", "e", "f"], ["g", "h", "i"]]
chars.quads    // [["a", "b", "c", "d"], ["e", "f", "g", "h"], ["i"]]
chars.dropFirst(2).quads  // [["c", "d", "e", "f"], ["g", "h", "i"]]

Éléments StringProtocol (String et SubString)

let str = "abcdefghi"
str.singles  // ["a", "b", "c", "d", "e", "f", "g", "h", "i"]
str.pairs    // ["ab", "cd", "ef", "gh", "i"]
str.triples  // ["abc", "def", "ghi"]
str.quads    // ["abcd", "efgh", "i"]
str.dropFirst(2).quads    // ["cdef", "ghi"]
0
Leo Dabus