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

89

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 filteredArray1 : [StatEvents] = []
var filteredArray2 : [StatEvents] = []

Podría llamar tantas veces manualmente a la siguiente función para tener 2 matrices agrupadas por "mismo nombre".

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

El problema es que no sabré el valor de la variable, en este caso "cena" y "almuerzo", así que me gustaría agrupar esta matriz de statEvents automáticamente por nombre, de modo que obtengo tantas matrices como el nombre sea diferente.

¿Cómo puedo hacer eso?

Ruben
fuente
Vea mi respuesta para Swift 4 que usa un nuevo Dictionary init(grouping:by:)inicializador.
Imanou Petit

Respuestas:

191

Rápido 4:

Desde Swift 4, esta funcionalidad se ha agregado a la biblioteca estándar . Puedes usarlo así:

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

Desafortunadamente, la appendfunción anterior copia la matriz subyacente, en lugar de mutarla en su lugar, lo que sería preferible. Esto provoca una gran desaceleración . Puede solucionar el problema utilizando un contenedor de tipo de referencia:

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 recorra el diccionario final dos veces, esta versión sigue siendo más rápida que la original en la mayoría de los casos.

Rápido 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 "claves" devueltas por keyFunclos 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 diccionario, donde cada clave es un nombre y cada valor es una matriz de StatEvents con ese nombre.

Rápido 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 }

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

oisdk
fuente
¡Muchas gracias @oisdk! ¿Sabes si existe alguna forma de acceder al índice de los valores del diccionario que se crea? Quiero decir, sé cómo obtener las claves y los valores, pero me gustaría obtener el índice "0", "1", "2" ... de esos diccionarios
Ruben
Entonces, si lo desea, diga los tres valores de "cena" en su diccionario, iría dict[key](en mi primer ejemplo sería ans["dinner"]). Si quisiera los índices de las tres cosas en sí, sería algo como enumerate(ans["dinner"]), o, si quisiera acceder a través de los índices, podría hacerlo como:, lo ans["dinner"]?[0]que le devolvería el primer elemento de la matriz almacenada debajo dinner.
oisdk
Ups, siempre me devuelve cero
Ruben
Oh, sí, lo entiendo, pero el problema es que en este ejemplo se supone que debo conocer el valor "cena", pero en el código real no sabré estos valores ni cuántos elementos tendrá el diccionario
Ruben
1
Este es un buen comienzo hacia una solución, pero tiene algunas deficiencias. El uso de la coincidencia de patrones aquí ( if case) es innecesario, pero lo que es más importante, agregar un archivo almacenado dentro de un diccionario dict[key]?.append)hace que se produzca una copia cada vez. Ver rosslebeau.com/2016/…
Alexander - Reinstate Monica
65

Con Swift 5, Dictionarytiene un método de inicialización llamado init(grouping:by:). init(grouping:by:)tiene la siguiente declaración:

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

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


El siguiente código de Playground muestra cómo utilizarlo 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 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
fuente
4
Buena, ¿podría incluir también que también se puede escribir como let dictionary = Dictionary(grouping: statEvents) { $0.name }- Recubrimiento de azúcar de sintaxis
user1046037
1
Esta debería ser la respuesta a partir de Swift 4: totalmente compatible con Apple y, con suerte, de alto rendimiento.
Herbal7ea
También preste atención a la clave no optinal devuelta en el predicado; de lo contrario, verá el error: "el tipo de expresión es ambigua sin más contexto ..."
Asike
1
@ user1046037 Swift 5.2Dictionary(grouping: statEvents, by: \.name)
Leo Dabus
31

Swift 4: puede usar init (agrupamiento: por :) desde el sitio de desarrolladores de Apple

Ejemplo :

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

Entonces en tu caso

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })
Mihuilk
fuente
1
Esta es la mejor respuesta de lejos, no sabía que esto existe gracias;)
RichAppz
Esto también funciona con una ruta de acceso clave: let dictionary = Dictionary (agrupación: currentStat.statEvents, por: \ .name)
Jim Haungs
26

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)
  ]
]
Michal Zaborowski
fuente
9
Un ejemplo de uso sería muy apreciado :) ¡Gracias!
Centurion
Aquí hay un ejemplo de uso: yourArray.categorise (currentStat.statEvents) {$ 0.name}. La función devolverá Dictionary <String, Array <StatEvents >>
Centurion
6

En Swift 4, esta extensión tiene el mejor rendimiento y ayuda a encadenar a sus operadores

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

Ejemplo:

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

crea:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
duan
fuente
¿Puede escribir un ejemplo de uso?
Utku Dalmaz
@duan es posible ignorar casos como BTC y btc deben contarse como lo mismo ...
Moin Shirazi
1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), pero es mejor mapear que agrupar
duan
3

También puede agrupar por KeyPath, así:

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

Usando el ejemplo de cifrado 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),
]

Entonces el uso se ve así:

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

Dando el mismo resultado:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
Sajjon
fuente
puede pasar un predicado en lugar de la ruta de acceso clave, func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }esto le permitiría llamar assets.grouped(by: \.coin)oassets.grouped { $0.coin }
Leo Dabus
2

Rápido 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)})
Heath Borders
fuente
0

Ampliación de la respuesta aceptada para permitir la agrupación ordenada :

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

Así como cualquier estructura o clase :

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
fuente
0

Aquí está mi enfoque basado en tuplas para mantener el orden mientras uso Swift 4 KeyPath como comparador de grupo:

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

Ejemplo de cómo usarlo:

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.
fuente
0

Oye, si necesitas mantener el orden al agrupar elementos en lugar del diccionario hash, he usado tuplas y mantuve el orden de la lista al agrupar.

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
fuente
0

¡Thr Dictionary (agrupación: arr) es tan fácil!

 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
}
ironRoei
fuente
-2

Tomando una hoja del ejemplo "oisdk" . Ampliación de la solución para agrupar objetos según el nombre de la clase Demo y enlace de código fuente .

Fragmento de código para agrupar según el nombre de la clase:

 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
fuente