Jak grupować według elementów tablicy w języku Swift

89

Powiedzmy, że mam ten kod:

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

Mógłbym tyle razy ręcznie wywołać następną funkcję, aby mieć 2 tablice pogrupowane według „tej samej nazwy”.

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

Problem w tym, że nie znam wartości zmiennej, w tym przypadku „obiad” i „obiad”, dlatego chciałbym pogrupować tę tablicę statEvents automatycznie według nazwy, aby uzyskać tyle tablic, ile różni się nazwa.

Jak mogłem to zrobić?

Ruben
źródło
Zobacz moją odpowiedź dla Swift 4, który używa nowego Dictionary init(grouping:by:)inicjatora.
Imanou Petit

Odpowiedzi:

191

Swift 4:

Od wersji Swift 4 ta funkcja została dodana do biblioteki standardowej . Możesz go używać w ten sposób:

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

Niestety, appendpowyższa funkcja kopiuje podstawową tablicę, zamiast ją mutować, co byłoby lepsze. To powoduje dość duże spowolnienie . Możesz obejść ten problem, używając opakowania typu referencyjnego:

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

Nawet jeśli dwukrotnie przeglądasz ostateczny słownik, ta wersja jest w większości przypadków szybsza od oryginału.

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

W twoim przypadku "klucze" mogą keyFuncbyć zwracane przez nazwy:

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

Otrzymasz więc słownik, w którym każdy klucz jest nazwą, a każda wartość jest tablicą zdarzeń StatEvents o tej nazwie.

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

Co daje wynik:

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

(Swiftstub jest tutaj )

oisdk
źródło
Dziękuję bardzo @oisdk! Czy wiesz, czy istnieje sposób na dostęp do indeksu wartości tworzonego słownika? To znaczy wiem, jak zdobyć klucze i wartości, ale chciałbym uzyskać indeks „0”, „1”, „2” ... tych słowników
Ruben
Więc jeśli chcesz, powiedz trzy wartości „kolacja” w swoim słowniku, możesz to zrobić dict[key](w moim pierwszym przykładzie byłoby to ans["dinner"]). Jeśli chciałbyś mieć same indeksy trzech rzeczy, byłoby to coś w stylu enumerate(ans["dinner"]), lub, gdybyś chciał uzyskać dostęp przez indeksy, mógłbyś to zrobić w ten sposób:, ans["dinner"]?[0]co zwróci ci pierwszy element tablicy przechowywanej pod dinner.
oisdk
Ups zawsze zwraca mi zero
Ruben
O tak, rozumiem, ale problem w tym, że w tym przykładzie mam znać wartość „obiad”, ale w prawdziwym kodzie nie będę wiedział ani tych wartości, ani ile pozycji będzie miało słownik
Ruben
1
To dobry początek w kierunku rozwiązania, ale ma kilka niedociągnięć. Użycie dopasowania wzorców w tym miejscu ( if case) jest niepotrzebne, ale co ważniejsze, dołączanie do pliku przechowywanego w słowniku dict[key]?.append)powoduje, że kopia występuje za każdym razem. Zobacz rosslebeau.com/2016/…
Alexander - Przywróć Monikę
65

W przypadku Swift 5 Dictionaryma metodę inicjowania o nazwie init(grouping:by:). init(grouping:by:)posiada następującą deklarację:

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

Tworzy nowy słownik, w którym klucze są grupami zwracanymi przez dane zamknięcie, a wartościami są tablice elementów, które zwróciły każdy określony klucz.


Poniższy kod Playground pokazuje, jak używać init(grouping:by:), aby rozwiązać problem:

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
źródło
4
Dobry, czy mógłbyś również dodać, że można go również zapisać jako let dictionary = Dictionary(grouping: statEvents) { $0.name }- Składnia Cukrowa powłoka
user1046037
1
To powinna być odpowiedź, zaczynając szybko 4 - w pełni obsługiwany przez Apple i miejmy nadzieję, że bardzo wydajny.
Herbal7ea
Zwróć także uwagę na klucz nieoptynalny zwrócony w predykacie, w przeciwnym razie zobaczysz błąd: „typ wyrażenia jest niejednoznaczny bez większego kontekstu ...”
Asike
1
@ user1046037 Swift 5.2Dictionary(grouping: statEvents, by: \.name)
Leo Dabus
31

Swift 4: możesz użyć init (grupowanie: według :) ze strony programistów Apple

Przykład :

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

Więc w twoim przypadku

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })
Mihuilk
źródło
1
To zdecydowanie najlepsza odpowiedź, nie wiedziałem, że istnieje dzięki;)
RichAppz
Działa to również z keypath: let Dictionary = Dictionary (grupowanie: currentStat.statEvents, według: \ .name)
Jim Haungs
26

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

Stosowanie:

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)
  ]
]
Michał Zaborowski
źródło
9
Przykład użycia byłby bardzo mile widziany :) Dzięki!
Centurion
Oto przykład użycia: yourArray.categorise (currentStat.statEvents) {$ 0.name}. Funkcja zwróci Dictionary <String, Array <StatEvents >>
Centurion
6

W Swift 4 to rozszerzenie ma najlepszą wydajność i pomaga połączyć operatorów

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

Przykład:

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

tworzy:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
duan
źródło
czy możesz napisać przykład użycia?
Utku Dalmaz
@duan czy można zignorować przypadki, takie jak BTC i btc, należy liczyć jako takie same ...
Moin Shirazi
1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), ale lepiej jest mapować niż grupować
duan
3

Możesz także grupować według KeyPath:

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

Na przykładzie kryptowalut @ 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),
]

Wtedy użycie wygląda następująco:

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

Dając ten sam wynik:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
Sajjon
źródło
możesz podać predykat zamiast func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }assets.grouped(by: \.coin)assets.grouped { $0.coin }
ścieżki klucza,
2

Szybki 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
źródło
0

Rozszerzenie po zaakceptowanej odpowiedzi, aby umożliwić uporządkowane grupowanie:

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

Wtedy zadziała na dowolnej krotce :

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

Jak również każda struktura lub klasa :

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
źródło
0

Oto moje podejście oparte na krotkach do utrzymywania porządku przy użyciu Swift 4 KeyPath jako komparatora grupowego:

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

Przykład użycia:

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.
źródło
0

Hej, jeśli chcesz zachować porządek podczas grupowania elementów zamiast słownika skrótów, użyłem krotek i zachowałem kolejność na liście podczas grupowania.

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
źródło
0

Thr Dictionary (grupowanie: arr) jest takie proste!

 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
źródło
-2

Wyjęcie liścia z przykładu „oisdk” . Rozszerzenie rozwiązania o grupowanie obiektów na podstawie nazwy klasy Demo i łącze do kodu źródłowego .

Fragment kodu do grupowania na podstawie nazwy klasy:

 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
źródło