Comment grouper par les éléments d'un tableau dans Swift

89

Disons que j'ai ce code:

class Stat {
   var statEvents : [StatEvents] = []
}

struct StatEvents {
   var name: String
   var date: String
   var hours: Int
}


var currentStat = Stat()

currentStat.statEvents = [
   StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
   StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
   StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
]

var filteredArray1 : [StatEvents] = []
var filteredArray2 : [StatEvents] = []

Je pourrais appeler autant de fois manuellement la fonction suivante afin d'avoir 2 tableaux regroupés par «même nom».

filteredArray1 = currentStat.statEvents.filter({$0.name == "dinner"})
filteredArray2 = currentStat.statEvents.filter({$0.name == "lunch"})

Le problème est que je ne connais pas la valeur de la variable, dans ce cas "dîner" et "déjeuner", je voudrais donc regrouper ce tableau de statEvents automatiquement par nom, donc j'obtiens autant de tableaux que le nom devient différent.

Comment pourrais-je faire ça?

Ruben
la source
Voir ma réponse pour Swift 4 qui utilise un nouvel Dictionary init(grouping:by:)initialiseur.
Imanou Petit

Réponses:

191

Swift 4:

Depuis Swift 4, cette fonctionnalité a été ajoutée à la bibliothèque standard . Vous pouvez l'utiliser comme ceci:

Dictionary(grouping: statEvents, by: { $0.name })
[
  "dinner": [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ],
  "lunch": [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
]

Swift 3:

public extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        var categories: [U: [Iterator.Element]] = [:]
        for element in self {
            let key = key(element)
            if case nil = categories[key]?.append(element) {
                categories[key] = [element]
            }
        }
        return categories
    }
}

Malheureusement, la appendfonction ci-dessus copie le tableau sous-jacent, au lieu de le mettre en place, ce qui serait préférable. Cela provoque un ralentissement assez important . Vous pouvez contourner le problème en utilisant un wrapper de type de référence:

class Box<A> {
  var value: A
  init(_ val: A) {
    self.value = val
  }
}

public extension Sequence {
  func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
    var categories: [U: Box<[Iterator.Element]>] = [:]
    for element in self {
      let key = key(element)
      if case nil = categories[key]?.value.append(element) {
        categories[key] = Box([element])
      }
    }
    var result: [U: [Iterator.Element]] = Dictionary(minimumCapacity: categories.count)
    for (key,val) in categories {
      result[key] = val.value
    }
    return result
  }
}

Même si vous parcourez le dictionnaire final deux fois, cette version est toujours plus rapide que l'original dans la plupart des cas.

Swift 2:

public extension SequenceType {

  /// Categorises elements of self into a dictionary, with the keys given by keyFunc

  func categorise<U : Hashable>(@noescape keyFunc: Generator.Element -> U) -> [U:[Generator.Element]] {
    var dict: [U:[Generator.Element]] = [:]
    for el in self {
      let key = keyFunc(el)
      if case nil = dict[key]?.append(el) { dict[key] = [el] }
    }
    return dict
  }
}

Dans votre cas, vous pourriez avoir les "clés" retournées par keyFuncles noms:

currentStat.statEvents.categorise { $0.name }
[  
  dinner: [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ], lunch: [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
  ]
]

Vous obtiendrez donc un dictionnaire, où chaque clé est un nom, et chaque valeur est un tableau des StatEvents avec ce nom.

Swift 1

func categorise<S : SequenceType, U : Hashable>(seq: S, @noescape keyFunc: S.Generator.Element -> U) -> [U:[S.Generator.Element]] {
  var dict: [U:[S.Generator.Element]] = [:]
  for el in seq {
    let key = keyFunc(el)
    dict[key] = (dict[key] ?? []) + [el]
  }
  return dict
}

categorise(currentStat.statEvents) { $0.name }

Ce qui donne la sortie:

extension StatEvents : Printable {
  var description: String {
    return "\(self.name): \(self.date)"
  }
}
print(categorise(currentStat.statEvents) { $0.name })
[
  dinner: [
    dinner: 01-01-2015,
    dinner: 01-01-2015,
    dinner: 01-01-2015
  ], lunch: [
    lunch: 01-01-2015,
    lunch: 01-01-2015
  ]
]

