【问题标题】:Group elements of an array by some property通过某些属性对数组的元素进行分组
【发布时间】:2017-05-24 16:36:06
【问题描述】:

我有一个属性为 date 的对象数组。

我想要的是创建数组数组,其中每个数组将包含具有相同日期的对象。

我明白,我需要像 .filter 这样的东西来过滤对象,然后 .map 将所有东西添加到数组中。

但是如何告诉.map 我想为每个组从过滤的对象中分离出一个数组,并且这个数组必须添加到“全局”数组中,以及如何告诉.filter 我想要具有相同日期的对象?

【问题讨论】:

标签: arrays swift data-structures grouping


【解决方案1】:

这是执行分组的一种简洁方式:

let grouped = allRows.group(by: {$0.groupId}) // Dictionary with the key groupId

假设您有一系列联系人,例如:

class ContactPerson {
    var groupId:String?
    var name:String?
    var contactRecords:[PhoneBookEntry] = []
}

要实现这一点,请添加此扩展:

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

【讨论】:

    【解决方案2】:

    可能晚了,但新的 Xcode 9 sdk 字典有新的 init 方法

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

    Documentation has simple example what this method does. 我只是在下面发布这个例子:

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

    结果将是:

    ["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]
    

    【讨论】:

      【解决方案3】:

      在 Swift 5 中,您可以使用 Dictionaryinit(grouping:by:) 初始化程序将数组元素按其属性之一分组到字典中。完成后,您可以使用 Dictionaryvalues 属性和 Array init(_:) 初始化器从字典中创建一个数组数组。


      以下 Playground 示例代码展示了如何按一个属性将数组的元素分组到一个新的数组数组中:

      import Foundation
      
      struct Purchase: CustomStringConvertible {
          let id: Int 
          let date: Date
          var description: String {
              return "Purchase #\(id) (\(date))"
          }
      }
      
      let date1 = Calendar.current.date(from: DateComponents(year: 2010, month: 11, day: 22))!
      let date2 = Calendar.current.date(from: DateComponents(year: 2015, month: 5, day: 1))!
      let date3 = Calendar.current.date(from: DateComponents(year: 2012, month: 8, day: 15))!
      let purchases = [
          Purchase(id: 1, date: date1),
          Purchase(id: 2, date: date1),
          Purchase(id: 3, date: date2),
          Purchase(id: 4, date: date3),
          Purchase(id: 5, date: date3)
      ]
      
      let groupingDictionary = Dictionary(grouping: purchases, by: { $0.date })
      print(groupingDictionary)
      /*
       [
          2012-08-14 22:00:00 +0000: [Purchase #4 (2012-08-14 22:00:00 +0000), Purchase #5 (2012-08-14 22:00:00 +0000)],
          2010-11-21 23:00:00 +0000: [Purchase #1 (2010-11-21 23:00:00 +0000), Purchase #2 (2010-11-21 23:00:00 +0000)],
          2015-04-30 22:00:00 +0000: [Purchase #3 (2015-04-30 22:00:00 +0000)]
       ]
       */
      
      let groupingArray = Array(groupingDictionary.values)
      print(groupingArray)
      /*
       [
          [Purchase #3 (2015-04-30 22:00:00 +0000)],
          [Purchase #4 (2012-08-14 22:00:00 +0000), Purchase #5 (2012-08-14 22:00:00 +0000)],
          [Purchase #1 (2010-11-21 23:00:00 +0000), Purchase #2 (2010-11-21 23:00:00 +0000)]
       ]
       */
      

      【讨论】:

        【解决方案4】:

        +1 给 GolenKovkosty 的回答。

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

        更多例子:

        enum Parity {
           case even, odd
           init(_ value: Int) {
               self = value % 2 == 0 ? .even : .odd
           }
        }
        let parity = Dictionary(grouping: 0 ..< 10 , by: Parity.init )
        

        相当于

        let parity2 = Dictionary(grouping: 0 ..< 10) { $0 % 2 }
        

        在你的情况下:

        struct Person : CustomStringConvertible {
            let dateOfBirth : Date
            let name :String
            var description: String {
                return "\(name)"
            }
        }
        
        extension Date {
            init(dateString:String) {
                let formatter = DateFormatter()
                formatter.timeZone = NSTimeZone.default
                formatter.dateFormat = "MM/dd/yyyy"
                self = formatter.date(from: dateString)!
            }
        }
        let people = [Person(dateOfBirth:Date(dateString:"01/01/2017"),name:"Foo"),
                      Person(dateOfBirth:Date(dateString:"01/01/2017"),name:"Bar"),
                      Person(dateOfBirth:Date(dateString:"02/01/2017"),name:"FooBar")]
        let parityFields = Dictionary(grouping: people) {$0.dateOfBirth}
        

        输出:

        [2017-01-01: [Foo, Bar], 2017-02-01:  [FooBar] ]
        

        【讨论】:

          【解决方案5】:

          Rapheal 的解决方案确实有效。但是,我建议更改解决方案以支持分组实际上是稳定的说法。

          就目前而言,调用 grouped() 将返回一个分组数组,但随后的调用可能会返回一个包含不同顺序的组的数组,尽管每个组的元素将按照预期的顺序。

          internal protocol Groupable {
              associatedtype GroupingType : Hashable
              var groupingKey : GroupingType? { get }
          }
          
          extension Array where Element : Groupable {
          
              typealias GroupingType = Element.GroupingType
          
              func grouped(nilsAsSingleGroup: Bool = false) -> [[Element]] {
                  var groups = [Int : [Element]]()
                  var groupsOrder = [Int]()
                  let nilGroupingKey = UUID().uuidString.hashValue
                  var nilGroup = [Element]()
          
                  for element in self {
          
                      // If it has a grouping key then use it. Otherwise, conditionally make one based on if nils get put in the same bucket or not
                      var groupingKey = element.groupingKey?.hashValue ?? UUID().uuidString.hashValue
                      if nilsAsSingleGroup, element.groupingKey == nil { groupingKey = nilGroupingKey }
          
                      // Group nils together
                      if nilsAsSingleGroup, element.groupingKey == nil {
                          nilGroup.append(element)
                          continue
                      }
          
                      // Place the element in the right bucket
                      if let _ = groups[groupingKey] {
                          groups[groupingKey]!.append(element)
                      } else {
                          // New key, track it
                          groups[groupingKey] = [element]
                          groupsOrder.append(groupingKey)
                      }
          
                  }
          
                  // Build our array of arrays from the dictionary of buckets
                  var grouped = groupsOrder.flatMap{ groups[$0] }
                  if nilsAsSingleGroup, !nilGroup.isEmpty { grouped.append(nilGroup) }
          
                  return grouped
              }
          }
          

          现在我们跟踪发现新分组的顺序,我们可以更一致地返回分组数组,而不仅仅是依赖字典的无序 values 属性。

          struct GroupableInt: Groupable {
              typealias GroupingType = Int
              var grouping: Int?
              var content: String
          }
          
          var a = [GroupableInt(groupingKey: 1, value: "test1"),
                   GroupableInt(groupingKey: 2, value: "test2"),
                   GroupableInt(groupingKey: 2, value: "test3"),
                   GroupableInt(groupingKey: nil, value: "test4"),
                   GroupableInt(groupingKey: 3, value: "test5"),
                   GroupableInt(groupingKey: 3, value: "test6"),
                   GroupableInt(groupingKey: nil, value: "test7")]
          
          print(a.grouped())
          // > [[GroupableInt(groupingKey: 1, value: "test1")], [GroupableInt(groupingKey: 2, value: "test2"),GroupableInt(groupingKey: 2, value: "test3")], [GroupableInt(groupingKey: nil, value: "test4")],[GroupableInt(groupingKey: 3, value: "test5"),GroupableInt(groupingKey: 3, value: "test6")],[GroupableInt(groupingKey: nil, value: "test7")]]
          
          print(a.grouped(nilsAsSingleGroup: true))
          // > [[GroupableInt(groupingKey: 1, value: "test1")], [GroupableInt(groupingKey: 2, value: "test2"),GroupableInt(groupingKey: 2, value: "test3")], [GroupableInt(groupingKey: nil, value: "test4"),GroupableInt(groupingKey: nil, value: "test7")],[GroupableInt(groupingKey: 3, value: "test5"),GroupableInt(groupingKey: 3, value: "test6")]]
          

          【讨论】:

          • 我已对此进行了更新以支持可选的分组键。 grouped 函数现在采用一个 Bool 参数,该参数指定是将 nil 分组到单个组中,还是在找到它们时单独分组,后者是默认值 (false)。 here's 要点
          【解决方案6】:

          改进 oriyentel 解决方案以允许对任何事物进行有序分组:

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

          然后它将适用于任何 tuplestructclass 以及任何属性:

          let a = [(grouping: 10, content: "a"),
                   (grouping: 20, content: "b"),
                   (grouping: 10, content: "c")]
          print(a.group { $0.grouping })
          
          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 })
          

          【讨论】:

          • 不错!这是此功能的自然进展。我已经更新了我的解决方案以允许可选的分组键。此功能在您的解决方案中也很方便。
          【解决方案7】:

          抽象一步,你想要的是将数组的元素按某个属性分组。您可以像这样让地图为您进行分组:

          protocol Groupable {
              associatedtype GroupingType: Hashable
              var grouping: GroupingType { get set }
          }
          
          extension Array where Element: Groupable  {
              typealias GroupingType = Element.GroupingType
          
              func grouped() -> [[Element]] {
                  var groups = [GroupingType: [Element]]()
          
                  for element in self {
                      if let _ = groups[element.grouping] {
                          groups[element.grouping]!.append(element)
                      } else {
                          groups[element.grouping] = [element]
                      }
                  }
          
                  return Array<[Element]>(groups.values)
              }
          }
          

          请注意,这种分组是稳定的,即组按出现的顺序出现,并且在组内,各个元素的出现顺序与原始数组中的顺序相同。

          使用示例

          我将举一个使用整数的例子;应该清楚如何为T 使用任何(可散列的)类型,包括Date

          struct GroupInt: Groupable {
              typealias GroupingType = Int
              var grouping: Int
              var content: String
          }
          
          var a = [GroupInt(grouping: 1, content: "a"),
                   GroupInt(grouping: 2, content: "b") ,
                   GroupInt(grouping: 1, content: "c")]
          
          print(a.grouped())
          // > [[GroupInt(grouping: 2, content: "b")], [GroupInt(grouping: 1, content: "a"), GroupInt(grouping: 1, content: "c")]]
          

          【讨论】:

          • FWIW,扩展中的typealias 应该不是必需的,但没有它编译器不会推断groups 的类型。 (Report)
          猜你喜欢
          • 2022-03-17
          • 2021-11-14
          • 2017-03-18
          • 1970-01-01
          • 2011-05-27
          • 1970-01-01
          • 1970-01-01
          • 2022-01-05
          • 2013-03-25
          相关资源
          最近更新 更多