DateComponents giving wrong hour - swift

I am trying to implement a simple countdown timer in my test app.
I have two dates:
fromDate - which is current time that I get by Date(), e.g. 2021-08-27 11:07:34 +0000
toDate - is a future date, e.g. 2021-11-17 01:00:00 +0000
I am using DateComponents to get back the difference in days, hours, minutes and seconds.
let components = Calendar.current.dateComponents([.day, .hour, .minute, .second],
from: fromDate,
to: toDate)
Its returning me back the values for days hours minute and second 81, 12, 52, 25
The values for day, minute and second are correct, but the hour is 1 hour less.
I suspect daylight timing has to do something with this but I cannot find anything that can help here.
Kindly help me what I am doing wrong as I have tried many things in past few days but nothing seems to work

I was able to reproduce the behaviour by using:
let from = Date(timeIntervalSince1970: 1630062455)
print(from) // 2021-08-27 11:07:35 +0000
let to = Date(timeIntervalSince1970: 1637110800)
print(to) // 2021-11-17 01:00:00 +0000
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "Europe/London")!
let comp = calendar.dateComponents([.day, .hour, .minute, .second], from: from, to: to)
print(comp.day!, comp.hour!, comp.minute!, comp.second!)
The reason why this happens is because when doing dateComponents(_:from:to:), Calendar takes into account its timezone. After all, without a timezone, (almost) no date components would make sense - you would not be able to tell what hour a Date is, for example. A Date just represents an instant in time/n seconds since the epoch.
(In the case of Calendar.current, the timezone it uses is TimeZone.current)
Europe/London would go out of DST at some point between from and to. This means the calendar would calculate the difference in date components between:
from: 2021-08-27 12:07:35
to: 2021-11-17 01:00:00
Notice that the first time is 12:07:35, rather than 11:07:35. This is because at 2021-08-27 11:07:35 +0000, the local date time at Europe/London really is 2021-08-27 12:07:35.
To get your desired output, just change the calendar's timeZone to UTC:
var calendar = Calendar.current
calendar.timeZone = TimeZone(identifier: "UTC")!
let comp = calendar.dateComponents([.day, .hour, .minute, .second], from: from, to: to)

Related

Swift Calendar.current.nextDate timeZone issues

I am trying to get a Date object for the next occurring future time where the hour in UTC time is 18. However my code doesn't work as expected. I have the following:
let dateComponents = DateComponents(timeZone: TimeZone(abbreviation: "GMT"), hour: 18)
let date = Calendar.current.nextDate(after: Date(), matching: dateComponents, matchingPolicy: .nextTime)
print(date)
The problem is that this results in 2019-02-09 23:00:00 +0000
The date is for the next occurring time where the hour is 18 in EST.
I would have expected, since the the dateComponents has the timezone set to UTC and the hour to 18, that the date would be 2019-02-09 18:00:00 +0000. Furthermore, changing the timezone seems to have no effect on the nextDate found.
Why doesn't the nextDate function respect the timezone set in the dateComponents passed to it?
It looks like the timezone in DateComponents is ignored.
However when you set the timezone in a new calendar you get correct results.
let dateComponents = DateComponents(hour: 18)
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
let date = calendar.nextDate(after: Date(), matching: dateComponents, matchingPolicy: .nextTime)
print(date) // Optional(2020-09-29 18:00:00 +0000)
I live in GMT time, and running this in a playground produces the expected result you are looking for. You are setting the time zone of your date component, but I would imagine your own calendar (Calendar.current) is set to EST. You would need to account for the offset in EST vs GMT for your required result

Swift date manipulation - strange month return

