Cách nhóm theo các phần tử của mảng trong Swift


89

Giả sử rằng tôi có mã này:

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

Tôi có thể gọi hàm tiếp theo theo cách thủ công bao nhiêu lần để có 2 mảng được nhóm theo "cùng tên".

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

Vấn đề là tôi sẽ không biết giá trị biến, trong trường hợp này là "bữa tối" và "bữa trưa", vì vậy tôi muốn nhóm mảng statEvents này tự động theo tên, vì vậy tôi nhận được càng nhiều mảng thì tên càng khác.

Làm thế nào tôi có thể làm điều đó?


Xem câu trả lời của tôi cho Swift 4 sử dụng bộ Dictionary init(grouping:by:)khởi tạo mới .
Imanou Petit

Câu trả lời:


191

Swift 4:

Kể từ Swift 4, chức năng này đã được thêm vào thư viện chuẩn . Bạn có thể sử dụng nó như vậy:

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

Thật không may, appendhàm trên sao chép mảng bên dưới, thay vì thay đổi nó tại chỗ, điều này sẽ tốt hơn. Điều này gây ra một sự chậm lại khá lớn . Bạn có thể khắc phục sự cố bằng cách sử dụng trình bao bọc loại tham chiếu:

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

Mặc dù bạn xem qua từ điển cuối cùng hai lần, phiên bản này vẫn nhanh hơn phiên bản gốc trong hầu hết các trường hợp.

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

Trong trường hợp của bạn, bạn có thể có các "khóa" được trả về bằng keyFunccác tên:

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

Vì vậy, bạn sẽ nhận được một từ điển, trong đó mọi khóa là một tên và mọi giá trị là một mảng của StatEvents với tên đó.

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

Điều này cho kết quả:

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

(The swiftstub là ở đây )


Cảm ơn bạn rất nhiều @oisdk! Bạn có biết nếu có một cách để truy cập vào chỉ mục của các giá trị của từ điển được tạo ra? Ý tôi là, tôi biết cách lấy các khóa và các giá trị, nhưng tôi muốn lấy chỉ mục "0", "1", "2" ... của những từ điển đó
Ruben

Vì vậy, nếu bạn muốn, hãy nói ba giá trị "bữa tối" trong từ điển của bạn, bạn sẽ đi dict[key], (trong ví dụ đầu tiên của tôi là như vậy ans["dinner"]). Nếu bạn muốn các chỉ số của chính ba thứ, nó sẽ giống như enumerate(ans["dinner"]), hoặc, nếu bạn muốn truy cập thông qua các chỉ số, bạn có thể làm như : ans["dinner"]?[0], sẽ trả về cho bạn phần tử đầu tiên của mảng được lưu trữ dưới đó dinner.
oisdk

Tăng nó luôn trả lại cho tôi con số không
Ruben

Ồ vâng, tôi hiểu rồi, nhưng vấn đề là trong ví dụ này, tôi phải biết giá trị "dinner", nhưng trong mã thực, tôi sẽ không biết giá trị này cũng như có bao nhiêu mục sẽ có từ điển
Ruben

1
Đây là một khởi đầu tốt để hướng tới một giải pháp, nhưng nó có một vài kết quả ngắn. Việc sử dụng đối sánh mẫu ở đây ( if case) là không cần thiết, nhưng quan trọng hơn, thêm vào một từ được lưu trữ trong từ điển với dict[key]?.append)nguyên nhân luôn xảy ra một bản sao. Xem rosslebeau.com/2016/…
Alexander - Phục hồi Monica

65

Với Swift 5, Dictionarycó một phương thức khởi tạo được gọi init(grouping:by:). init(grouping:by:)có khai báo sau:

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

Tạo một từ điển mới trong đó các khóa là các nhóm được trả về bởi bao đóng đã cho và các giá trị là mảng của các phần tử trả về từng khóa cụ thể.


Mã Playground sau đây cho biết cách sử dụng init(grouping:by:)để giải quyết vấn đề của bạn:

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
Tốt nhất, bạn cũng có thể bao gồm mà nó cũng có thể được viết như sau let dictionary = Dictionary(grouping: statEvents) { $0.name }- Cú pháp Đường sơn
user1046037

1
Đây sẽ là câu trả lời bắt đầu từ phiên bản nhanh 4 - được Apple hỗ trợ đầy đủ và hy vọng sẽ có hiệu suất cao.
Herbal7ea

Ngoài ra chú ý về chính phi optinal trả lại trong vị ngữ, nếu không bạn sẽ gặp lỗi: "loại biểu hiện là mơ hồ mà không bối cảnh nhiều hơn ..."
Asike

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

31

Swift 4: bạn có thể sử dụng init (grouping: by :) từ trang web của nhà phát triển apple

Ví dụ :

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

Vì vậy, trong trường hợp của bạn

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

1
Đây là câu trả lời tốt nhất cho đến nay, không biết điều này tồn tại nhờ;)
RichAppz

Điều này cũng hoạt động với một đường dẫn phím: let dictionary = Dictionary (nhóm: currentStat.statEvents, bởi: \ .name)
Jim Haungs

26

Đối với 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
    }
}

Sử dụng:

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
Một ví dụ sử dụng sẽ được đánh giá cao :) Cảm ơn!
Centurion

Đây là một ví dụ về cách sử dụng: yourArray.categorise (currentStat.statEvents) {$ 0.name}. Hàm sẽ trả về Dictionary <String, Mảng <StatEvents >>
Centurion

6

Trong Swift 4, tiện ích mở rộng này có hiệu suất tốt nhất và giúp chuỗi các nhà khai thác của bạn

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

Thí dụ:

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

tạo ra:

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

bạn có thể viết một ví dụ về cách sử dụng?
Utku Dalmaz

@duan là nó có thể bỏ qua trường hợp như BTC và btc nên được tính là cùng ...
Moin Shirazi

1
@MoinShirazi assets.group(by: { $0.coin.uppercased() }), nhưng tốt hơn để lập bản đồ sau đó nhóm của nó
duan

3

Bạn cũng có thể nhóm theo KeyPath, như thế này:

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

Sử dụng ví dụ về tiền điện tử của @ 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),
]

Sau đó, cách sử dụng trông như thế này:

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

Mang lại cùng một kết quả:

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

bạn có thể chuyển một vị từ thay vì đường dẫn phím, func grouped<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] { .init(grouping: self, by: keyForValue) }điều này sẽ cho phép bạn gọi assets.grouped(by: \.coin)hoặcassets.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

Mở rộng câu trả lời được chấp nhận để cho phép nhóm theo thứ tự :

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

Sau đó, nó sẽ hoạt động trên bất kỳ tuple nào :

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

Cũng như bất kỳ cấu trúc hoặc lớp nào :

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

Đây là cách tiếp cận dựa trên tuple của tôi để giữ trật tự trong khi sử dụng Swift 4 KeyPath làm trình so sánh nhó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
        }
    }
}

Ví dụ về cách sử dụng nó:

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

Này nếu bạn cần giữ trật tự trong khi nhóm các phần tử thay vì từ điển băm, tôi đã sử dụng các bộ giá trị và giữ thứ tự của danh sách trong khi nhóm.

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

Từ điển Thr (phân nhóm: arr) thật dễ dàng!

 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

Lấy một chiếc lá từ ví dụ "oisdk" . Mở rộng giải pháp nhóm các đối tượng dựa trên liên kết Demo & Mã nguồn tên lớp .

Đoạn mã để nhóm dựa trên Tên lớp:

 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)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.