Group elements of an array by some property
Asked Answered
D

8

30

I have an array of objects with property date.

What I want is to create 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 every thing to array.

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

Defilade answered 10/1, 2017 at 8:43 Comment(1)
Possible duplicate of How to group by the elements of an array in SwiftDissogeny
G
51

It might be late but new Xcode 9 sdk dictionary has new init method

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. I just post this example below:

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

Result will be:

["E": ["Efua"], "K": ["Kofi", "Kweku"], "A": ["Abena", "Akosua"]]
Girardi answered 11/12, 2017 at 18:30 Comment(1)
Simple and elegant solution!Kraemer
P
20

improving on oriyentel solution to allow ordered grouping on anything:

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, 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 })
Peace answered 20/4, 2017 at 12:49 Comment(1)
nice! this was the natural progression for this function. i've updated my solution to allow for optional grouping keys. this feature would be handy in your solution as well.Deuced
A
9

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


The following Playground sample code shows how to group the elements of an array 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)]
 ]
 */
Aggrandize answered 5/5, 2019 at 18:40 Comment(0)
A
2

Abstracting one step, what you want is to group elements of an array by a certain property. You can let a map do the grouping for you like so:

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 appear in order of appearance, and inside the groups the individual elements appear in the same order as in the original array.

Usage Example

I'll give an example using integers; it should be clear how to use any (hashable) 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")]]
Assemble answered 10/1, 2017 at 10:49 Comment(1)
FWIW, the typealias in the extension should not be necessary but the compiler won't infer the type of groups without it. (Report)Assemble
D
2

Rapheal's solution does work. However, I would propose altering the solution to support the claim that the grouping is in fact stable.

As it stands now, calling grouped() will return a grouped array but subsequent calls could return an array with groups in a different order, albeit 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 track the order that we discover new groupings, we can return a grouped array more consistently than just relying on a Dictionary's 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")]]
Deuced answered 20/3, 2017 at 22:18 Comment(1)
I've updated this to support optional grouping keys. the grouped function now takes a Bool parameter that specifies whether to group nils into a single group or to group them individually as they are found, with the latter being the default (false). here's the gist for itDeuced
F
1

+1 to GolenKovkosty answer.

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

More 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] ]
Floorage answered 12/12, 2017 at 22:54 Comment(0)
P
1

This is a clean way to perform group by:

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

Assuming you have array of contacts like :

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

To achieve this, add this extension:

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
    }
}
Pipkin answered 19/1, 2021 at 2:44 Comment(0)
N
0

2024

let arr: [(String, String)] = [("a","b"), ("a","c")]
        
let rez1 = arr.group(by: { $0.0 })

enter image description here

let rez2 = arr.group(by: { $0.0 }, values: \.1 )

enter image description here

public extension Sequence {
    /// groupBy
    ///
    /// Usage:
    /// ```
    /// arr.group(by: { $0.propertyName })
    /// ```
    func group<T: Hashable>(by key: (Iterator.Element) -> T) -> [T : [Self.Element]] {
        return Dictionary(grouping: self, by: key )
    }
    
    /// groupBy
    ///
    /// Usage:
    /// ```
    /// arr.group(by: { $0.propertyName }, values: \.propertyName2 )
    /// ```
    func group<T: Hashable>(by key: (Iterator.Element) -> T, values keyPath: KeyPath<Element, T> ) -> [T : [T]] {
        return Dictionary(grouping: self, by: key )
            .valuesMapped { $0.map{ $0[keyPath: keyPath] } }
    }
}

public extension Dictionary {
    func valuesMapped<T>(_ transform: (Value) -> T) -> [Key: T] {
        var newDict = [Key: T]()
        for (key, value) in self {
            newDict[key] = transform(value)
        }
        return newDict
    }
}
Nicotine answered 14/5, 2024 at 21:33 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.