I'm trying to do some date manipulation with Swift and I'm getting an unexpected result. The webservice will pass in a string date, and then I want to get that month and the previous month. I'm using this code (with input grab and such removed):
import Foundation
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(identifier: "America/New_York")
formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.date(from: "2018-12-01")!
let prev = Calendar.current.date(byAdding: .month, value: -1, to: date)!
formatter.string(from: date)
formatter.string(from: prev)
So I've got a valid date, and then I subtract a month from it. The first formatted date shows my expected 2018-12-01 but then on the second line, instead of saying 2018-11-01 it says 2018-10-31.
I'm in PST, which is of course 3 hours ahead of EST. If I add 3 hours I'd get the expected strings. However, since both the input and the output strings were done with a formatter using the timezone, why don't I get the expected output?
The problem is that Calendar.current is in a different timezone (for you) than the formatter.
So date is December 1, 2018 at midnight New York time. But that is November 30, 2018 at 9pm local time (PST) for you.
When you subtract one month it is done in local time (Calendar.current) so you get October 30, 2018 at 9pm. Then you format that date to New York time and it results in October 31, 2018 at midnight.
To get the proper results you want a Calendar in the same timezone as the formatter:
var cal = Calendar(identifier: Calendar.current.identifier)
cal.timeZone = formatter.timeZone
let prev = cal.date(byAdding: .month, value: -1, to: date)!
This will give the expected result.

Comparing dates in Swift ignoring current timezone

In Swift I can find the difference between two dates using Calendar.current.dateComponents
The problem is that this gives the difference relative to my current time zone.
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm XXX"
let from = zoneFormatter.date(from: "2018-03-25 00:00 +00:00")!
let to = zoneFormatter.date(from: "2018-03-26 00:00 +00:00")!
let components = Calendar.current.dateComponents([.month, .day, .hour, .minute], from: from, to: to)
// components is 1 day, 1 hour, because I am in UK and Daylight Savings started on 25 March.
How can I retrieve the absolute difference between the two instants specified in the same format which dateComponents returns?
Incorrect Date Format String
Looks like your using the incorrect format for your dates. The date format for the specified date of "2018-03-25 00:00 +00:00" is actually "yyyy-MM-dd HH:mm ZZZZZ". Give that format a try and it should fix the problem.
Why does the incorrect format not work for comparison between time zones?
This is because it was the timeZone component of your format string that is incorrect. This means the timeZone will not be accounted for in the comparison.
The timezone of Calendar.current can be changed by setting the system default timezone as follows:
NSTimeZone.default = NSTimeZone(abbreviation: "UTC")! as TimeZone

How to prevent DateComponents from updating for time zone

I am trying to get date components from a date value from an API endpoint. I need to preserve the date values as they are already adjusted for time zone. Unfortunately, I have no control over how the API returns a date value.
When I get the date value (sessionTime) from the API, it is returned as follows:
2017-12-05 08:00:00 +0000
I need to set up a local notification based on that time, however, when I try to extract components from that date object with the following code:
let notifyTime = Calendar.current.dateComponents(
[.year, .month, .day, .hour, .minute, .second], from: sessionTime)
I get this:
year: 2017 month: 12 day: 5 hour: 2 minute: 0 second: 0 isLeapMonth: false
I am six hours from GMT so it is obvious what is going on, but I wish I could prevent it and extract the date components exactly as they are. The desired output would be:
year: 2017 month: 12 day: 5 hour: 8 minute: 0 second: 0 isLeapMonth: false
Can anyone help? Thanks!
You can specify the timeZone that DateComponents uses.
Like this:
let notifyTime = Calendar.current.dateComponents(in: TimeZone.current, from: sessionTime)

How to separate the date and time components of NSDate() in Swift?

Would anyone be able to tell me how to separate the date and time components of NSDate() in Swift? I saw a few things about separating the date itself into components (day, month, year), but nothing really about how to get the time only or the day, month, and year only.
Basically, I'm trying to filter data on a table by date, but to also display the time the entry was made. I tried using the plain ol' NSDate(), but then the filtering didn't work because filtering by just today's date didn't yield results because the times those entries were made will always be less than the current time.
You need to use Calendar to break a date apart, using CalendarUnit to specify what components you want.
let calendar = Calendar.current
let components = calendar.dateComponents([.month, .day, .year, .hour, .minute], from: Date())
print("Hour: \(components.hour)")
print("Minute: \(components.minute)")
print("Day: \(components.day)")
print("Month: \(components.month)")
print("Year: \(components.year)")
Swift 5 Xcode 14.2
let myDate = .... // you date here
let cal = Calendar.current
let required : Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let components : DateComponents = cal.dateComponents(required, from: myDate)
print("Hour: \(components.hour)")
print("Minute: \(components.minute)")
print("Day: \(components.day)")
print("Month: \(components.month)")
print("Year: \(components.year)")