How to group array of objects by date in swift?
Asked Answered
F

7

16

Need to group according to date. Response coming is in sorted format. Need to apply a filter on date to group. Response coming from backend:

[
    {
    "date": "date1"
    }, 
    {
    "date": "date1"
    },
    {
    "date": "date1"
    },
    {
    "date": "date2"
    },
    {
    "date": "date2"
    },
    {
    "date": "date3"
    }
]

Required:

[
    [
        "date": "2017-05-30T12:40:39.000Z",
        "message": [
            {
                "date_time": 2017-05-30T12: 40: 39.000Z
            }
        ]
    ],
    [
        "date": "2017-05-31T05:43:17.000Z",
        "message": [
            {
                "date_time": 2017-05-31T05: 43: 17.000Z
            },
            {
                "date_time": 2017-05-31T05: 44: 15.000Z
            },
            {
                "date_time": 2017-05-31T05: 44: 38.000Z
            }
        ]
    ]
]

I have checked multiple answers but wasn't able to find a good solution.

Fungicide answered 29/5, 2017 at 12:9 Comment(2)
How are you grouping the objects? Based on what logic?Th
post your solution so far.Trochee
T
46

This is my solution to group by date(just day, month and year):

let groupDic = Dictionary(grouping: arr) { (pendingCamera) -> DateComponents in

    let date = Calendar.current.dateComponents([.day, .year, .month], from: (pendingCamera.date)!)

    return date
}
Tactless answered 17/11, 2018 at 19:40 Comment(0)
B
15

You can use an array extension.

extension Array {
  func sliced(by dateComponents: Set<Calendar.Component>, for key: KeyPath<Element, Date>) -> [Date: [Element]] {
    let initial: [Date: [Element]] = [:]
    let groupedByDateComponents = reduce(into: initial) { acc, cur in
      let components = Calendar.current.dateComponents(dateComponents, from: cur[keyPath: key])
      let date = Calendar.current.date(from: components)!
      let existing = acc[date] ?? []
      acc[date] = existing + [cur]
    }

    return groupedByDateComponents
  }
}

You can use it on any model property using the Swift 5 KeyPath

let grouped = models.sliced(by: [.year, .month, .day], for: \.createdAt)
Booster answered 23/10, 2020 at 8:56 Comment(0)
A
9

You can use flatMap and filter like this to group your array to dictionary.

let datesArray = yourArray.flatMap { $0["date"] as? String } // return array of date
var dic = [String:[[String:Any]]]() // Your required result
datesArray.forEach {
    let dateKey = $0
    let filterArray = yourArray.filter { $0["date"] as? String == dateKey }
    dic[$0] = filterArray
}
print(dic)

Note: Make sure one thing that dictionary don't have any order so order of printing of date might changed.

Ambush answered 29/5, 2017 at 12:27 Comment(0)
C
9

Thank you ElegyD. This is worked for me.

extension Sequence {
    func groupSort(ascending: Bool = true, byDate dateKey: (Iterator.Element) -> Date) -> [[Iterator.Element]] {
        var categories: [[Iterator.Element]] = []
        for element in self {
            let key = dateKey(element)
            guard let dayIndex = categories.index(where: { $0.contains(where: { Calendar.current.isDate(dateKey($0), inSameDayAs: key) }) }) else {
                guard let nextIndex = categories.index(where: { $0.contains(where: { dateKey($0).compare(key) == (ascending ? .orderedDescending : .orderedAscending) }) }) else {
                    categories.append([element])
                    continue
                }
                categories.insert([element], at: nextIndex)
                continue
            }

            guard let nextIndex = categories[dayIndex].index(where: { dateKey($0).compare(key) == (ascending ? .orderedDescending : .orderedAscending) }) else {
                categories[dayIndex].append(element)
                continue
            }
            categories[dayIndex].insert(element, at: nextIndex)
        }
        return categories
    }
}

Usage:

class Model {
    let date: Date!
    let anotherProperty: String!

    init(date: Date, _ anotherProperty: String) {
        self.date = date
        self.anotherProperty = anotherProperty
    }
}

