So gruppieren Sie nach den Elementen eines Arrays in Swift

86

Nehmen wir an, ich habe diesen 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] = []

Ich könnte die nächste Funktion so oft manuell aufrufen, um 2 Arrays nach "gleichem Namen" zu gruppieren.

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

Das Problem ist, dass ich den Variablenwert nicht kenne, in diesem Fall "Abendessen" und "Mittagessen". Daher möchte ich dieses Array von statEvents automatisch nach Namen gruppieren, damit ich so viele Arrays erhalte, wie der Name unterschiedlich wird.

Wie könnte ich das machen?

Ruben
quelle
Siehe meine Antwort für Swift 4 , das einen neuen Dictionary init(grouping:by:)Initialisierer verwendet.
Imanou Petit

Antworten:

184

Swift 4:

Seit Swift 4 wurde diese Funktionalität der Standardbibliothek hinzugefügt . Sie können es so verwenden:

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

Leider appendkopiert die obige Funktion das zugrunde liegende Array, anstatt es an Ort und Stelle zu mutieren, was vorzuziehen wäre. Dies führt zu einer ziemlich starken Verlangsamung . Sie können das Problem mithilfe eines Referenztyp-Wrappers umgehen:

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

Obwohl Sie das endgültige Wörterbuch zweimal durchlaufen, ist diese Version in den meisten Fällen immer noch schneller als das Original.

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

In Ihrem Fall könnten die "Schlüssel" wie folgt zurückgegeben keyFuncwerden:

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

Sie erhalten also ein Wörterbuch, in dem jeder Schlüssel ein Name und jeder Wert ein Array der StatEvents mit diesem Namen ist.

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

Welches gibt die Ausgabe:

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

(Der Swiftstub ist hier )

oisdk
quelle
Vielen Dank @oisdk! Wissen Sie, ob es eine Möglichkeit gibt, auf den Index der Werte des erstellten Wörterbuchs zuzugreifen? Ich meine, ich weiß, wie man die Schlüssel und die Werte erhält, aber ich möchte den Index "0", "1", "2" ... dieser Wörterbücher erhalten
Ruben
Wenn Sie also die drei "Abendessen" -Werte in Ihrem Wörterbuch sagen möchten, würden Sie gehen dict[key](in meinem ersten Beispiel wäre es das ans["dinner"]). Wenn Sie die Indizes der drei Dinge selbst haben möchten, wäre dies ungefähr so enumerate(ans["dinner"]), oder wenn Sie über die Indizes zugreifen möchten , können Sie dies wie folgt tun: ans["dinner"]?[0]Dies würde Ihnen das erste Element des Arrays zurückgeben, unter dem gespeichert ist dinner.
oisdk
Ups es gibt mir immer null zurück
Ruben
Oh ja, ich verstehe, aber das Problem ist, dass ich in diesem Beispiel den Wert "Abendessen" kennen soll, aber im realen Code weiß ich weder diese Werte noch wie viele Elemente das Wörterbuch haben werden
Ruben
1
Dies ist ein guter Anfang für eine Lösung, hat aber einige Mängel. Die Verwendung des Mustervergleichs hier ( if case) ist nicht erforderlich, aber was noch wichtiger ist, das Anhängen an ein in einem Wörterbuch gespeichertes Muster dict[key]?.append)bewirkt, dass jedes Mal eine Kopie erfolgt. Siehe rosslebeau.com/2016/…
Alexander - Reinstate Monica
63

Mit Swift 5 Dictionarywird eine Initialisierungsmethode aufgerufen init(grouping:by:). init(grouping:by:)hat die folgende Erklärung:

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

Erstellt ein neues Wörterbuch, in dem die Schlüssel die Gruppierungen sind, die vom angegebenen Abschluss zurückgegeben werden, und die Werte Arrays der Elemente sind, die jeden bestimmten Schlüssel zurückgegeben haben.


Der folgende Spielplatzcode zeigt, wie Sie init(grouping:by:)Ihr Problem lösen können:

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
quelle
4
Gut, könnten Sie auch einschließen, dass es auch geschrieben werden kann als let dictionary = Dictionary(grouping: statEvents) { $0.name }- Syntax Sugar Coating
user1046037
1
Dies sollte die Antwort sein, die mit Swift 4 beginnt - vollständig von Apple unterstützt und hoffentlich sehr leistungsfähig.
Herbal7ea
Achten Sie auch auf den nicht-optinalen Schlüssel, der im Prädikat zurückgegeben wird. Andernfalls wird der Fehler angezeigt: "Der Ausdruckstyp ist ohne weiteren Kontext mehrdeutig ..."
Asike,
30

Swift 4: Sie können init (Gruppierung: nach :) von der Apple-Entwicklerseite verwenden

Beispiel :

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

Also in deinem Fall

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })
Mihuilk
quelle
1
Dies ist bei weitem die beste Antwort, wusste nicht, dass dies existiert, danke;)
RichAppz
Dies funktioniert auch mit einem Schlüsselpfad: let dictionary = Dictionary (Gruppierung: currentStat.statEvents, nach: \ .name)
Jim Haungs
26

Für 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
    }
}

Verwendung:

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
quelle
9
Ein Anwendungsbeispiel wäre sehr dankbar :) Danke!
Centurion
Hier ist ein Verwendungsbeispiel: yourArray.categorise (currentStat.statEvents) {$ 0.name}. Die Funktion gibt Dictionary <String, Array <StatEvents >>
Centurion
6

In Swift 4 bietet diese Erweiterung die beste Leistung und hilft dabei, Ihre Bediener zu verketten

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

Beispiel:

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

erstellt:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
duan
quelle
Können Sie ein Anwendungsbeispiel schreiben?
Utku Dalmaz
@duan ist es möglich, Fall wie BTC zu ignorieren und BTC sollten als gleich gezählt werden ...
Moin Shirazi
1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), aber es ist besser zu kartieren als zu gruppieren
duan
3

Sie können auch KeyPathwie folgt gruppieren :

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

Verwenden des Krypto-Beispiels von @ 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),
]

Dann sieht die Verwendung folgendermaßen aus:

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

Das gleiche Ergebnis erzielen:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
Sajjon
quelle
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)})
Heidegrenzen
quelle
0

Erweiterung der akzeptierten Antwort, um eine geordnete Gruppierung zu ermöglichen :

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

Dann funktioniert es mit jedem Tupel :

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

Sowie jede Struktur oder Klasse :

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

Hier ist mein tupelbasierter Ansatz zur Aufrechterhaltung der Ordnung bei Verwendung von Swift 4 KeyPaths als Gruppenvergleicher:

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

Beispiel für die Verwendung:

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

Hey, wenn Sie beim Gruppieren von Elementen anstelle des Hash-Wörterbuchs die Reihenfolge beibehalten müssen, habe ich Tupel verwendet und beim Gruppieren die Reihenfolge der Liste beibehalten.

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

Das Wörterbuch (Gruppierung: arr) ist so einfach!

 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
quelle
-2

Ein Blatt aus dem Beispiel "oisdk" nehmen . Erweitern der Lösung zum Gruppieren von Objekten basierend auf dem Klassennamen Demo & Quellcode-Link .

Code-Snippet für die Gruppierung basierend auf dem Klassennamen:

 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
quelle