Lets imagine we have time line with two points:
A - start time of day
B - end time of day
extension Date: {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}
var endOfDay: Date? {
var components = DateComponents()
components.day = 1
components.second = -1
return Calendar.current.date(byAdding: components, to: startOfDay)
}
}
On the time line there is going one event -> it goes from specifed time, has event time stamp as an interval from 1970 ( event.eventTimestamp ) and there are two options :
It doesn't have end time -> it goes out of bounds after B- point ( duration = 0, to check for it we need to use Int(Date().timeIntervalSince1970) - event.eventTimeStamp
It does have specified end time -> We do have duration.
In both cases It might start few days before A and finish few days after B.
How could I check whenever event is in current day -> specifed by user. After we detect this let's save in variable 24h.
let duration = 24 * 60 * 60
struct Event {
var eventTimestamp: Int
var duration: Int = 0
}
Related
I have a UIDatePicker and set minimum date and minute interval, however there is a visual bug when selecting date. Minimum minute is selectable but appears with the non-selectable grey color.
My code:
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
datePicker.preferredDatePickerStyle = .wheels
datePicker.minuteInterval = 5
datePicker.minimumDate = Date(timeIntervalSinceNow: 7200) // 7200 is equal to 2 hours
}
Screenshot:
I think the date picker is somewhat confused because the minimum date isn't a multiple of five minutes. For example, if you opened the app at 12:34, then Date(timeIntervalSinceNow: 7200) is going to be 14:34, and that doesn't correspond to any date on the date picker, which might be why it is behaving weirdly like this. I don't think this intended though.
Anyway, if you always give it a date that is a multiple of 5 minutes (or whatever minuteInterval you have), then it will work as expected.
From your description of the desired behaviour, it seems like you want to find the next five minute mark if the current minute component is not divisible by 5, then add 2 hours. You can do:
let calendar = Calendar.current
let now = Date()
let currentMinute = calendar.component(.minute, from: now)
let mod = currentMinute % 5
let minutesUntilNextMultipleOf5 = mod == 0 ? mod : 5 - mod
let minDate = calendar.date(byAdding:
DateComponents(hour: 2, minute: minutesUntilNextMultipleOf5),
to: now)
datePicker.minimumDate = minDate
I am trying to calculate the proportion of a day that a specific time equates to. For example, 06:00 is 0.25, 18:00 is 0.75 etc.
I am evaluating a series of dates of Date type, which were created in timeZone = "GMT". The routine below works fine. However when I evaluate a time after 23:00 for dates in DST, then the calculation goes wrong, as the time is evaluated as the next day (e.g. 23:08 is evaluated as 00:08)
Is there any way that I can recognise when the move from GMT to DST takes the date into the next day? I can then adjust the calculation accordingly.
My function for determining the proportion that the input time represents is:
func getTimeAsProportionOfDay(time: Date) -> Double {
//calculates the amount of day, between 0 and 1 given the input date
let calendar = Calendar.current
let hours = calendar.component(.hour, from: time)
let minutes = calendar.component(.minute, from: time)
let seconds = calendar.component(.second, from: time)
let totalSeconds = Double(hours * 60 * 60 + minutes * 60 + seconds)
return Double(totalSeconds) / Double(secondsInDay)
}
Also, I'm aware that my constant of secondsInDay (= 24*60*60) may not be technically correct but I'm not sure what system constant to replace it with.
Thanks.
You just need get the day after the original date and subtract a second. Then calculate the number of seconds in that date using calendar method
func ordinality(of smaller: Calendar.Component, in larger: Calendar.Component, for date: Date) -> Int?
You can make your life easier with some helpers
extension Date {
var dayAfter: Date { Calendar.current.date(byAdding: .day, value: 1, to: noon)!}
var noon: Date { Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)! }
var startOfDay: Date { Calendar.current.startOfDay(for: self) }
var endOfDay: Date { Calendar.current.date(byAdding: .init(second: -1), to: dayAfter.startOfDay)! }
}
Testing the endOfDay
Date().endOfDay // "Feb 7, 2020 at 11:59 PM"
And your method:
func getTimeAsProportionOfDay(time: Date) -> Double {
// discarding the fractional seconds
let time = Calendar.current.date(bySetting: .nanosecond, value: 0, of: time)!
return Double(Calendar.current.ordinality(of: .second, in: .day, for: time)!-1) /
Double(Calendar.current.ordinality(of: .second, in: .day, for: time.endOfDay)!-1)
}
Playground testing:
let date = DateComponents(calendar: .current, year: 2020, month: 2, day: 7, hour: 23, minute: 08).date!
date.endOfDay
let result = getTimeAsProportionOfDay(time: date) // 0.9639000451394113
Generally speaking, I would do something like this:
let date = Date()
var dayStart = Date()
var dayDuration: TimeInterval = 0
Calendar.current.dateInterval(of: .day, start: &dayStart, interval: &dayDuration, for: date)
let timeInterval = date.timeIntervalSince(dayStart)
let percentage = timeInterval / dayDuration
print(percentage)
All, thanks for your help. I think I have found a solution to this by using a fixed start of day to compare against.
func getTimeAsProportionOfDay(time: Date) -> Double {
//calculates the amount of day, between 0 and 1 given the input date
if Int(time.timeIntervalSince(tides.tideDate)) > secondsInDay { //time has been moved to next day by BST change) so return 1 for gradient
return 1.0
} else {/// this was my original code
let calendar = Calendar.current
let hours = calendar.component(.hour, from: time)
let minutes = calendar.component(.minute, from: time)
let seconds = calendar.component(.second, from: time)
let totalSeconds = Double(hours * 60 * 60 + minutes * 60 + seconds)
return Double(totalSeconds) / Double(secondsInDay)
}
}
I have 2 TimeIntervals, which just represent date-agnostic times of day (e.g. 8:00 AM and 5:00 PM). So 0 represents exactly midnight, in this case, and 29,040 represents 8:04 AM. I want to check if the phone's time of day is between the two TimeIntervals.
I found a few similar Stack Overflow questions, but none of them really dealt with TimeIntervals. It seems like just using start <= Date().timeIntervalSinceReferenceDate <= end or something wouldn't work, because it would return a huge value.
What's the best way to handle this type of situation in Swift 3?
Edit: To clarify, I don't need to worry about things like daylight savings. As an example, assume that the user only wants certain things in the app to happen between the hours of X and Y, where X and Y are given to me as TimeInterval values since midnight. So it should be sufficient to check if the phone's TimeInterval since midnight on a typical day is between X and Y before completing the action.
Date().timeIntervalSinceReferenceDate returns the number of seconds since Jan 1, 2000 so no doubt it's a huge number.
It's inadvisable to store time as seconds since midnight due to this naggy little thing called Daylight Saving Time. Every year, different countries do it on different days and on different hours. For example, even though Britain and France change their clock on the same day (March 26, 2017), one makes the shift from 1AM to 2AM, the other goes from 2AM to 3AM. That's very easy to make for a mess!
Use DateComponents instead:
let calendar = Calendar.current
let startTimeComponent = DateComponents(calendar: calendar, hour: 8)
let endTimeComponent = DateComponents(calendar: calendar, hour: 17, minute: 30)
let now = Date()
let startOfToday = calendar.startOfDay(for: now)
let startTime = calendar.date(byAdding: startTimeComponent, to: startOfToday)!
let endTime = calendar.date(byAdding: endTimeComponent, to: startOfToday)!
if startTime <= now && now <= endTime {
print("between 8 AM and 5:30 PM")
} else {
print("not between 8 AM and 5:30 PM")
}
I ended up using DateComponents to calculate a TimeInterval.
let components = Calendar.current.dateComponents(
[.hour, .minute, .second], from: Date())
guard let seconds = components.second,
let minutes = components.minute,
let hours = components.hour else
{
return false
}
let currentTime = Double(seconds + minutes * 60 + hours * 60 * 60)
return startTime <= currentTime && currentTime <= endTime
I am trying to find the time (in seconds) between now and a specific time during that day e.g 9:00 AM and if the time now is past this specified time in the day, I would want the time between now and that set time in the next day.
My ultimate goal is to run a function at every hour from 8:00 AM to 5:00 PM, while the application is not open/ in background. If there is a better way to do this without using a timer with an interval of 24 hours then I would really appreciate some suggestions.
For adding an hour to the current time. Use the below extension
extension Date {
func addHours(_ hours: Int) -> Date {
let seconds: TimeInterval = Double(hours) * 60 * 60
let newDate: Date = self.addingTimeInterval(seconds)
return newDate
}
}
call this function as Date().addHours(1)
To get specific time of current date
func getDateFromHour(hour: Int) -> Date {
let date = Date()
let calendar = Calendar.current
et componentsCurrent = calendar.dateComponents([.year, .month, .day], from: date)
var components = DateComponents()
components.hour = hour
components.minute = 0
components.second = 0
components.month = componentsCurrent.month
components.day = componentsCurrent.day
components.year = componentsCurrent.year
return calendar.date(from: components)!
}
Note: you should use 24-hour clock while passing the parameter hour.
I've created an extension for NSDate which removes the time component to allow equality checks for NSDate based on date alone. I have achieved this by taking the original NSDate object, obtaining the day, month and year using the DateComponent class and then constructing a new NSDate using the information obtained. Although the NSDate objects obtained look correct when printed to the console (i.e. timestamp is 00:00:00) and using the NSDate.compare function on two identical dates returns NSComparisonResult.OrderedSame, if you deconstruct them using DateComponent once more, some of them have the hour set to 1. This appears to be a random event with this error being present about 55% of the time. Forcing the hour, minute and second properties of DateComponent to zero before constructing the new NSDate rather than assuming they will default to these values does not rectify the situation. Ensuring the timezone is set helps a little but again does not fix it.
I am guessing there may be a rounding error somewhere (possibly in my test code), I've fluffed the conversion or there is a Swift bug but would appreciate comments. Code and output from a unit test below.
Code as follows:
extension NSDate {
// creates a NSDate object with time set to 00:00:00 which allows equality checks on dates alone
var asDateOnly: NSDate {
get {
let userCalendar = NSCalendar.currentCalendar()
let dayMonthYearUnits: NSCalendarUnit = .CalendarUnitDay | .CalendarUnitMonth | .CalendarUnitYear
var dateComponents = userCalendar.components(dayMonthYearUnits, fromDate: self)
dateComponents.timeZone = NSTimeZone(name: "GMT")
// dateComponents.hour = 0
// dateComponents.minute = 0
// dateComponents.second = 0
let result = userCalendar.dateFromComponents(dateComponents)!
return result
}
}
Test func:
func testRemovingTimeComponentFromRandomNSDateObjectsAlwaysResultsInNSDateSetToMidnight() {
var dates = [NSDate]()
let dateRange = NSDate.timeIntervalSinceReferenceDate()
for var i = 0; i < 30; i++ {
let randomTimeInterval = Double(arc4random_uniform(UInt32(dateRange)))
let date = NSDate(timeIntervalSinceReferenceDate: randomTimeInterval).asDateOnly
let dateStrippedOfTime = date.asDateOnly
// get the hour, minute and second components from the stripped date
let userCalendar = NSCalendar.currentCalendar()
var hourMinuteSecondUnits: NSCalendarUnit = .CalendarUnitHour | .CalendarUnitMinute | .CalendarUnitSecond
var dateComponents = userCalendar.components(hourMinuteSecondUnits, fromDate: dateStrippedOfTime)
dateComponents.timeZone = NSTimeZone(name: "GMT")
XCTAssertTrue((dateComponents.hour == 0) && (dateComponents.minute == 0) && (dateComponents.second == 0), "Time components were not set to zero - \nNSDate: \(date) \nIndex: \(i) H: \(dateComponents.hour) M: \(dateComponents.minute) S: \(dateComponents.second)")
}
}
Output:
testRemovingTimeComponentFromRandomNSDateObjectsAlwaysResultsInNSDateSetToMidnight] : XCTAssertTrue failed - Time components were not set to zero -
NSDate: 2009-06-19 00:00:00 +0000
Index: 29 H: 1 M: 0 S: 0
I am sure that your test dates you created randomly will contain dates that live in DST (Daylight Saving Time) hence the 1 hour offset — indeed a clock would show 0:00.
Your code is anyway overly complicated and not timezone aware, as you overwrite it.
Preparation: create to dates on the same day with 5 hours apart.
var d1 = NSDate()
let cal = NSCalendar.currentCalendar()
d1 = cal.dateBySettingUnit(NSCalendarUnit.CalendarUnitHour, value: 12, ofDate: d1, options: nil)!
d1 = cal.dateBySettingUnit(NSCalendarUnit.CalendarUnitMinute, value: 0, ofDate: d1, options: nil)!
d1 is noon at users location today.
let comps = NSDateComponents()
comps.hour = 5;
var d2 = cal.dateByAddingComponents(comps, toDate: d1, options: nil)!
d2 five hours later
Comparison: This comparison will yield equal, as the dates are on the same day
let b = cal.compareDate(d1, toDate: d2, toUnitGranularity: NSCalendarUnit.CalendarUnitDay)
if b == .OrderedSame {
println("equal")
} else {
println("not equal")
}
The following will yield non equal, as the dates are not in the same hour
let b = cal.compareDate(d1, toDate: d2, toUnitGranularity: NSCalendarUnit.CalendarUnitHour)
if b == .OrderedSame {
println("equal")
} else {
println("not equal")
}
Display the dates with a date formatter, as it will take DST and timezones in account.