17

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.

2
  • 1
    How are you grouping the objects? Based on what logic? Commented May 29, 2017 at 12:12
  • 1
    post your solution so far. Commented May 29, 2017 at 12:13

7 Answers 7

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
}
Sign up to request clarification or add additional context in comments.

Comments

17

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)

Comments

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.

Comments

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

Comments

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

Comments

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

1 Comment

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.
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.