I have an array of Date objects like the following example:
let dates = [date1, date2, date3, date4, date5]
How can I create subarrays for dates that have the same day of month, month and year.
Example:
date1 = 20/04/2020
date2 = 19/04/2020
date3 = 19/04/2020
date4 = 18/04/2020
date5 = 18/04/2020
...
With this I wanted the following:
let group1 = [date1]
let group2 = [date2, date3]
let group3 = [date4, date5]
let group4 = //...
I have tried DateComponents and .map, .filter, etc functions but somehow I cannot do this.
Can I get a hint?
thanks.
The code extracts the date components for year, month and day from each date and groups by them and then maps the values from the grouped result to an array of arrays per date. To be able to have the original dates in the end result a tuple is used as value type for the dictionary
let groupedDates = Dictionary(grouping: dates
.map { ($0,Calendar.current.dateComponents([.year, .month, .day], from: $0))}, by: {$0.1})
.mapValues {value in value.map {$0.0}}
.values
Just create structure that tracks the year, month, and day.
struct DateCluster {
let day: Int
let month: Int
let year: Int
var dates: [Date] = []
init(date: Date) {
let cal = Calendar.current
day = cal.component(.day, from: date)
month = cal.component(.month, from: date)
year = cal.component(.year, from: date)
dates.append(date)
}
func doesDateBelong(_ date: Date) -> Bool {
let cal = Calendar.current
return cal.component(.day, from: date) == day
&& cal.component(.month, from: date) == month
&& cal.component(.year, from: date) == year
}
}
And here's the usage:
func makeDate(year: Int, month: Int, day: Int) -> Date? {
let cal = Calendar.current
let components = DateComponents(year: year, month: month, day: day)
return cal.date(from: components)
}
var clusters: [DateCluster] = []
var dates = [
makeDate(year: 2020, month: 4, day: 20),
makeDate(year: 2020, month: 4, day: 19),
makeDate(year: 2020, month: 4, day: 19),
makeDate(year: 2020, month: 4, day: 18),
makeDate(year: 2020, month: 4, day: 18),
].compactMap { $0 }
for date in dates {
if let index = clusters.firstIndex(where: { $0.doesDateBelong(date) }) {
clusters[index].dates.append(date)
} else {
clusters.append(.init(date: date))
}
}
The result below will be an array of an array of dates all grouped by year, month, and day.
let groupedDates = clusters.map { $0.dates }
Related
I have the following range:
Weekday Hour:Minute --> Weekday Hour:Minute
Weekday is an integer from 1 (Monday) to 7 (Sunday)
For example, a given range can be the following:
Monday 8:00 --> Friday 18:00
Saturday 10:00 --> Tuesday 10:00
Monday 10:00 --> Monday 20:00 (Just 10 hours on Monday)
Monday 20:00 --> Monday 10:00 (All week except Monday from 10:00 to 20:00)
I'm trying to find if the current date is in the selected range.
I tried multiple ways like creating NSDates from the ranges and comparing them but it still didn't pass all the tests.
You could do something like this:
First create the start and end dates from hour, minute and weekday. If start date results after end date go back to 1 week. Then compare the current date to start and end dates calculated to see if it is in range.
let currentDate = Date()
var startDate = dateBySetting(hour: 8, minute: 0, weekday: 1, of: currentDate)
let endDate = dateBySetting(hour: 9, minute: 0, weekday: 5, of: currentDate)
// If start date results after end date remove a week from the start
if startDate > endDate {
startDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: startDate) ?? startDate
}
let dateIsInRange = startDate <= currentDate && currentDate <= endDate
func dateBySetting(hour: Int, minute: Int, weekday: Int, of date: Date) -> Date {
let calendar = Calendar.current
var date = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: date) ?? date
date = calendar.date(bySetting: .weekday, value: weekday, of: date) ?? date
return date
}
It can be done using Calendar's nextDate function.
Eg: here I am checking if my current time falls in between Monday 8:00 --> Friday 18:00
//Monday 8:00 --> Friday 18:00
let currentDate = Date()
let calendar = Calendar.current
let yearComponent = (calendar.dateComponents(in: .current, from: Date()).year)!
let monthComponent = (calendar.dateComponents(in: .current, from: Date()).month)!
let previousTime = calendar.nextDate(after: currentDate,
matching: DateComponents(calendar: calendar, timeZone: .current, year: yearComponent, month: monthComponent, hour: 8, minute: 0, weekday: 2),
matchingPolicy: .strict,
repeatedTimePolicy: .first,
direction: .backward)
print(previousTime)
let nextTime = calendar.nextDate(after: previousTime!,
matching: DateComponents(calendar: calendar, timeZone: .current, year: yearComponent, month: monthComponent, hour: 18, minute: 0, weekday: 6),
matchingPolicy: .strict,
repeatedTimePolicy: .first,
direction: .forward)
print(nextTime)
print(currentDate > previousTime! && currentDate < nextTime!) //true
A solution is to use date components. Your last example Monday 20:00 --> Monday 10:00 is
let startComponents = DateComponents(hour:20, minute:0, weekday:1)
let endComponents = DateComponents(hour:10, minute:0, weekday:1)
Then get the next occurrence of the first components from the current date backward and the second forward
let now = Date()
let startDate = Calendar.current.nextDate(after: now, matching: startComponents, matchingPolicy: .nextTime, direction: .backward)!
let endDate = Calendar.current.nextDate(after: startDate , matching: endComponents, matchingPolicy: .nextTime)!
and create a DateInterval and check if the current date is in the range
let isInRange = DateInterval(start: startDate, end: endDate).contains(now)
extension Date {
func isSameWeek(as date: Date) -> Bool {
let dateComponents = Calendar.current.dateComponents([.day, .weekOfYear, .year], from: date)
let currentDateComponents = Calendar.current.dateComponents([.day, .weekOfYear, .year], from: self)
return dateComponents.year == currentDateComponents.year && dateComponents.weekOfYear == currentDateComponents.weekOfYear
}
}
you can try this extension:-
extension Date{
func timeAgoDisplay() -> String {
let secondsAgo = Int(Date().timeIntervalSince(self))
let minute = 60
let hour = minute * 60
let day = hour * 24
let week = day * 7
let month = week * 4
if secondsAgo < 60 {
return "\(secondsAgo) seconds ago"
}else if secondsAgo < hour {
return "\(secondsAgo/minute) minutes ago"
}else if secondsAgo < day {
return "\(secondsAgo/hour) hours ago"
}else if secondsAgo < week {
return "\(secondsAgo/day) days ago"
}else if secondsAgo < month {
return "\(secondsAgo/week) weeks ago"
}else {
return "\(secondsAgo/month) month(s) ago"
}
}
}
I'm trying to work out how many days in a month there are for each day of the week. I've used this question / answer below as a basis for what I want to achieve and it's working for the most part.
how can we get the number of sundays on an given month ? ( swift )
Unfortunately it's also calculating the 1st day of the following month, if it's a weekday.
I'm not familiar enough with how the code is calculated to be able to understand whether there is anywhere I can add a -1 or something to the total days of the month.
If anyone could recommend a solution it would be most appreciated.
I've tried changing the "numberOfSundays += 1" to "numberOfSundays += 0" as I thought that might be causing the issue.
func getNumberOfDaysInMonth (month : Int, Year : Int, nameOfDay: String) -> Int? {
var dateComponents = DateComponents()
dateComponents.year = Year
dateComponents.month = month
let calendar = NSCalendar.current
let date = calendar.date(from: dateComponents)
guard let range = calendar.range(of: .day, in: .month, for: date!) else { return nil }
let numDays = range.endIndex
// New code starts here:
var numberOfSundays = 0
let dateFormatter = DateFormatter()
for day in 1...numDays {
dateComponents.day = day
guard let date = calendar.date(from: dateComponents) else { return nil }
dateFormatter.dateFormat = "EEEE"
let dayOfWeek = dateFormatter.string(from: date) // Get day of week
if dayOfWeek == nameOfDay { // Check if it's a Monday
numberOfSundays += 1
}
}
return numberOfSundays
}
getNumberOfDaysInMonth(month: 06, Year: 2019, nameOfDay: "Monday")
getNumberOfDaysInMonth(month: 06, Year: 2019, nameOfDay: "Tuesday")
getNumberOfDaysInMonth(month: 06, Year: 2019, nameOfDay: "Wednesday")
getNumberOfDaysInMonth(month: 06, Year: 2019, nameOfDay: "Thursday")
getNumberOfDaysInMonth(month: 06, Year: 2019, nameOfDay: "Friday")
Where I call the function "getNumberOfDaysInMonth(month: 06, Year: 2019, nameOfDay: "Monday")" it returns 5 as the number of days in the month of June 2019, in actual fact there are 4.
func getNumberOfDaysInMonth(month: Int , year: Int, nameOfDay: String) -> Int? {
var components = DateComponents()
components.year = year
components.month = month
let calendar = Calendar.current
guard let date = calendar.date(from: components),
let range = calendar.range(of: .day, in: .month, for: date) else { return nil }
return range
.map { DateComponents(year: year, month: month, day: $0) }
.map { calendar.date(from: $0) }
.compactMap { date -> String? in
guard let date = date else { return nil }
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: date)
}
.filter { $0 == nameOfDay }
.count
}
It's a bit hard to find in the documentation of Range, however, the documentation comment for .endIndex property is:
The collection's "past the end" position---that is, the position one greater than the last valid subscript argument.
Anyway, you should not be using indices the calculate the size.
Correctly, you should use:
let numDays = range.upperBound - range.lowerBound
or simply
let numDays = range.count
to correctly calculate the number of days.
I have a function below (datesCheck) that cycles through an array of dates and firstly removes any entries if there is more than one a day and then checks whether the dates are consecutive within the array, returning the number of consecutive days from the current date.
func datesCheck(_ dateArray: [Date]) -> Int {
let calendar = Calendar.current
let uniqueDates = NSSet(array: dateArray.map { calendar.startOfDay(for: $0) }).sorted {
($0 as AnyObject).timeIntervalSince1970 > ($1 as AnyObject).timeIntervalSince1970
} as! [Date]
var lastDate = calendar.startOfDay(for: Date())
var i = 0
while i < uniqueDates.count && uniqueDates[i].compare(lastDate) == .orderedSame {
lastDate = (calendar as NSCalendar).date(byAdding: .day, value: -1, to: lastDate, options: [])!
i += 1
}
numberOfConsecutiveDays = i
return i
}
This function works well but I want to only apply this to dates that are Monday – Friday, with the consecutive dates checker checking Friday – Monday, effectively ignoring saturday and sunday. I have tried to achieve this using calendar.components but cannot find a way to ignore weekends when checking if the dates are consecutive excluding weekends.
let today = Date()
let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
let components = calendar!.components([.weekday], fromDate: today)
if components.weekday == 2 {
print("Monday")
} else {
print("Monday")
}
A couple points:
Since you don't need weekends, filter them out
Your function is non-deterministic, since it uses the current time (Date()). The result is theoretically different for every run. I added a second parameter fromDate to make it deterministic.
func datesCheck(_ dates: [Date], fromDate: Date = Date()) -> Int {
let calendar = Calendar.current
let weekdays = dates.map { calendar.startOfDay(for: $0) }
.filter { 2...6 ~= calendar.component(.weekday, from: $0) }
let uniqueDates = Set(weekdays).sorted(by: >)
var i = 0
var lastDate = calendar.startOfDay(for: fromDate)
while i < uniqueDates.count && uniqueDates[i] == lastDate {
switch calendar.component(.weekday, from: uniqueDates[i]) {
case 2:
// When the lastDate is a Monday, the previous weekday is 3 days ago
lastDate = calendar.date(byAdding: .day, value: -3, to: lastDate)!
default:
// Otherwise, it's 1 day ago
lastDate = calendar.date(byAdding: .day, value: -1, to: lastDate)!
}
i += 1
}
return i
}
Test:
let dates = [
DateComponents(calendar: .current, year: 2017, month: 8, day: 29).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 28).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 25).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 24).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 22).date!,
DateComponents(calendar: .current, year: 2017, month: 8, day: 21).date!
]
let today = DateComponents(calendar: .current, year: 2017, month: 8, day: 29).date!
print(datesCheck(dates, fromDate: today)) // 4
The code prints 4 since the gap between the 28th and 25th is ignored according to the weekend rule.
I have an array of journal entries with timestamps.
class JournalNoteEntry {
var timeCreated: NSDate?
var value: Int?
var note: String?
}
How can I filter this array for entries of Today, Yesterday, This Week, This Month, etc. and plot the my value vs. timeCreated and label the X-axis
I have searched a lot for this basic need but unable to find answers.
Can someone please help...
Since you are writing in Swift 3, you should use the native Swift's type rather than the Foundation's classes (those that start with NS...):
class JournalNoteEntry {
var timeCreated: Date?
var value: Int?
var note: String?
}
On to your problem: let's write a simple function to filter for all entry between a start and end date:
func entries(in journal: [JournalNoteEntry], from: Date, to: Date) -> [JournalNoteEntry] {
return journal.filter {
guard let timeCreated = $0.timeCreated else {
return false
}
// Notice that this is half-open range
return from <= timeCreated && timeCreated < to
}
}
The majority of your efforts will be on determining the date ranges. One way to do it is via an extension to the Calendar class:
extension Calendar {
func todayRange() -> (Date, Date) {
let startDate = self.startOfDay(for: Date())
let endDate = self.date(byAdding: .day, value: 1, to: startDate)!
return (startDate, endDate)
}
func yesterdayRange() -> (Date, Date) {
let endDate = self.startOfDay(for: Date())
let startDate = self.date(byAdding: .day, value: -1, to: endDate)!
return (startDate, endDate)
}
func thisWeekRange() -> (Date, Date) {
var components = DateComponents()
components.weekday = self.firstWeekday
let startDate = self.nextDate(after: Date(), matching: components, matchingPolicy: .nextTime, direction: .backward)!
let endDate = self.nextDate(after: startDate, matching: components, matchingPolicy: .nextTime)!
return (startDate, endDate)
}
func thisMonthRange() -> (Date, Date) {
var components = self.dateComponents([.era, .year, .month], from: Date())
components.day = 1
let startDate = self.date(from: components)!
let endDate = self.date(byAdding: .month, value: 1, to: startDate)!
return (startDate, endDate)
}
}
// Run this in Playground to see it formatted in your local timezone
// If you call print(), it will always display in UTC
// Assuming today = Nov 7, 2016
Calendar.current.todayRange() // "Nov 7, 2016, 12:00 AM", "Nov 8, 2016, 12:00 AM"
Calendar.current.yesterdayRange() // "Nov 6, 2016, 12:00 AM", "Nov 7, 2016, 12:00 AM"
Calendar.current.thisWeekRange() // "Nov 6, 2016, 12:00 AM", "Nov 13, 2016, 12:00 AM"
Calendar.current.thisMonthRange() // "Nov 1, 2016, 12:00 AM", "Dec 1, 2016, 12:00 AM"
To get all the journal entries within the current month:
// journal is an array of JournalNoteEntry
let (startDate, endDate) = Calendar.current.thisMonthRange()
let results = entries(in: journal, from: startDate, to: endDate)
let predicate = NSPredicate(format: "timeCreated == %f", NSDate().timeIntervalSince1970)
let arrFiltedArray = array .filteredArrayUsingPredicate(predicate)
where timeCreated contain timeIntervalSince1970 for your date, Means convert your datatype from NSDate to double
In my app a user can select whether or not they are under 18 or over 18 years of age. The user enters their Date of Birth using a Date Picker. I need to make a function that will compare the current date in MM/DD/YYYY format to the DatePicker date to see if the user's entered age is over 18.
My current function for setting the DatePicker text to the associated textfield is:
func updatePicker() {
var dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy"
dob.text = dateFormatter.stringFromDate(datePickerView.date)
}
When the user tries to go the next page, the form is validated which is when I need to compare the dates and display the alert if they're under 18.
Just not sure where to start with date string evaluation.
Use this function to compare date
func compareDate(date1: NSDate!, toDate date2: NSDate!, toUnitGranularity unit: NSCalendarUnit) -> NSComparisonResult
see this question
You can use NSCalendar components method and extract NSCalendarUnit.CalendarUnitYear difference from two dates:
extension NSDate {
var is18yearsOld:Bool {
return NSDate().yearsFrom(self) > 18
}
func yearsFrom(date:NSDate) -> Int { return NSCalendar.currentCalendar().components(NSCalendarUnit.CalendarUnitYear, fromDate: date, toDate: self, options: nil).year }
}
let dob1 = NSCalendar.currentCalendar().dateWithEra(1, year: 1970, month: 3, day: 27, hour: 7, minute: 19, second: 26, nanosecond: 0)!
let dob2 = NSCalendar.currentCalendar().dateWithEra(1, year: 2000, month: 3, day: 27, hour: 7, minute: 19, second: 26, nanosecond: 0)!
let is18years1 = dob1.is18yearsOld // true
let is18years2 = dob2.is18yearsOld // false
don't convert the date to text and later convert the text to the date again.
just keep the date from the datePicker around till you need it for the validation.
have a member variable var selectedDate : NSDate?
then later to check if older than 18 just do
if let userDate=self.selectedDate {
if(NSDate().timeIntervalSinceDate(userDate) >= (568024668 /*18 years in seconds*/) {
.... /*is 18*/
}
}
Firstly, you have to get parts of date as Int type using Calendar component methods:
// Current date
let currentDate = Date()
let calendar = Calendar.current
let currentYear = calendar.component(.year, from: currentDate) //year: Int
let currentMonth = calendar.component(.month, from: currentDate) //month: Int
let currentDay = calendar.component(.day, from: currentDate) //day: Int
// DatePicker's date
let yearFromDatePicker = calendar.component(.year, from: datePicker.date) //year: Int
let monthFromDatePicker = calendar.component(.month, from: datePicker.date) //month: Int
let dayFromDatePicker = calendar.component(.day, from: datePicker.date) //day: Int
and then you can compare parts of the date with each other based on your conditions.