Cara mengelompokkan berdasarkan elemen array di Swift

90

Katakanlah saya memiliki kode ini:

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

Saya bisa memanggil sebanyak mungkin fungsi berikutnya secara manual agar memiliki 2 array yang dikelompokkan dengan "nama yang sama".

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

Masalahnya adalah saya tidak akan tahu nilai variabelnya, dalam hal ini "makan malam" dan "makan siang", jadi saya ingin mengelompokkan array statEvents ini secara otomatis berdasarkan nama, jadi saya mendapatkan array sebanyak namanya berbeda.

Bagaimana saya bisa melakukan itu?

Ruben
sumber
Lihat jawaban saya untuk Swift 4 yang menggunakan Dictionary init(grouping:by:)penginisialisasi baru .
Imanou Petit

Jawaban:

196

Cepat 4:

Sejak Swift 4, fungsionalitas ini telah ditambahkan ke pustaka standar . Anda dapat menggunakannya seperti ini:

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

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

Sayangnya, appendfungsi di atas menyalin larik yang mendasarinya, alih-alih memutasinya di tempatnya, yang lebih disukai. Ini menyebabkan perlambatan yang cukup besar . Anda bisa mengatasi masalah ini dengan menggunakan pembungkus tipe referensi:

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

Meskipun Anda melintasi kamus terakhir dua kali, versi ini masih lebih cepat daripada aslinya dalam banyak kasus.

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

Dalam kasus Anda, Anda dapat memiliki "kunci" yang dikembalikan dengan keyFuncnama:

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

Jadi Anda akan mendapatkan kamus, di mana setiap kunci adalah nama, dan setiap nilai adalah larik StatEvents dengan nama itu.

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

Yang memberikan output:

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 ada di sini )

oisdk
sumber
Terima kasih banyak @oisdk! Apakah Anda tahu jika ada cara untuk mengakses indeks nilai kamus yang dibuat? Maksud saya, saya tahu cara mendapatkan kunci dan nilainya, tetapi saya ingin mendapatkan indeks "0", "1", "2" ... dari kamus-kamus itu
Ruben
Jadi jika Anda ingin, ucapkan tiga nilai "makan malam" dalam kamus Anda, Anda harus memilih dict[key], (dalam contoh pertama saya, itu adalah ans["dinner"]). Jika Anda menginginkan indeks dari tiga hal itu sendiri, itu akan menjadi seperti enumerate(ans["dinner"]), atau, jika Anda ingin mengakses melalui indeks, Anda dapat melakukannya seperti:, ans["dinner"]?[0]yang akan mengembalikan Anda elemen pertama dari array yang disimpan di bawah dinner.
oisdk
Ups itu selalu mengembalikan saya nihil
Ruben
Oh ya saya mengerti, tetapi masalahnya adalah bahwa dalam contoh ini saya seharusnya mengetahui nilai "makan malam", tetapi dalam kode sebenarnya saya tidak akan tahu nilai ini atau berapa banyak item yang akan memiliki kamus
Ruben
1
Ini adalah awal yang baik menuju solusi, tetapi memiliki beberapa kekurangan. Penggunaan pencocokan pola di sini ( if case) tidak diperlukan, tetapi yang lebih penting, menambahkan ke yang disimpan dalam kamus dengan dict[key]?.append)menyebabkan salinan terjadi setiap saat. Lihat rosslebeau.com/2016/…
Alexander
65

Dengan Swift 5, Dictionarymemiliki metode penginisialisasi yang disebut init(grouping:by:). init(grouping:by:)memiliki deklarasi berikut:

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

Membuat kamus baru di mana kuncinya adalah pengelompokan yang dikembalikan oleh closure yang diberikan dan nilainya adalah array dari elemen yang mengembalikan setiap kunci tertentu.


Kode Playground berikut menunjukkan cara menggunakan init(grouping:by:)untuk memecahkan masalah Anda:

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
sumber
4
Bagus, dapatkah Anda juga memasukkan bahwa itu juga dapat ditulis sebagai let dictionary = Dictionary(grouping: statEvents) { $0.name }- Lapisan Sintaks Gula
pengguna1046037
1
Ini harus menjadi jawaban mulai cepat 4 - didukung sepenuhnya oleh apel, dan semoga berkinerja tinggi.
Herbal7ea
Perhatikan juga kunci non-optinal yang dikembalikan dalam predikat, jika tidak, Anda akan melihat error: "jenis ekspresi ambigu tanpa konteks lebih ..."
Asike
1
@ user1046037 Swift 5.2Dictionary(grouping: statEvents, by: \.name)
Leo Dabus
31

Swift 4: Anda dapat menggunakan init (pengelompokan: menurut :) dari situs pengembang apple

Contoh :

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

Jadi dalam kasus Anda

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })
Mihuilk
sumber
1
Ini adalah jawaban terbaik sejauh ini, tidak tahu ini ada terima kasih;)
RichAppz
Ini juga bekerja dengan keypath: let dictionary = Dictionary (pengelompokan: currentStat.statEvents, oleh: \ .name)
Jim Haungs
26

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

Pemakaian:

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
sumber
9
Contoh penggunaan akan sangat dihargai :) Terima kasih!
Centurion
Berikut adalah contoh penggunaan: yourArray.categorise (currentStat.statEvents) {$ 0.name}. Fungsi ini akan mengembalikan Dictionary <String, Array <StatEvents >>
Centurion
6

Di Swift 4, ekstensi ini memiliki kinerja terbaik dan membantu menghubungkan operator Anda

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

Contoh:

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

menciptakan:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
duan
sumber
dapatkah Anda menulis contoh penggunaan?
Utku Dalmaz
@duan apakah mungkin untuk mengabaikan kasus seperti BTC dan btc harus dihitung sama ...
Moin Shirazi
1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), tetapi 'lebih baik memetakannya daripada mengelompokkan
duan
3

Anda juga dapat mengelompokkan menurut KeyPath, seperti ini:

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

Menggunakan contoh kripto @ 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),
]

Kemudian penggunaannya terlihat seperti ini:

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

Menghasilkan hasil yang sama:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]
Sajjon
sumber
Anda dapat memberikan predikat alih-alih keypath func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }ini akan memungkinkan Anda untuk menelepon assets.grouped(by: \.coin)atauassets.grouped { $0.coin }
Leo Dabus
2

Cepat 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
sumber
0

Memperluas jawaban yang diterima untuk memungkinkan pengelompokan yang teratur :

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

Kemudian ini akan berfungsi pada tupel apa pun :

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

Serta setiap struct atau kelas :

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

Berikut adalah pendekatan berbasis tupel saya untuk menjaga ketertiban saat menggunakan Swift 4 KeyPath sebagai pembanding grup:

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

Contoh cara menggunakannya:

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

Hai jika Anda perlu menjaga ketertiban saat mengelompokkan elemen, bukan kamus hash, saya telah menggunakan tupel dan menjaga urutan daftar saat mengelompokkan.

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

Kamus Thr (pengelompokan: arr) sangat mudah!

 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
}
besiRoei
sumber
-2

Mengambil contoh dari "oisdk" . Memperluas solusi untuk mengelompokkan objek berdasarkan nama kelas Tautan Demo & Kode Sumber .

Potongan kode untuk pengelompokan berdasarkan Nama Kelas:

 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
sumber