(Le swiftstub est ici )

oisdk
la source
Merci beaucoup @oisdk! Savez-vous s'il existe un moyen d'accéder à l'index des valeurs du dictionnaire créé? Je veux dire, je sais comment obtenir les clés et les valeurs, mais je voudrais obtenir l'index "0", "1", "2" ... de ces dictionnaires
Ruben
Donc, si vous voulez, dites les trois valeurs "dîner" dans votre dictionnaire, vous irez dict[key], (dans mon premier exemple ce serait ans["dinner"]). Si vous vouliez les indices des trois choses eux-mêmes, ce serait quelque chose comme enumerate(ans["dinner"]), ou, si vous vouliez accéder via les index, vous pourriez le faire comme:, ans["dinner"]?[0]qui vous renverrait le premier élément du tableau stocké sous dinner.
oisdk le
Ups ça me retourne toujours nul
Ruben
Oh oui, je comprends, mais le problème est que dans cet exemple, je suis censé connaître la valeur "dîner", mais dans le vrai code, je ne connais pas ces valeurs ni combien d'articles auront le dictionnaire
Ruben
1
C'est un bon début vers une solution, mais il comporte quelques lacunes. L'utilisation de la correspondance de modèle ici ( if case) n'est pas nécessaire, mais plus important encore, l'ajout à un fichier stocké dans un dictionnaire dict[key]?.append)entraîne une copie à chaque fois. Voir rosslebeau.com/2016/…
Alexander - Réintégrer Monica le
65

Avec Swift 5, Dictionarya une méthode d'initialisation appelée init(grouping:by:). init(grouping:by:)a la déclaration suivante:

init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence

Crée un nouveau dictionnaire où les clés sont les regroupements retournés par la fermeture donnée et les valeurs sont des tableaux des éléments qui ont renvoyé chaque clé spécifique.


Le code Playground suivant montre comment utiliser init(grouping:by:)pour résoudre votre problème:

struct StatEvents: CustomStringConvertible {
    
    let name: String
    let date: String
    let hours: Int
    
    var description: String {
        return "Event: \(name) - \(date) - \(hours)"
    }
    
}

let statEvents = [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
]

let dictionary = Dictionary(grouping: statEvents, by: { (element: StatEvents) in
    return element.name
})
//let dictionary = Dictionary(grouping: statEvents) { $0.name } // also works  
//let dictionary = Dictionary(grouping: statEvents, by: \.name) // also works

print(dictionary)
/*
prints:
[
    "dinner": [Event: dinner - 01-01-2015 - 1, Event: dinner - 01-01-2015 - 1],
    "lunch": [Event: lunch - 01-01-2015 - 1, Event: lunch - 01-01-2015 - 1]
]
*/
Imanou Petit
la source
4
Bon, pourriez-vous également inclure qu'il peut également être écrit comme let dictionary = Dictionary(grouping: statEvents) { $0.name }- Syntaxe Sugar coating
user1046037
1
Cela devrait être la réponse à partir de Swift 4 - entièrement pris en charge par Apple et, espérons-le, très performant.
Herbal7ea
Faites également attention à la clé non optinale retournée dans le prédicat, sinon vous verrez l'erreur: "le type d'expression est ambigu sans plus de contexte ..."
Asike
1
@ user1046037 Swift 5.2Dictionary(grouping: statEvents, by: \.name)
Leo Dabus
31

Swift 4: vous pouvez utiliser init (grouping: by :) à partir du site des développeurs Apple

Exemple :

let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let studentsByLetter = Dictionary(grouping: students, by: { $0.first! })
// ["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]

Donc dans ton cas

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })
Mihuilk
la source
1
C'est de loin la meilleure réponse, je ne savais pas que cela existe merci;)
RichAppz
Cela fonctionne également avec un keypath: let dictionary = Dictionary (grouping: currentStat.statEvents, by: \ .name)
Jim Haungs
26

Pour Swift 3:

