Group elements of an array by some property

I have an array of objects with a date property.

I want to create an array of arrays where each array will contain objects with the same date.

I understand that I need something like .filter to filter objects, and then .map to add each thing to the array.

But how to tell .map that I want a separate array for each group of filtered objects and that this array should be added to the "global" array and how to tell .filter that I want objects with the same date?

+12
arrays data-structures swift grouping
source share
6 answers

It may be too late, but a new init method has appeared in the new Xcode 9 sdk dictionary

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

The documentation contains a simple example of what this method does. I just posted this example below:

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

The result will be:

 ["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]] 
+13
source share

improves oriyentel's solution to allow ordered grouping:

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

Then it will work with any tuple , struct or class and for any property:

 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 }) 
+9
source share

Abstracting one step, you want to group the elements of an array using a specific property. You can let the map do the grouping for you like this:

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

Note that this grouping is stable, that is, groups are displayed in the order of appearance, and within the groups, individual elements are displayed in the same order as in the original array.

Usage example

I will give an example using integers; it should be clear how to use any (hashed) type for T , including 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")]] 
+1
source share

Rapheal's solution really works. However, I would suggest changing the decision to support the assertion that the grouping is actually stable.

As of now, a call to grouped() will return a grouped array, but subsequent calls can return an array with groups in a different order, although the elements of each group will be in the expected order.

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

Now that we are tracking the order in which new groupings are opened, we can return the grouped array more consistently than simply relying on the unordered values property.

 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")]] 
+1
source share

+1 GolenKovkosty answer.

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

Other examples:

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

Equilvalent to

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

In your case:

 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} 

Output:

 [2017-01-01: [Foo, Bar], 2017-02-01: [FooBar] ] 
+1
source share

In Swift 5, you can group the elements of an array by one of their properties into a dictionary using Dictionary init(grouping:by:) initializer. After that, you can create an array of arrays from the dictionary using the Dictionary values property and init(_:) Array init(_:) .


The following Playground code example shows how to group array elements by one property into a new array of arrays:

 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)] ] */ 
0
source share

All Articles