Swift'de bir dizinin öğelerine göre gruplama


90

Diyelim ki bu koda sahibim:

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

"Aynı isimle" gruplanmış 2 diziye sahip olmak için sonraki işlevi manuel olarak birçok kez çağırabilirim.

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

Sorun şu ki, bu durumda "akşam yemeği" ve "öğle yemeği" değişken değerini bilmeyeceğim, bu nedenle bu statEvents dizisini otomatik olarak ada göre gruplamak istiyorum, böylece ad farklılaştıkça çok sayıda dizi elde ederim.

Bunu nasıl yapabilirim?


Yeni başlatıcı kullanan Swift 4 için cevabımı görün Dictionary init(grouping:by:).
Imanou Petit

Yanıtlar:


196

Swift 4:

Swift 4'ten bu yana, bu işlevsellik standart kitaplığa eklenmiştir . Bunu şu şekilde kullanabilirsiniz:

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

Ne yazık ki, appendyukarıdaki işlev, tercih edilen bir şekilde, onu yerinde değiştirmek yerine temeldeki diziyi kopyalar. Bu oldukça büyük bir yavaşlamaya neden oluyor . Bir referans türü sarmalayıcı kullanarak sorunu aşabilirsiniz:

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

Son sözlüğü iki kez geçseniz bile, bu sürüm çoğu durumda orijinalinden daha hızlıdır.

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

Sizin durumunuzda, "anahtarların" keyFuncşu isimlerle döndürülmesini sağlayabilirsiniz :

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

Böylece, her anahtarın bir ad olduğu ve her değerin bu ada sahip StatEvents dizisi olduğu bir sözlük elde edeceksiniz.

Hızlı 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 }

Hangi çıktıyı verir:

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


Çok teşekkür ederim @oisdk! Oluşturulan sözlüğün değerlerinin indeksine erişmenin bir yolu olup olmadığını biliyor musunuz? Demek istediğim, anahtarları ve değerleri nasıl alacağımı biliyorum, ama bu sözlüklerin "0", "1", "2" dizinini almak istiyorum
Ruben

Yani eğer isterseniz, sözlüğünüzdeki üç "akşam yemeği" değerini söyleyin, gidersiniz dict[key](ilk örneğimde böyle olurdu ans["dinner"]). Üç şeyin indekslerini kendileri istiyorsanız, bu şöyle bir şey olabilirdi enumerate(ans["dinner"])ya da indisler aracılığıyla erişmek isterseniz, şöyle yapabilirsiniz: ans["dinner"]?[0]altında depolanan dizinin ilk öğesini size döndürecektir dinner.
oisdk

Ups it her zaman sıfır döndürür
Ruben

Ah evet anladım, ama sorun şu ki bu örnekte "akşam yemeği" değerini bilmem gerekiyor, ancak gerçek kodda bu değerleri veya sözlüğün kaç öğede olacağını bilmeyeceğim
Ruben

1
Bu, çözüme doğru iyi bir başlangıçtır, ancak bazı eksiklikleri vardır. Burada ( if case) kalıp eşleştirmesinin kullanılması gereksizdir, ancak daha da önemlisi, bir sözlükte saklanan bir dict[key]?.append)ifadeye her seferinde bir kopyanın oluşmasına neden olur. Bkz. Rosslebeau.com/2016/…
Alexander

65

Swift 5 ile, Dictionaryadı verilen bir başlatıcı yöntemi vardırinit(grouping:by:) . init(grouping:by:)aşağıdaki beyana sahiptir:

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

Anahtarların verilen kapanış tarafından döndürülen gruplandırmalar olduğu ve değerlerin her bir belirli anahtarı döndüren öğelerin dizileri olduğu yeni bir sözlük oluşturur.


Aşağıdaki Oyun Alanı kodu, init(grouping:by:)sorununuzu çözmek için nasıl kullanılacağını gösterir :

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

4
İyi bir şey, şu şekilde de yazılabileceğini ekleyebilir misiniz let dictionary = Dictionary(grouping: statEvents) { $0.name }- Sözdizimi Şeker kaplama
user1046037

1
Bu, hızlı 4'ten başlayarak cevap olmalıdır - tamamen Apple tarafından desteklenmektedir ve umarım yüksek performanslıdır.
Herbal7ea

Ayrıca yüklemde döndürülen isteğe bağlı olmayan anahtara da dikkat edin, aksi takdirde şu hatayı görürsünüz: "ifade türü daha fazla bağlam olmadan belirsizdir ..."
Asike

1
@ user1046037 Swift 5.2Dictionary(grouping: statEvents, by: \.name)
Aslan Dabus

31

Swift 4: kullanabileceğiniz init (gruplama: by :) dan elma geliştirici sitesinde

Örnek :

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

Yani senin durumunda

   let dictionary = Dictionary(grouping: currentStat.statEvents, by:  { $0.name! })

1
Şimdiye kadarki en iyi cevap bu, bunun var olduğunu bilmiyordum;)
RichAppz

Bu aynı zamanda bir ana yolla da çalışır: let dictionary = Dictionary (grouping: currentStat.statEvents, by: \ .name)
Jim Haungs

26

Swift 3 için:

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

Kullanım:

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

9
Bir kullanım örneği çok takdir edilecektir :) Teşekkürler!
Centurion

İşte bir kullanım örneği: yourArray.categorise (currentStat.statEvents) {$ 0.name}. İşlev Dictionary <String, Array <StatEvents >>
Centurion

6

Swift 4'te, bu uzantı en iyi performansa sahiptir ve operatörlerinizin zincirlenmesine yardımcı olur

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

Misal:

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

oluşturur:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]

bir kullanım örneği yazabilir misin?
Utku Dalmaz

@duan mümkün böyle BTC ve BTC olarak davayı görmezden gelmektir ... Aynı sayılması gerektiğini belirtmektedir
Moin Shirazi

1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), ancak bu 'haritadan sonra gruplandırmak daha iyi
duan

3

Ayrıca KeyPath, aşağıdaki gibi gruplandırabilirsiniz :

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

@ Duan'ın kripto örneğini kullanarak:

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

Daha sonra kullanım şuna benzer:

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

Aynı sonucu vermek:

[
    "ETH": [
        Asset(coin: "ETH", amount: 15)
    ],
    "BTC": [
        Asset(coin: "BTC", amount: 12),
        Asset(coin: "BTC", amount: 30)
    ]
]

ana yol yerine bir yüklemi geçebilirsiniz, func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }bu çağrı yapmanızı sağlar assets.grouped(by: \.coin)veyaassets.grouped { $0.coin }
Leo Dabus

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

0

Sıralı gruplamaya izin vermek için kabul edilen yanıtı genişletme :

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

Daha sonra herhangi bir tuple üzerinde çalışacaktır :

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

Herhangi bir yapı veya sınıfın yanı sıra :

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

0

Swift 4 KeyPath'leri grup karşılaştırıcısı olarak kullanırken düzeni korumak için tuple tabanlı yaklaşımım :

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

Nasıl kullanılacağına dair örnek:

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)

0

Hey, öğeleri gruplandırırken sıraya uymanız gerekiyorsa, hash sözlük yerine tuple kullandım ve gruplama sırasında listenin sırasını korudum.

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

0

Thr Sözlük (gruplama: arr) çok kolay!

 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
}

-2

"Oisdk" örneğinden bir yaprak çıkarmak . Çözümü sınıf adı Demo ve Kaynak Kodu bağlantısına göre gruplayacak şekilde genişletme .

Sınıf Adına göre gruplama için kod pasajı:

 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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.