'How to group by the elements of an array in Swift
Let's say that I have this code:
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] = []
I could call as many times manually the next function in order to have 2 arrays grouped by "same name".
filteredArray1 = currentStat.statEvents.filter({$0.name == "dinner"})
filteredArray2 = currentStat.statEvents.filter({$0.name == "lunch"})
The problem is that I won't know the variable value, in this case "dinner" and "lunch", so I would like to group this array of statEvents automatically by name, so I get as many arrays as the name gets different.
How could I do that?
Solution 1:[1]
Swift 4:
Since Swift 4, this functionality has been added to the standard library. You can use it like so:
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
}
}
Unfortunately, the append
function above copies the underlying array, instead of mutating it in place, which would be preferable. This causes a pretty big slowdown. You can get around the problem by using a reference type wrapper:
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
}
}
Even though you traverse the final dictionary twice, this version is still faster than the original in most cases.
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
}
}
In your case, you could have the "keys" returned by keyFunc
be the names:
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)
]
]
So you'll get a dictionary, where every key is a name, and every value is an array of the StatEvents with that name.
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 }
Which gives the 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
]
]
(The swiftstub is here)
Solution 2:[2]
With Swift 5, Dictionary
has an initializer method called init(grouping:by:)
. init(grouping:by:)
has the following declaration:
init<S>(grouping values: S, by keyForValue: (S.Element) throws -> Key) rethrows where Value == [S.Element], S : Sequence
Creates a new dictionary where the keys are the groupings returned by the given closure and the values are arrays of the elements that returned each specific key.
The following Playground code shows how to use init(grouping:by:)
in order to solve your problem:
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]
]
*/
Solution 3:[3]
Swift 4: you can use init(grouping:by:) from apple developer site
Example:
let students = ["Kofi", "Abena", "Efua", "Kweku", "Akosua"]
let studentsByLetter = Dictionary(grouping: students, by: { $0.first! })
// ["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]
So in your case
let dictionary = Dictionary(grouping: currentStat.statEvents, by: { $0.name! })
Solution 4:[4]
For 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
}
}
Usage:
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)
]
]
Solution 5:[5]
In Swift 4, this extension has the best performance and help chain your operators
extension Sequence {
func group<U: Hashable>(by key: (Iterator.Element) -> U) -> [U:[Iterator.Element]] {
return Dictionary.init(grouping: self, by: key)
}
}
Example:
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 })
creates:
[
"ETH": [
Asset(coin: "ETH", amount: 15)
],
"BTC": [
Asset(coin: "BTC", amount: 12),
Asset(coin: "BTC", amount: 30)
]
]
Solution 6:[6]
You can also group by KeyPath
, like this:
public extension Sequence {
func group<Key>(by keyPath: KeyPath<Element, Key>) -> [Key: [Element]] where Key: Hashable {
return Dictionary(grouping: self, by: {
$0[keyPath: keyPath]
})
}
}
Using @duan's crypto example:
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),
]
Then usage looks like this:
let grouped = assets.group(by: \.coin)
Yielding the same result:
[
"ETH": [
Asset(coin: "ETH", amount: 15)
],
"BTC": [
Asset(coin: "BTC", amount: 12),
Asset(coin: "BTC", amount: 30)
]
]
Solution 7:[7]
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)})
Solution 8:[8]
Extending on accepted answer 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 on any tuple:
let a = [(grouping: 10, content: "a"),
(grouping: 20, content: "b"),
(grouping: 10, content: "c")]
print(a.group { $0.grouping })
As well as any struct or class:
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 })
Solution 9:[9]
Hey if you need to keep order while grouping elements instead of hash dictionary i have used tuples and kept the order of the list while grouping.
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.firstIndex(where: { $0.0 == groupKey }) else { groupCategorized.append((groupKey, [item])); continue }
groupCategorized[index].1.append(item)
}
return groupCategorized
}
}
Solution 10:[10]
Here is my tuple based approach for keeping order while using Swift 4 KeyPath's as group comparator:
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
}
}
}
Example of how to use it:
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)
Solution 11:[11]
Thr Dictionary(grouping: arr) is so easy!
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
}
Solution 12:[12]
You can use reduce
let result = currentStat.statEvents.reduce([String:[StatEvents]](), {
var previous = $0
previous[$1.name] = (previous[$1.name] ?? []) + [$1]
return previous
})
let filteredArray1 = result["lunch"]
let filteredArray2 = result["dinner"]
or alternatively you could change it to
let result = currentStat.statEvents.reduce(["lunch":[StatEvents](), "dinner":[StatEvents]()], {
var previous = $0
if let array = previous[$1.name] {
previous[$1.name] = array + [$1]
}
return previous
})
let filteredArray1 = result["lunch"]
let filteredArray2 = result["dinner"]
to limit to only finding lunch and diner
Solution 13:[13]
My way
extension Array {
func group<T: Hashable>(by key: (_ element: Element) -> T) -> [[Element]] {
var categories: [T: [Element]] = [:]
for element in self {
let key = key(element)
categories[key, default: []].append(element)
}
return categories.values.map { $0 }
}
}
Solution 14:[14]
Based on this, I did this:
func filterItems() -> Dictionary<Int, [YourDataType]> {
return Dictionary(grouping: yourDataTypeVar, by: { (element: YourDataType) in
return item.name
})
}
Solution 15:[15]
Taking a leaf out of "oisdk" example. Extending the solution to group objects based on class name Demo & Source Code link.
Code snippet for grouping based on Class Name:
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)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow