I have an array of objects that contain Date property. I want to distribute them in UITableView sections according to their month by month order. How I can do that?
My model:
class Birthday: Object {
#objc dynamic var name: String = ""
#objc dynamic var date: Date = Date()
#objc dynamic var dayLeft: Int = 0
#objc dynamic var userImageData: Data?
}
First I use the next code with DateFormatter:
var grouped: [String: [Birthday]] = [:]
grouped = Dictionary(grouping: birthdaysList) { item -> String in
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: item.date)
let date = calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
return date.monthAsString()
}
extension Date {
func monthAsString() -> String {
let df = DateFormatter()
df.setLocalizedDateFormatFromTemplate("MMM")
return df.string(from: self)
}
}
Then I use:
struct Section {
let month: String
let birthdays: [Birthday]
}
var sections: [Section] = []
let keys = Array(grouped.keys)
sections = keys.map({Section(month: $0, birthdays: grouped[$0]!)})
But I need to sort month names in order. January, february etc. How this can be done?
I try to return tuple from my code, but get error
Declared closure result '(String, Int)' is incompatible with contextual type 'String'
grouped = Dictionary(grouping: birthdaysList) { item -> (String, Int) in
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: item.date)
let date = calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
return (date.monthAsString(), components.month!)
}
Create a wrapper struct
struct Section {
let month : String
let birthdays : [Birthday]
}
then map the grouped dictionary to an array of Section
let keys = Array(grouped.keys)
let sections = keys.map({Section(month: $0, birthdays: grouped[$0]!)})
sections represent the sections, birthdays the rows, month the section header titles.
Edit:
To be able to sort the months by their ordinal number return the number in front of the month name
grouped = Dictionary(grouping: birthdaysList) { item -> String in
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month], from: item.date)
let date = calendar.date(from: components) ?? Date(timeIntervalSince1970: 0)
return "\(components.month!)_" + date.monthAsString()
}
And modify the mapping code
let keys = grouped.keys.sorted{$0.localizedStandardCompare($1) == .orderedAscending}
let sections = keys.map({Section(month: $0.components(separatedBy:"_").last!, birthdays: grouped[$0]!)})
Related
I am new to Swift and I have not worked with NSDate. For my app I need how to make to calculate how many days there are until the event. The date of the event is written with DatePicker on Firebase, and I need to calculate from the current date how many days are left until the day it's written. All I need is to count down the days.
Convert your data from DatePicker to Date object, and you can use the following function which returns an Int, a representation of the number of days to a passed date.
func daysTo(date: Date) -> Int? {
let calendar = Calendar.current
let date1 = calendar.startOfDay(for: Date())
let date2 = calendar.startOfDay(for: date)
let components = calendar.dateComponents([.day], from: date1, to: date2)
return components.day
}
you can use this extension for find difference between date:
extension Date {
public func diffrenceTime() -> (Int, Int) {
var cal = Calendar.init(identifier: .persian)
let d1 = Date()
let components = cal.dateComponents([.hour, .minute], from: self, to: d1)
let diffHour = components.hour!
let diffMinute = components.minute!
return (diffHour, diffMinute)
}
public func fullDistance(from date: Date, resultIn component: Calendar.Component, calendar: Calendar = .current) -> Int? {
calendar.dateComponents([component], from: self, to: date).value(for: component)
}
public func distance(from date: Date, only component: Calendar.Component, calendar: Calendar = .current) -> Int {
let days1 = calendar.component(component, from: self)
let days2 = calendar.component(component, from: date)
return days1 - days2
}
public func hasSame(_ component: Calendar.Component, as date: Date) -> Bool {
self.distance(from: date, only: component) == 0
}
}
example for use:
let dateOne = Date() // or any date
let dateTwo = getDateFromServer // your second date for
// option One
let distanceDay = dateOne.fullDistance(from: dateTwo, resultIn: .day)
var cal = Calendar.current // for your calendar
cal.locale = Locale.init(identifier: "EN")
// option Two
let distanceDay = dateOne.fullDistance(from: dateTwo, resultIn: .day, calendar: cal)
you can set hour, minute or any Component for find difference instead of day in result parameter
I have an array of posts which I currently group by day. I use the below functions to do so;
private func splitDay(from date: Date) -> Date {
let calendar = Calendar.current
let components = calendar.dateComponents([.year, .month, .day], from: date)
return calendar.date(from: components)!
}
private func sectionPosts(posts: [Post]) {
let groups = Dictionary(grouping: posts) { (posts) in
return splitDay(from: Date(timeIntervalSince1970: posts.createdOn))
}
self.sections = groups.map { (date, posts) in
return PostSection(date: date, posts: posts)
}
}
However, i'd like to implement a custom grouping which would be the following;
Today
Yesterday
This Week
This Month
Older
How would I build this into my grouping function? My section struct is like such;
struct PostSection {
var date: Date
var posts: [Post]
}
Since you are trying to group your data into groups such as "today", "yesterday", "this week" and "this month", you should first create a type to represent these groups:
enum PostGroup {
case today
case yesterday
case thisWeek
case thisMonth
case older
case coming // added "coming" group so that the groups cover all possible dates
}
Then your PostSection struct would have a PostGroup property, rather than a Date property:
struct PostSection {
let group: PostGroup
let posts: [Post]
}
Now we just need a function that goes (Post) -> PostGroup that we can pass to Dictionary(grouping:by:). This can be implemented just by comparing the date components of the post date with that of today:
func group(for post: Post) -> PostGroup {
let today = Date()
let calendar = Calendar.current
let postDateComponents = calendar.dateComponents([.year, .month, .day], from: post.createdOn)
let todayDateComponents = calendar.dateComponents([.year, .month, .day], from: today)
if postDateComponents == todayDateComponents {
return .today
}
let daysDifference = calendar.dateComponents([.day], from: postDateComponents, to: todayDateComponents)
if daysDifference.day == 1 {
return .yesterday
}
let postWeekComponents = calendar.dateComponents([.weekOfYear, .yearForWeekOfYear], from: post.createdOn)
let todayWeekComponents = calendar.dateComponents([.weekOfYear, .yearForWeekOfYear], from: today)
if postWeekComponents == todayWeekComponents {
return .thisWeek
}
if postDateComponents.year == todayDateComponents.year &&
postDateComponents.month == todayDateComponents.month {
return .thisMonth
}
if post.createdOn < today {
return .older
} else {
return .coming
}
}
To finish it off:
private func sectionPosts(posts: [Post]) {
let groups = Dictionary(grouping: posts, by: group(for:))
self.sections = groups.map { (group, posts) in
return PostSection(group: group, posts: posts)
}
}
This is not an elegant solution, but it solves the problem, please feel free to optimize this function, specially since date calculations can be slow.
private func preSplitProcess(from date: Date) -> Date {
let calendar = Calendar.current
let isToday = calendar.isDateInToday(date)
if isToday {
return date
} else {
let wasYesterday = calendar.isDateInYesterday(date)
if wasYesterday {
return date
} else {
let bowComponents = calendar.dateComponents([.weekOfYear], from: date)
let beginningOfWeek = calendar.date(from: bowComponents)! // Handle errors, please
if beginningOfWeek < date {
return date
} else {
let bomComponents = calendar.dateComponents([.month], from: date)
let beginningsOfMonth = calendar.date(from: bomComponents)! // Handle errors, please
if beginningsOfMonth < date {
return date
} else {
var components = DateComponents()
components.year = 1970
components.month = 01
components.day = 01
let oldDate = calendar.date(from: components)!
return oldDate
}
}
}
}
}
Add this before you call your splitDay function, since this doesn't reduce the return dates to the same date.
I am working on a scheduling app. I want all the dates of the given months I am not able to group dates by months that is what I tried but I want a different expected result
extension Date {
static func dates(from fromDate: Date, to toDate: Date) -> [Date] {
var dates: [Date] = []
var date = fromDate
while date <= toDate {
dates.append(date)
guard let newDate = Calendar.current.date(byAdding: .day, value: 1, to: date) else { break }
date = newDate
}
return dates
}
var month: Int {
return Calendar.current.component(.month, from: self)
}
}
let fromDate = Calendar.current.date(byAdding: .day, value: 30, to: Date())
let datesBetweenArray = Date.dates(from: Date(), to: fromDate!)
var sortedDatesByMonth: [[Date]] = []
let filterDatesByMonth = { month in datesBetweenArray.filter { $0.month == month } }
(1...12).forEach { sortedDatesByMonth.append(filterDatesByMonth($0)) }
The result is in this format [[], [], [], [], [], [], [2019-07-31
03:51:19 +0000],……., [], [], [], []]
This kinda result I want expecting
struct ScheduleDates {
var month: String
var dates: [Date]
init(month: String, dates: [Date]) {
self.month = month
self.dates = dates
}
}
var sections = [ScheduleDates]()
If you want to group your dates by month you can create a Dictionary like this:
Dictionary(grouping: datesBetweenArray, by: { $0.month })
This results in the following output of the format [Int: [Date]]
The key of the dictionary will be your month.
Now you can initialize your scheduleDates struct by looping through this dictionary in this way:
var sections = Dictionary(grouping: datesBetweenArray,
by: ({$0.month}))
.map { tuple in
ScheduleDates(month: String(tuple.0), dates: tuple.1)
}
Here's the code for a Playground
I think your structs should probably be Int values for the months, as when you go to populate something like a tableview, it'll be a PITA to re-order months if you've got them as Strings.
struct ScheduleDates {
var month: String
var dates: [Date]
}
Anyway, here's the extension I wrote based on what you provided. I frankly think you should return a dictionary with an Int as the key and an array of Dates as the value, but here's what you wanted...
I used Dictionary(grouping:by:) to construct a dictionary from an array of dates.
extension Date {
static func dateDictionary(from arrayOfDates: [Date]) -> [String: [Date]] {
// declare a dictionary
return Dictionary(grouping: arrayOfDates) { date -> String in
// get the month as an int
let monthAsInt = Calendar.current.dateComponents([.month], from: date).month
// convert the int to a string...i think you probably want to return an int value and do the month conversion in your tableview or collection view
let monthName = DateFormatter().monthSymbols[(monthAsInt ?? 0) - 1]
// return the month string
return monthName
}
}
}
Here's a utility method I wrote to generate data while I figured out how to do it. If you're going to be in production, don't force unwrap stuff as I did here.
// Utility method to generate dates
func createDate(month: Int, day: Int, year: Int) -> Date? {
var components = DateComponents()
components.month = month
components.day = day
components.year = year
return Calendar.current.date(from: components)!
}
Below is how I generated an array of sample dates to experiment.
// generate array of sample dates
let dateArray: [Date] = {
let months = Array(1...12)
let days = Array(1...31)
let years = [2019]
var dateArray: [Date] = []
while dateArray.count < 100 {
let randomMonth = months.randomElement()
let randomDay = days.randomElement()
let randomYear = years.randomElement()
if let month = randomMonth,
let day = randomDay,
let year = randomYear,
let date = createDate(month: month,
day: day,
year: year) {
dateArray.append(date)
}
}
return dateArray
}()
let monthDictionary = Date.dateDictionary(from: dateArray)
var arrayOfStructs: [ScheduleDates] = []
monthDictionary.keys.forEach { key in
let scheduleDate = ScheduleDates(month: key,
dates: monthDictionary[key] ?? [])
arrayOfStruct.append(scheduleDate)
}
print(arrayOfStructs)
You can use my code. Which i write to adapt your case.
let fromDate = Calendar.current.date(byAdding: .day, value: 30, to: Date())
let datesBetweenArray = Date.dates(from: Date(), to: fromDate!)
if datesBetweenArray.count <= 1 {
print(datesBetweenArray)
}
var sortedDatesByMonth: [[Date]] = []
var tempMonth = datesBetweenArray[0].month
var dates: [Date] = []
for i in 0..<datesBetweenArray.count {
if tempMonth == datesBetweenArray[i].month {
dates.append(datesBetweenArray[i])
if i == datesBetweenArray.count - 1 {
sortedDatesByMonth.append(dates)
}
} else {
sortedDatesByMonth.append(dates)
tempMonth = datesBetweenArray[i].month
dates.removeAll()
dates.append(datesBetweenArray[i])
}
}
print(sortedDatesByMonth.count)
print(sortedDatesByMonth)
I want to group data by dates which has initial format of yyyy-MM-dd'T'HH:mm:ss.SSSZ and display as E, dd MMM. I have managed to group them by the dates but failed to sort the dates in a descending order. How do I sort the date components after grouping in Dictionary?
JSON Response
{
"list": [
{
"userId": "test1",
"transactionTime": "2019-06-20T14:01:00.253+08:00"
},
{
"userId": "test2",
"transactionTime": "2019-06-16T14:02:00.253+08:00"
},
{
"userId": "test3",
"transactionTime": "2019-06-12T14:01:00.253+08:00"
},
{
"userId": "tes4",
"transactionTime": "2019-06-17T14:02:00.253+08:00"
},
]
}
Grouping
func convertToDateObj() -> Date {
let dateFormatter = DateFormatter()
// Convert from initial date string to date object
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let dateObj = dateFormatter.date(from: self)!
return dateObj
}
// Group list by date
let groupedList = Dictionary(grouping: rawTransactionsList, by: { list -> DateComponents in
let dateObj = list.transactionTime.convertToDateObj()
let date = Calendar.current.dateComponents([.day, .month], from: (dateObj))
return date
})
// QUESTION
// How do I sort the keys?
// groupList.keys.sorted(...)?
// Populate my list with grouped list
groupedList.keys.forEach { key in
print("Group keys \(key)")
let values = groupedList[key]
groupedtransactionsList.append(values ?? [])
}
You should probably also group by the year, otherwise the same day in different years will be in the same group:
let date = Calendar.current.dateComponents([.day, .month, .year], from: (dateObj))
One way to sort the keys is to convert the date components back into Dates, using Calendar.current.date(from:):
let sortedList = groupedList.sorted {
Calendar.current.date(from: $0.key) ?? Date.distantFuture <
Calendar.current.date(from: $1.key) ?? Date.distantFuture
}
sortedList.forEach { key, values in
print("Group keys \(key)")
groupedtransactionsList.append(values ?? [])
}
My suggestion is to use a date string with format yyyy-MM-dd as dictionary key rather than date components. This string can be sorted.
let groupedList = Dictionary(grouping: rawTransactionsList, by: { list -> String in
let dateObj = list.transactionTime.convertToDateObj()
let comps = Calendar.current.dateComponents([.day, .month, .year], from: dateObj)
return String(format: "%ld-%.2ld-%.2ld", comps.year!, comps.month!, comps.day!)
})
groupedtransactionsList = groupedList.keys.sorted(by: >).map { groupedList[$0]! }
I have Date() properties. startingAt and endingAt. And an array of Date(), which are alreadyRegistred. I have to create an array of strings with dates between startingAt and endingAt. StartingAt and endingAt are included and the last requirement is to exclude alreadyRegistred dates.
Do you have some elegant idea, how to do it? Thanks for help!
Edit: Maximum number of dates in final array will be about 7 days.
Dont forget that a Date is basically just a timestamp, and that you can have access to the addingTimeInterval(_:) method.
Knowing that, is very easy to do some calculation between two dates.
I do not have the whole knowledge about your required business logic, but here is a naive implementation that generates Dates between two dates. I'm sure you can run it in a playground and explore a little bit.
import UIKit
func intervalDates(from startingDate:Date, to endDate:Date, with interval:TimeInterval) -> [Date] {
guard interval > 0 else { return [] }
var dates:[Date] = []
var currentDate = startingDate
while currentDate <= endDate {
currentDate = currentDate.addingTimeInterval(interval)
dates.append(currentDate)
}
return dates
}
let startingDate = Date() // now
let endDate = Date(timeIntervalSinceNow: 3600 * 24 * 7) // one week from now
let intervalBetweenDates:TimeInterval = 3600 * 3// three hours
let dates:[Date] = intervalDates(from: startingDate, to: endDate, with: intervalBetweenDates)
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .long
let dateStrings = dates.map{dateFormatter.string(from: $0)}
print("NOW : \(startingDate)")
for (index, string) in dateStrings.enumerated() {
print("\(index) : \(string)")
}
print("END DATE : \(endDate)")
Try this and see:
// Start & End date string
let startingAt = "01/01/2018"
let endingAt = "08/03/2018"
// Sample date formatter
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy"
// start and end date object from string dates
var startDate = dateFormatter.date(from: startingAt) ?? Date()
let endDate = dateFormatter.date(from: endingAt) ?? Date()
// String date array, to be excluded
let alreadyRegistred = ["01/01/2018", "15/01/2018", "10/02/2018", "20/02/2018", "05/03/2018"]
// Actual operational logic
var dateRange: [String] = []
while startDate <= endDate {
let stringDate = dateFormatter.string(from: startDate)
startDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate) ?? Date()
if (alreadyRegistred.contains(stringDate)) {
continue
} else {
dateRange.append(stringDate)
}
}
print("Resulting Array - \(dateRange)")
Here is result:
Resulting Array - ["02/01/2018", "03/01/2018", "04/01/2018", "05/01/2018", "06/01/2018", "07/01/2018", "08/01/2018", "09/01/2018", "10/01/2018", "11/01/2018", "12/01/2018", "13/01/2018", "14/01/2018", "16/01/2018", "17/01/2018", "18/01/2018", "19/01/2018", "20/01/2018", "21/01/2018", "22/01/2018", "23/01/2018", "24/01/2018", "25/01/2018", "26/01/2018", "27/01/2018", "28/01/2018", "29/01/2018", "30/01/2018", "31/01/2018", "01/02/2018", "02/02/2018", "03/02/2018", "04/02/2018", "05/02/2018", "06/02/2018", "07/02/2018", "08/02/2018", "09/02/2018", "11/02/2018", "12/02/2018", "13/02/2018", "14/02/2018", "15/02/2018", "16/02/2018", "17/02/2018", "18/02/2018", "19/02/2018", "21/02/2018", "22/02/2018", "23/02/2018", "24/02/2018", "25/02/2018", "26/02/2018", "27/02/2018", "28/02/2018", "01/03/2018", "02/03/2018", "03/03/2018", "04/03/2018", "06/03/2018", "07/03/2018", "08/03/2018"]
let startDate = Date()
let endDate = Date().addingTimeInterval(24*60*60*10) // i did this to get the end date for now
var stringdateArray = [String]()
if let days = getNumberofDays(date1: startDate, date2: endDate) {
for i in 0...days-1 {
let date = startDate.addingTimeInterval(Double(i)*24*3600)
let stringDate = getStringDate(fromDate: date, havingFormat: "yyyy-MM-dd")
if !(alreadyRegisteredArray.contains(stringDate)) { // checking if already registered
stringdateArray.append(stringDate)
}
}
}
and our helper method
let dateFormatter = DateFormatter()
func getStringDate(fromDate: Date,havingFormat: String) -> String {
dateFormatter.dateFormat = havingFormat
dateFormatter.amSymbol = "AM"
dateFormatter.pmSymbol = "PM"
let date = dateFormatter.string(from: fromDate)
return date
}
func getNumberofDays(date1: Date, date2: Date) -> Int? {
let calendar = NSCalendar.current
let date1 = calendar.startOfDay(for: date1)
let date2 = calendar.startOfDay(for: date2)
let components = calendar.dateComponents([.day], from: date1, to: date2)
return components.day
}