Sahil Jaidka
Sahil Jaidka

Reputation: 309

How to group array of objects by date in swift?

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.

Upvotes: 17

Views: 28653

Answers (7)

Artem
Artem

Reputation: 35

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

Upvotes: 0

Mahesh Rathod
Mahesh Rathod

Reputation: 1

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)
                    }
                })

Upvotes: 0

Dattatray Deokar
Dattatray Deokar

Reputation: 2103

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 })

Upvotes: 0

Rick Pasveer
Rick Pasveer

Reputation: 720

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)

Upvotes: 16

ironRoei
ironRoei

Reputation: 2209

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
}

Upvotes: 46

Kakashi
Kakashi

Reputation: 553

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"]]

Upvotes: 9

Nirav D
Nirav D

Reputation: 72410

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.

Upvotes: 9

Related Questions