Group elements of an array by some property

2019-01-19 00:07发布

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 ?

5条回答
Viruses.
2楼-- · 2019-01-19 00:44

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"]]
查看更多
倾城 Initia
3楼-- · 2019-01-19 00:48

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 })
查看更多
不美不萌又怎样
4楼-- · 2019-01-19 00:48

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")]]
查看更多
做个烂人
5楼-- · 2019-01-19 00:59

+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] ]
查看更多
Anthone
6楼-- · 2019-01-19 01:01

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")]]
查看更多
登录 后发表回答