public extension Sequence {
    func categorise<U : Hashable>(_ key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        var dict: [U:[Iterator.Element]] = [:]
        for el in self {
            let key = key(el)
            if case nil = dict[key]?.append(el) { dict[key] = [el] }
        }
        return dict
    }
}

Usage:

currentStat.statEvents.categorise { $0.name }
[  
  dinner: [
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1),
    StatEvents(name: "dinner", date: "01-01-2015", hours: 1)
  ], lunch: [
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1),
    StatEvents(name: "lunch", date: "01-01-2015", hours: 1)
  ]
]
Michal Zaborowski
la source
9
Un exemple d'utilisation serait très apprécié :) Merci!
Centurion
Voici un exemple d'utilisation: yourArray.categorise (currentStat.statEvents) {$ 0.name}. La fonction retournera Dictionary <String, Array <StatEvents >>
Centurion
6

Dans Swift 4, cette extension a les meilleures performances et aide à enchaîner vos opérateurs

extension Sequence {
    func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
        return Dictionary.init(grouping: self, by: key)
    }
}

Exemple:

struct Asset {
    let coin: String
    let amount: Int
}

let assets = [
    Asset(coin: "BTC", amount: 12),
    Asset(coin: "ETH", amount: 15),
    Asset(coin: "BTC", amount: 30),
]
let grouped = assets.group(by: { $0.coin })

crée:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
duan
la source
pouvez-vous écrire un exemple d'utilisation?
Utku Dalmaz
@duan est-il possible d'ignorer les cas tels que BTC et btc devraient être comptés comme les mêmes ...
Moin Shirazi
1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), mais il vaut mieux cartographier que le groupe
duan
3

Vous pouvez également grouper par KeyPath, comme ceci:

public extension Sequence {
    func group<Key>(by keyPath: KeyPath<Element, Key>) -> [Key: [Element]] where Key: Hashable {
        return Dictionary(grouping: self, by: {
            $0[keyPath: keyPath]
        })
    }
}

En utilisant l'exemple de crypto de @ duan:

struct Asset {
    let coin: String
    let amount: Int
}

let assets = [
    Asset(coin: "BTC", amount: 12),
    Asset(coin: "ETH", amount: 15),
    Asset(coin: "BTC", amount: 30),
]

Ensuite, l'utilisation ressemble à ceci:

let grouped = assets.group(by: \.coin)

Donnant le même résultat:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
Sajjon
la source
vous pouvez passer un prédicat au lieu du chemin d'accès, func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }cela vous permettrait d'appeler assets.grouped(by: \.coin)ouassets.grouped { $0.coin }
Leo Dabus
2

Swift 4

struct Foo {
  let fizz: String
  let buzz: Int
}

let foos: [Foo] = [Foo(fizz: "a", buzz: 1), 
                   Foo(fizz: "b", buzz: 2), 
                   Foo(fizz: "a", buzz: 3),
                  ]
// use foos.lazy.map instead of foos.map to avoid allocating an
// intermediate Array. We assume the Dictionary simply needs the
// mapped values and not an actual Array
let foosByFizz: [String: Foo] = 
    Dictionary(foos.lazy.map({ ($0.fizz, $0)}, 
               uniquingKeysWith: { (lhs: Foo, rhs: Foo) in
                   // Arbitrary business logic to pick a Foo from
                   // two that have duplicate fizz-es
                   return lhs.buzz > rhs.buzz ? lhs : rhs
               })
// We don't need a uniquing closure for buzz because we know our buzzes are unique
let foosByBuzz: [String: Foo] = 
    Dictionary(uniqueKeysWithValues: foos.lazy.map({ ($0.buzz, $0)})
Frontières de la santé
la source
0

Extension de la réponse acceptée pour permettre le regroupement ordonné :

extension Sequence {
    func group<GroupingType: Hashable>(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] {
        var groups: [GroupingType: [Iterator.Element]] = [:]
        var groupsOrder: [GroupingType] = []
        forEach { element in
            let key = key(element)
            if case nil = groups[key]?.append(element) {
                groups[key] = [element]
                groupsOrder.append(key)
            }
        }
        return groupsOrder.map { groups[$0]! }
    }
}

Ensuite, cela fonctionnera sur n'importe quel tuple :

let a = [(grouping: 10, content: "a"),
         (grouping: 20, content: "b"),
         (grouping: 10, content: "c")]
print(a.group { $0.grouping })

Ainsi que toute structure ou classe :

struct GroupInt {
    var grouping: Int
    var content: String
}
let b = [GroupInt(grouping: 10, content: "a"),
         GroupInt(grouping: 20, content: "b"),
         GroupInt(grouping: 10, content: "c")]
print(b.group { $0.grouping })
Cœur
la source
0

Voici mon approche basée sur les tuple pour garder l'ordre tout en utilisant Swift 4 KeyPath comme comparateur de groupe:

extension Sequence{

    func group<T:Comparable>(by:KeyPath<Element,T>) -> [(key:T,values:[Element])]{

        return self.reduce([]){(accumulator, element) in

            var accumulator = accumulator
            var result :(key:T,values:[Element]) = accumulator.first(where:{ $0.key == element[keyPath:by]}) ?? (key: element[keyPath:by], values:[])
            result.values.append(element)
            if let index = accumulator.index(where: { $0.key == element[keyPath: by]}){
                accumulator.remove(at: index)
            }
            accumulator.append(result)

            return accumulator
        }
    }
}

Exemple d'utilisation:

struct Company{
    let name : String
    let type : String
}

struct Employee{
    let name : String
    let surname : String
    let company: Company
}

let employees : [Employee] = [...]
let companies : [Company] = [...]

employees.group(by: \Employee.company.type) // or
employees.group(by: \Employee.surname) // or
companies.group(by: \Company.type)
Zell B.
la source
0

Hé, si vous devez garder l'ordre tout en regroupant des éléments au lieu du dictionnaire de hachage, j'ai utilisé des tuples et conservé l'ordre de la liste lors du regroupement.

extension Sequence
{
   func zmGroup<U : Hashable>(by: (Element) -> U) -> [(U,[Element])]
   {
       var groupCategorized: [(U,[Element])] = []
       for item in self {
           let groupKey = by(item)
           guard let index = groupCategorized.index(where: { $0.0 == groupKey }) else { groupCategorized.append((groupKey, [item])); continue }
           groupCategorized[index].1.append(item)
       }
       return groupCategorized
   }
}
Suat KARAKUSOGLU
la source
0

Le dictionnaire Thr (regroupement: arr) est si simple!

 func groupArr(arr: [PendingCamera]) {

    let groupDic = Dictionary(grouping: arr) { (pendingCamera) -> DateComponents in
        print("group arr: \(String(describing: pendingCamera.date))")

        let date = Calendar.current.dateComponents([.day, .year, .month], from: (pendingCamera.date)!)

        return date
    }

    var cams = [[PendingCamera]]()

    groupDic.keys.forEach { (key) in
        print(key)
        let values = groupDic[key]
        print(values ?? "")

        cams.append(values ?? [])
    }
    print(" cams are \(cams)")

    self.groupdArr = cams
}
ferRoei
la source
-2

Prenant une feuille de l' exemple "oisdk" . Extension de la solution pour regrouper des objets en fonction du nom de la classe Lien Démo et code source .

Extrait de code pour le regroupement basé sur le nom de la classe:

 func categorise<S : SequenceType>(seq: S) -> [String:[S.Generator.Element]] {
    var dict: [String:[S.Generator.Element]] = [:]
    for el in seq {
        //Assigning Class Name as Key
        let key = String(el).componentsSeparatedByString(".").last!
        //Generating a dictionary based on key-- Class Names
        dict[key] = (dict[key] ?? []) + [el]
    }
    return dict
}
//Grouping the Objects in Array using categorise
let categorised = categorise(currentStat)
print("Grouped Array :: \(categorised)")

//Key from the Array i.e, 0 here is Statt class type
let key_Statt:String = String(currentStat.objectAtIndex(0)).componentsSeparatedByString(".").last!
print("Search Key :: \(key_Statt)")

//Accessing Grouped Object using above class type key
let arr_Statt = categorised[key_Statt]
print("Array Retrieved:: ",arr_Statt)
print("Full Dump of Array::")
dump(arr_Statt)
Abhijeet
la source