let modelArray = [
    Model(date: Date(), anotherProperty: "Original Date"),
    Model(date: Date().addingTimeInterval(86400), anotherProperty: "+1 day"),
    Model(date: Date().addingTimeInterval(172800), anotherProperty: "+2 days"),
    Model(date: Date().addingTimeInterval(86401), anotherProperty: "+1 day & +1 second"),
    Model(date: Date().addingTimeInterval(172801), anotherProperty: "+2 days & +1 second"),
    Model(date: Date().addingTimeInterval(86400), anotherProperty: "+1 day"),
    Model(date: Date().addingTimeInterval(172800), anotherProperty: "+2 days")
]

let groupSorted = modelArray.groupSort(byDate: { $0.date })
print(groupSorted) // [["Original Date"], ["+1 day", "+1 day", "+1 day & +1 second"], ["+2 days", "+2 days", "+2 days & +1 second"]]

let groupSortedDesc = modelArray.groupSort(ascending: false, byDate: { $0.date })
print(groupSortedDesc) // [["+2 days & +1 second", "+2 days", "+2 days"], ["+1 day & +1 second", "+1 day", "+1 day"], ["Original Date"]]
Ceiling answered 12/9, 2018 at 8:34 Comment(0)
S
0

Modified ElegyD's solution to handle optional date object

    fileprivate func compare(date:Date, to:Date, toGranularity:Calendar.Component)->ComparisonResult{
    return Calendar.current.compare(date, to: to, toGranularity: toGranularity)
}

    func groupSort(ascending: Bool = true, toGranularity:Calendar.Component, byDate dateKey: (Iterator.Element) -> Date?) -> [[Iterator.Element]] {
    var categories: [[Iterator.Element]] = []
    for element in self {
        guard let key = dateKey(element) else {
            continue
        }
        guard let dayIndex = categories.firstIndex(where: { $0.contains(where: {
            if let date = dateKey($0){
                return Calendar.current.isDate(date, inSameDayAs: key)
            }
            return false
        })
        }) else {
            guard let nextIndex = categories.firstIndex(where: {
                $0.contains(where: {
                    if let date = dateKey($0){
                        return self.compare(date: date, to: key, toGranularity: toGranularity) == (ascending ? .orderedDescending : .orderedAscending)
                    }
                    return false
                })
            }) else {
                categories.append([element])
                continue
            }
            categories.insert([element], at: nextIndex)
            continue
        }
        
        guard let nextIndex = categories[dayIndex].firstIndex(where: {
            if let date = dateKey($0){
                return self.compare(date: date, to: key, toGranularity: toGranularity) == (ascending ? .orderedDescending : .orderedAscending)
            }
            return false
        }) else {
            categories[dayIndex].append(element)
            continue
        }
        categories[dayIndex].insert(element, at: nextIndex)
    }
    return categories
}

USAGE struct DataItem{ var date:Date? }

let items = [DataItem(), DataItem(date:Date())] let grouped = items.groupSort(toGranularity: .month, byDate: {$0.date })

Satire answered 27/10, 2021 at 14:38 Comment(0)
F
0
struct Article: Codable {
    let title, publishedDate, cleanURL: String?

    enum CodingKeys: String, CodingKey {
        case title
        case publishedDate = "published_date"
        case cleanURL = "clean_url"
    }
}

 struct Section {
    let title:String
    var article:[Article]
}

 newsResponse.articles?.forEach({ article in
                    if !self.dataSource.contains(where: {$0.title == article.publishedDate}) {
                        self.dataSource.append(Section(title: article.publishedDate ?? "", article: [article]))
                    } else {
                        guard let index = self.dataSource.firstIndex(where: { $0.title == article.publishedDate}) else { return }
                        self.dataSource[index].article.append(article)
                    }
                })
Fabian answered 30/6, 2022 at 4:47 Comment(1)
here we take object and check if date already contains on dataSource. if contains then we find existing array object of dataSource and append new date object other wise we append new section object.Fabian
P
0

Since the date can be converted to a number, we can group objects using not only DateComponents, but also apply custom ranges using regular int and dictionary group

 // multiplier = seconds * minutes * hours * days...etc
 let groupedObjects = Dictionary(grouping: array) { (currentDate) -> Int in
            return Int(currentDate.date.timeIntervalSince1970) / multiplier
 }
 let correctDateObjects = Dictionary(uniqueKeysWithValues: groupedMarkers.map { (key, value) in
            return (key*multiplier, value)
 })

Remember to convert the Int type to Date if you need it

Poulterer answered 14/7, 2022 at 13:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.