135

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?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Ruben
  • 1,789
  • 2
  • 17
  • 26
  • 1
    See [my answer for Swift 4](https://stackoverflow.com/a/44555642/1966109) that uses new `Dictionary` `init(grouping:by:)` initializer. – Imanou Petit Jul 14 '17 at 22:21

16 Answers16

258

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)

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
oisdk
  • 9,763
  • 4
  • 18
  • 36
  • Thank you so much @oisdk ! Do you know if there is a way to access to the index of the values of the dictionary that is created? I mean, I know how to get the keys and the values, but I would like to get the index "0", "1", "2"... of those dictionaries – Ruben Jul 04 '15 at 19:55
  • So if you want, say the three "dinner" values in your dictionary, you would go `dict[key]`, (in my first example it would be `ans["dinner"]`). If you wanted the indices of the three things themselves, it would be something like `enumerate(ans["dinner"])`, or, if you wanted to *access* via the indices, you could do it like: `ans["dinner"]?[0]`, which would return you the first element of the array stored under `dinner`. – oisdk Jul 04 '15 at 20:09
  • Ups it always returns me nil – Ruben Jul 04 '15 at 20:40
  • Oh yes I get it, but the problem is that in this example I'm supposed to know the value "dinner", but in the real code I won't know this values nor how many items will have the dictionary – Ruben Jul 04 '15 at 21:19
  • So how do you want to access them? – oisdk Jul 04 '15 at 21:22
  • I mean, in the example that you showed me, it returns 2 key,value elements. I would like to get access to the indexes of those 2 elements, in this case would be "0" for the "dinner element and "1" for the "lunch" element. I need this for a titleforheaderinsection function – Ruben Jul 04 '15 at 21:29
  • Oh right. In Swift 2, it's the `.indices` method. Not sure about Swift 1, though. – oisdk Jul 04 '15 at 21:45
  • The swift 1.2 equivalent to `.indices` is `.keys`. However, note that Swift’s Dictionary type does not have a defined ordering. So, if you call `.keys` or `.indices` on a dictionary, there is no guarantee that you will get the keys back in the same order every time. To support something like a grouped `UITableView` you would need to extract the keys to an array and use the array as the backing store for the titles for sections. – memmons Sep 23 '15 at 00:51
  • Is there a way to do with multiple entries name ,date – Nassif Nov 10 '15 at 03:40
  • 1
    This is a good start towards a solution, but it has a few short comings. The use of pattern matching here (`if case`) is unnecessary, but more importantly, appending to an stored within a dictionary with `dict[key]?.append)` causes a copy to occur every time. See http://rosslebeau.com/2016/swift-copy-write-psa-mutating-dictionary-entries – Alexander Apr 26 '17 at 17:26
  • The copy-on-write problem is a good point, I'll add it to the answer. What's the problem with `if case`, though? – oisdk May 06 '17 at 11:32
  • Swift 4 solution is great! It would probably be better to have struct name `StateEvent` instead of `StateEvents` to make the naming more clear. – Guven Sep 15 '20 at 08:21
  • 2
    Swift 5.2 `Dictionary(grouping: currentStat.statEvents, by: \.name)` – Leo Dabus Oct 02 '20 at 03:30
87

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]
]
*/
Roman
  • 1,309
  • 14
  • 23
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
  • 4
    Good one, could you also include that it can also be written as `let dictionary = Dictionary(grouping: statEvents) { $0.name }` - Syntax Sugar coating – user1046037 Sep 01 '17 at 10:41
  • 1
    This should be the answer starting swift 4 - fully supported by apple, and hopefully highly performant. – Herbal7ea May 02 '18 at 23:51
  • Also pay attention on non-optinal key returned in predicate, otherwise you will see error: "type of expression is ambiguous without more context..." – Asike Jun 25 '18 at 10:52
  • 4
    @user1046037 Swift 5.2 `Dictionary(grouping: statEvents, by: \.name)` – Leo Dabus Oct 02 '20 at 03:38
34

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! })
Mihuilk
  • 1,901
  • 1
  • 22
  • 17
26

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)
  ]
]
Michal Zaborowski
  • 5,039
  • 36
  • 35
10

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)
    ]
]
duan
  • 8,515
  • 3
  • 48
  • 70
4

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)
    ]
]
Sajjon
  • 8,938
  • 5
  • 60
  • 94
  • you can pass a predicate instead of the keypath `func grouped(by keyForValue: (Element) -> Key) -> [Key: [Element]] {` `.init(grouping: self, by: keyForValue)` `}` this would allow you to call `assets.grouped(by: \.coin)` or `assets.grouped { $0.coin }` – Leo Dabus Oct 02 '20 at 03:44
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)})
Heath Borders
  • 30,998
  • 16
  • 147
  • 256
1

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 })
Cœur
  • 37,241
  • 25
  • 195
  • 267
1

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
   }
}
mital solanki
  • 2,394
  • 1
  • 15
  • 24
Suat KARAKUSOGLU
  • 622
  • 6
  • 14
0

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)
Zell B.
  • 10,266
  • 3
  • 40
  • 49
0

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
}
ironRoei
  • 2,049
  • 24
  • 45
0

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 }
    }
}
Bobj-C
  • 5,276
  • 9
  • 47
  • 83
0

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

Nathan Day
  • 5,981
  • 2
  • 24
  • 40
0

Based on this, I did this:

func filterItems() -> Dictionary<Int, [YourDataType]> {
        return Dictionary(grouping: yourDataTypeVar, by: { (element: YourDataType) in
            return item.name
        })
    }
Edi
  • 1,900
  • 18
  • 27
0

You can use OrderedDictionary from https://github.com/apple/swift-collections to create dictionary with ordered keys.

import OrderedCollections

OrderedDictionary(grouping: statEvents, by: \.name)

or Dictionary to create Dictionary with unordered keys.

Dictionary(grouping: statEvents, by: \.name)
Victor Kushnerov
  • 3,706
  • 27
  • 56
-2

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)
Community
  • 1
  • 1
Abhijeet
  • 8,561
  • 5
  • 70
  • 76