Cómo agrupar por los elementos de una matriz en Swift

Digamos que tengo este código:

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 filtenetworkingArray1 : [StatEvents] = [] var filtenetworkingArray2 : [StatEvents] = [] 

Podría llamar tantas veces manualmente la siguiente function para tener 2 arrays agrupados por el "mismo nombre".

 filtenetworkingArray1 = currentStat.statEvents.filter({$0.name == "dinner"}) filtenetworkingArray2 = currentStat.statEvents.filter({$0.name == "lunch"}) 

El problema es que no conoceré el valor de la variable, en este caso "cena" y "almuerzo", por lo que me gustaría agrupar este set de events estadísticos automáticamente por nombre, así que obtengo tantos arreglos como el nombre se vuelve diferente.

¿Cómo podría hacer eso?

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

Desafortunadamente, la function de append arriba copy la matriz subyacente, en lugar de mutarla en su lugar, lo que sería preferible. Esto causa una gran desaceleración . Puede solucionar el problema utilizando un contenedor de tipo de reference:

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

Aunque atravieses el dictionary final dos veces, esta versión es aún más rápida que la original en la mayoría de los casos.

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

En su caso, podría tener las "keys" devueltas por keyFunc los nombres:

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

Entonces obtendrá un dictionary, donde cada key es un nombre, y cada valor es una matriz de StatEvents con ese nombre.

La versión Swift 1 sería:

 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 } 

Lo que da la salida:

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

(El swiftstub está aquí )

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

Uso:

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

Con Swift 4, Dictionary tiene un método de initialization llamado init(grouping:by:) . init(grouping:by:) tiene la siguiente statement:

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

Crea un nuevo dictionary donde las keys son las agrupaciones devueltas por el cierre dado y los valores son matrices de los elementos que devolvieron cada key específica.


El siguiente código de Zona de juegos muestra cómo usar init(grouping:by:) para resolver su problema:

 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 pnetworkingicate = { (element: StatEvents) in return element.name } let dictionary = Dictionary(grouping: statEvents, by: pnetworkingicate) 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] ] */ 

Extender la respuesta aceptada para permitir la agrupación orderada :

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

Entonces funcionará en cualquier tupla :

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

Además de cualquier estructura o class :

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

Oye si necesitas mantener el order mientras agrupas elementos en vez de dictionary hash he usado tuples y mantuviste el order de la list mientras agrupas.

 extension Sequence { func zmGroup<U : Hashable>(by: (Element) -> U) -> [(U,[Element])] { var groupCategorized: [(U,[Element])] = [] for item in self { let groupKey = by(item) if var foundGroup = groupCategorized.filter({ $0.0 == groupKey }).first{ foundGroup.1.append(item) }else{ groupCategorized.append((groupKey, [item])) } } return groupCategorized } } 

Tomando una hoja del ejemplo "oisdk" . Extender la solución a los objects grupales según el nombre de la class Enlace Demo y Código fuente .

Fragmento de código para agrupación basado en Nombre de class:

  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 ie, 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)