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.
Related
May I know, what is reliable way, to calculate day differences without taking time into consideration?
A similar question is asked before. However, the highest voted and accepted answer isn't entirely accurate - https://stackoverflow.com/a/28163560/72437
The code is broken, when dealing with Day light saving case. You can run the following code in Playground
Use startOfDay (Broken)
import UIKit
struct LocalDate: Equatable {
let year: Int
let month: Int
let day: Int
}
struct LocalTime: Equatable, Codable {
let hour: Int
let minute: Int
}
extension Date {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}
static func of(localDate: LocalDate, localTime: LocalTime) -> Date {
var dateComponents = DateComponents()
dateComponents.year = localDate.year
dateComponents.month = localDate.month
dateComponents.day = localDate.day
dateComponents.hour = localTime.hour
dateComponents.minute = localTime.minute
dateComponents.second = 0
return Calendar.current.date(from: dateComponents)!
}
func adding(_ component: Calendar.Component, _ value: Int) -> Date {
return Calendar.current.date(byAdding: component, value: value, to: self)!
}
}
// During 22 March 2021, Tehran will advance by 1 hour from 00:00 AM, to 01:00 AM.
let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
NSTimeZone.default = oldDefault
}
// Just a random local time. We will use 'startOfDay' to perform local time resetting.
let localTime = LocalTime(hour: 2, minute: 59)
let localDate1 = LocalDate(year: 2021, month: 3, day: 22)
let localDate2 = LocalDate(year: 2021, month: 3, day: 23)
let date1 = Date.of(localDate: localDate1, localTime: localTime).startOfDay
let date2 = Date.of(localDate: localDate2, localTime: localTime).startOfDay
let components = Calendar.current.dateComponents([.day], from: date1, to: date2)
/*
date1 Monday, March 22, 2021 at 1:00:00 AM Iran Daylight Time
date2 Tuesday, March 23, 2021 at 12:00:00 AM Iran Daylight Time
diff in day is Optional(0)
*/
print("date1 \(date1.description(with: .current))")
print("date2 \(date2.description(with: .current))")
print("diff in day is \(components.day)")
The different of day should be 1, without taking time into consideration. However, due to day light saving, the computed hour difference is 23 hours instead of 24 hours.
We are then getting 0 day difference.
One of the workaround, is using 12:00 (noon) as local time, with an assumption there is no place in this world, where day light saving occurs during 12:00. I am not sure how solid is this assumption. Such assumption seems to be pretty fragile. What if one day government decides to admen day light saving to be at 12:00?
Use 12:00 (Seems to work. But, how solid it is?)
import UIKit
struct LocalDate: Equatable {
let year: Int
let month: Int
let day: Int
}
struct LocalTime: Equatable, Codable {
let hour: Int
let minute: Int
}
extension Date {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}
static func of(localDate: LocalDate, localTime: LocalTime) -> Date {
var dateComponents = DateComponents()
dateComponents.year = localDate.year
dateComponents.month = localDate.month
dateComponents.day = localDate.day
dateComponents.hour = localTime.hour
dateComponents.minute = localTime.minute
dateComponents.second = 0
return Calendar.current.date(from: dateComponents)!
}
func adding(_ component: Calendar.Component, _ value: Int) -> Date {
return Calendar.current.date(byAdding: component, value: value, to: self)!
}
}
// During 22 March 2021, Tehran will advance by 1 hour from 00:00 AM, to 01:00 AM.
let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
NSTimeZone.default = oldDefault
}
// Use noon
let localTime = LocalTime(hour: 12, minute: 00)
let localDate1 = LocalDate(year: 2021, month: 3, day: 22)
let localDate2 = LocalDate(year: 2021, month: 3, day: 23)
let date1 = Date.of(localDate: localDate1, localTime: localTime)
let date2 = Date.of(localDate: localDate2, localTime: localTime)
let components = Calendar.current.dateComponents([.day], from: date1, to: date2)
/*
date1 Monday, March 22, 2021 at 12:00:00 PM Iran Daylight Time
date2 Tuesday, March 23, 2021 at 12:00:00 PM Iran Daylight Time
diff in day is Optional(1)
*/
print("date1 \(date1.description(with: .current))")
print("date2 \(date2.description(with: .current))")
print("diff in day is \(components.day)")
May I know, what is reliable way, to calculate day differences without taking time into consideration?
Date is a precise point in time, hence expressible as a TimeInterval (aka Double) from an exact moment in time (that'll be reference date aka January 1st 2001 00:00 GMT+0).
Thus that same point in time is differently calculated between TimeZones through Calendar: if the TimeZone has daylight savings, then the calendar take it into account.
Therefore when you operate through a Calendar adopting DateComponents you should keep that in mind.
Depending on what you are trying to do in your application it could be useful to just adopt a private Calendar instance set to adopt TimeZone(secondsFromGMT: 0)! for calculating dates as absolutes values.
As in:
extension Calendar {
static let appCal: Self = {
// I'm used to reason with Gregorian calendar
var cal = Calendar(identifier: .gregorian)
// I just need this calendar for executing absolute time calculations
cal.timeZone = TimeZone(secondsFromGMT: 0)!
return cal
}()
}
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 }
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 to create a date...
func date(withYear year: Int, month: Int, day: Int) -> Date {
let components = DateComponents(calendar: Calendar.current, timeZone: nil, era: nil, year: year, month: month, day: day, hour: 0, minute: 0, second: 0, nanosecond: 0, weekday: nil, weekdayOrdinal: nil, quarter: nil, weekOfMonth: nil, weekOfYear: nil, yearForWeekOfYear: nil)
return components.date!
}
It just takes the day, month and year as a convenience.
So I create two dates.
let startDate = date(withYear: 2016, month: 9, day: 30) // prints to console as 2016-09-29 23:00:00 +0000
let endDate = date(withYear: 2016, month: 11, day: 1) // prints to console as 2016-11-01 00:00:00 +0000
And then calculate the number of months between them...
let components = Calendar.current.dateComponents([.month], from: startDate, to: endDate)
print(components.month) // returns 1
Shouldn't this be 2? How is the month component calculated? Is it just the number of days divided by 30 or something? If so I'll need a new way of calculating the number of months here.
I have the same issue, but NSCalendar is working fine this code example proofs this.
let date = NSDate()
let calendar = NSCalendar.currentCalendar()
let components = calendar.components([.Day , .Month , .Year], fromDate: date)
let twoMonthdate = calendar.dateByAddingUnit(.Month, value: 2, toDate:date, options: [])
let difference = calendar.components([.Month], fromDate: date, toDate: twoMonthdate!, options: [])
//differences is 2 monthses
In you're case difference is 1 month and a few days. And that is why you receive such result.