When I run the following (on January 7 2017):
let dateComponents = Calendar.current.dateComponents(in: TimeZone.current, from: date)
print("dateComponents = \(dateComponents)")
let trigger = UNCalendarNotificationTrigger(dateMatching:dateComponents, repeats: false)
let nextDate = trigger.nextTriggerDate()
print("nextDate = \(nextDate)")
then I get the following output:
dateComponents = calendar: gregorian (current) timeZone: Europe/Stockholm (current) era: 1 year: 2017 month: 1 day: 8 hour: 21 minute: 34 second: 0 nanosecond: 0 weekday: 1 weekdayOrdinal: 2 quarter: 0 weekOfMonth: 1 weekOfYear: 1 yearForWeekOfYear: 2017 isLeapMonth: false
nextDate = nil
Question: why is trigger.nextTriggerDate() = nil ?
UPDATE: I had the feeling that my dateComponents could be overdetermined. I therefore introduced a nextEvent that only contained the day hour and minute of the dateComponents:
var nextEvent = DateComponents()
nextEvent.day = dateComponents.day
nextEvent.hour = dateComponents.hour
nextEvent.minute = dateComponents.minute
let trigger = UNCalendarNotificationTrigger(dateMatching:nextEvent, repeats: false)
when I now invoke trigger.nextTriggerDate() it becomes
nextDate = Optional(2017-01-08 20:34:00 +0000)
as it should. But I do not understand why I cannot use the dateComponents when I create the trigger.
During my tests I found dateComponents.quarter = 0 to be the cause of the problems. Zero quarter is obviously wrong, it should be quarter = 1 It seems there is an old bug that causes the date components to have an invalid quarter.
If you manually set dateComponents.quarter = 1 or just dateComponents.quarter = nil, everything starts to work.
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 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"
}
}
}
How do you convert a DateComponents to a Date Object.
Currently, I have following code
Calendar.date(from: DateComponents(year: 2018, month: 1, day: 15))
but I'm getting an error stating "No 'date' candidates produce the expected contextual result type 'Date'
Sorry for the basic question, but I'm still struggling with how to work with dates in Swift
You cannot call date(from on the type. You have to use an instance of the calendar, either the current calendar
Calendar.current.date(from: DateComponents(year: 2018, month: 1, day: 15))
or a fixed one
let calendar = Calendar(identifier: .gregorian)
calendar.date(from: DateComponents(year: 2018, month: 1, day: 15))
//create an instance of DateComponents to keep your code flexible
var dateComponents = DateComponents()
//create the date components
dateComponents.year = 2018
dateComponents.month = 1
dateComponents.day = 15
//dateComponents.timeZone = TimeZone(abbreviation: "EST")
dateComponents.hour = 4
dateComponents.minute = 12
//make something useful out of it
func makeACalendarObject(){
//create an instance of a Calendar for point of reference ex: myCalendar, and use the dateComponents as the parameter
let targetCalendar = Calendar.current
let newCalendarObject = targetCalendar.date(from: dateComponents)
guard let newCalObject = newCalendarObject else {
return
}
print(newCalObject)
}
//call the object
makeACalendarObject()
I'm trying to calculate the time from now (i.e. Date()) till the next 5pm.
If the current time is 3pm, the output will be 02:00:00. (in HH:MM:SS)
If the current time is 6pm, the output will be 23:00:00. (until the next 5pm!)
How do I do that in Swift 3?
Thanks.
You can use Calendar.nextDate to find the Date of the coming 5pm.
let now = Date()
let calendar = Calendar.current
let components = DateComponents(calendar: calendar, hour: 17) // <- 17:00 = 5pm
let next5pm = calendar.nextDate(after: now, matching: components, matchingPolicy: .nextTime)!
then, just compute the different between next5pm and now using dateComponents(_:from:to:).
let diff = calendar.dateComponents([.hour, .minute, .second], from: now, to: next5pm)
print(diff)
// Example outputs:
// hour: 2 minute: 21 second: 39 isLeapMonth: false
// hour: 23 minute: 20 second: 10 isLeapMonth: false
func tommorrow5() ->Date {
var todayAt5 = Calendar.current.dateComponents([.year,.month,.day,.hour], from: Date() )
todayAt5.hour = 17
let dateToDisplay = Calendar.current.date(from: todayAt5)
return Calendar.current.date(byAdding: .day , value: 1, to: dateToDisplay!)! }
func showTimeDifference()->DateComponents{
var todayAt5 = Calendar.current.dateComponents([.year,.month,.day,.hour], from: Date() )
todayAt5.hour = 17
let dateToDisplay = Calendar.current.date(from: todayAt5)
let now = Date()
switch now.compare(dateToDisplay!) {
case .orderedAscending : // now is earlier than 5pm
return Calendar.current.dateComponents([Calendar.Component.hour, Calendar.Component.minute, Calendar.Component.second], from: Date(), to: dateToDisplay!)
case .orderedDescending : // now is later than 5 pm
return Calendar.current.dateComponents([Calendar.Component.hour, Calendar.Component.minute, Calendar.Component.second], from: Date(), to: tommorrow5())
case .orderedSame : break // now is 5 pm
}
return Calendar.current.dateComponents([Calendar.Component.hour, Calendar.Component.minute, Calendar.Component.second], from: Date(), to: dateToDisplay!)
}
you can use it just like this
it will return time to 5pm today
if its already passed 5pm will return time to 5pm tomorrow
showTimeDifference()
How might the day number of the year be found with swift? Is there a simple way that I'm not seeing, or do I have to find the number of seconds from Jan 1 to the current date and divide by the number of seconds in a day?
This is a translation of the answer to How do you calculate the day of the year for a specific date in Objective-C? to Swift.
Swift 2:
let date = NSDate() // now
let cal = NSCalendar.currentCalendar()
let day = cal.ordinalityOfUnit(.Day, inUnit: .Year, forDate: date)
print(day)
Swift 3:
let date = Date() // now
let cal = Calendar.current
let day = cal.ordinality(of: .day, in: .year, for: date)
print(day)
This gives 1 for the first day in the year, and 56 = 31 + 25 for today (Feb 25).
... or do I have to find the number of seconds from Jan 1 to the current date
and divide by the number of seconds in a day
This would be a wrong approach, because a day does not have a fixed
number of seconds (transition from or to Daylight Saving Time).
Swift 3
extension Date {
var dayOfYear: Int {
return Calendar.current.ordinality(of: .day, in: .year, for: self)!
}
}
use like
Date().dayOfYear
Not at all !!! All you have to do is to use NSCalendar to help you do your calendar calculations as follow:
let firstDayOfTheYear = NSCalendar.currentCalendar().dateWithEra(1, year: NSCalendar.currentCalendar().component(.CalendarUnitYear, fromDate: NSDate()), month: 1, day: 1, hour: 0, minute: 0, second: 0, nanosecond: 0)! // "Jan 1, 2015, 12:00 AM"
let daysFromJanFirst = NSCalendar.currentCalendar().components(.CalendarUnitDay, fromDate: firstDayOfTheYear, toDate: NSDate(), options: nil).day // 55
let secondsFromJanFirst = NSCalendar.currentCalendar().components(.CalendarUnitSecond, fromDate: firstDayOfTheYear, toDate: NSDate(), options: nil).second // 4,770,357
You can find the number of days since your date like this:
let date = NSDate() // your date
let days = cal.ordinalityOfUnit(.CalendarUnitDay, inUnit: .CalendarUnitYear, forDate: date)
println(days)