Swift Calendar.current.nextDate timeZone issues - swift

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

Related

Swift date components incorrect return of date day number

I need to obtain a date from some variable values
So I specify year, month and day and I need a Date as return
Doing the following works except for the day because it return the day input - 1
let todayDate: Date = Calendar.current.startOfDay(for: Date.from(year: 2022, month: 09, day: 05)!)
print("today date = \(todayDate)")
extension Date {
static func from(year: Int, month: Int, day: Int) -> Date? {
let calendar = Calendar(identifier: .gregorian)
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
return calendar.date(from: dateComponents) ?? nil
}
}
And the output is
today date = 2022-09-04 22:00:00 +0000
Date and time can be a bit tricky. The Date struct stores a point in time relative to GMT. If you print it it will show exactly that.
Solution:
Don´t use print, use a proper Dateformatter. To illustrate what I mean use this in a playground:
let date = Calendar.current.startOfDay(for: Date())
print(date)
//2022-09-03 22:00:00 +0000
// when it is 4.th of september 00:00 in my timezone (+- Daylight saving) it is this time in GMT
let formatter = DateFormatter()
formatter.dateFormat = "dd MM yyyy HH:mm:ss"
print(formatter.string(from: date))
//04 09 2022 00:00:00
// this is the time in my timezone
So the issue here is not that it has the wrong time, it is just not presented in the correct time zone.

DateComponents giving wrong hour

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)

How can I ignore time zones in Swift Dates?

I wish to use Swift 5 to create a Date object with the day, month, and year I pass it. Problem is that the DateFormatter has its own ideas, and seems to be treating my Date objects as if they were UTC even whatever I set <formatter>.timeZone = ... to.
Say I want t date object with the date the first of April.
private func firstOfApril() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return "\(formatter.date(from: "2021-04-01")!)"
}
If I do this in the AM here (GMT + 13) I get 2021-03-31 11:00:00 +0000. I expect: 2021-04-01 00:00:00
WHat I have tried:
enum TZType{
case None
case Current
case Auto
case Nil
case Default
}
private func firstOfApril(none_current:TZType) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
switch none_current {
case .None:
formatter.timeZone = .none
case .Current:
formatter.timeZone = .current
case .Nil:
formatter.timeZone = nil
case .Auto:
formatter.timeZone = .autoupdatingCurrent
case .Default:
break
}
return "\(formatter.date(from: "2021-04-01")!)"
}
print(firstOfApril(none_current: .None))
print(firstOfApril(none_current: .Current))
print(firstOfApril(none_current: .Auto))
print(firstOfApril(none_current: .Nil))
print(firstOfApril(none_current: .Default))
Outputs
2021-03-31 11:00:00 +0000
2021-03-31 11:00:00 +0000
2021-03-31 11:00:00 +0000
2021-03-31 11:00:00 +0000
There is no concept of time zones in my application. I think there should be, but I am overruled. All time is "local time", as in wall clock time, not local time zone.
Leo got it.
The last lines of that function should be:
let retDate = formatter.date(from: "2021-04-01")!
return formatter.string(from: retDate)
In total:
private func firstOfApril(none_current:TZType) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
switch none_current {
case .None:
formatter.timeZone = .none
case .Current:
formatter.timeZone = .current
case .Nil:
formatter.timeZone = nil
case .Auto:
formatter.timeZone = .autoupdatingCurrent
case .Default:
break
}
let retDate = formatter.date(from: "2021-04-01")!
return formatter.string(from: retDate)
}
You've gotten some good, but incomplete, information.
A Date object captures a moment in time, anywhere on the planet. It is devoid of a time-zone. (Internally, it's recorded by a number of seconds since the iOS/MacOS "epoch date", or "zero date". That zero date is in UTC.)
Date objects don't have a time zone, but they also don't have a fixed time or date. The same instant in time will be at different times of day in different time zones, and may even be different dates (e.g. It is about 7 PM on Monday 29 March here in the US Eastern Daylight Time timezone. It is about midnight on Tuesday 30 March in London.)
If you create a date formatter and feed it a month/day/year, it will default to creating a Date object using your local time zone. It's not the Date object that has a time zone, it's the date formatter.
You can either use the date formatter you already created to convert you dates back to strings (using the DateFormatter function string(from:), or you can use a DateFormatter class function called localizedString(from:dateStyle:timeStyle:. That function takes a date and displays it in your local time zone, using the conventions of your locale, and lets you specify short, medium, or long format dates.
If you write
let date = Date()
print(DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium))
You'll get something like this:
Mar 29, 2021 at 7:07:13 PM
That is expressed in my local time zone, using US date and time formats.
(But with the date and time at which you run it, and using the date and time formatting conventions for your locale.)
Next, it's how you display the date. If you write the code
print(Date())
What you will see is the date at the instant you run the code, but expressed in UTC. For me, that will be 4 hours ahead of my current time, so it will claim it's about 23:00 on 30 January.
2021-03-29 23:00:48 +0000
If you are in China, and I am in the US, and our phones' clocks are both synchronized exactly, and we capture the current date using Date() at the same instant, we will get the exact same Date value. If we were to both express it in UTC, it would show the same date and time. (The Date object doesn't have a time

Swift 5 Datetime Conversion triple timezone cast (???)

I have a DateTime that I'm reading from an API that is in GMT.
I want to force cast it to an EST date object.
The original object is a string that I then cast to a Date to do some time comparison. Unfortunately, I don't think I did this right which made me do this unholy abomination:
extension String {
//this force casts from our original data being in GMT to EST
func getDateTimeTZ() -> Date{
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
if let date = dateFormatter.date(from: self) {
dateFormatter.timeZone = TimeZone(abbreviation: "EST")
let localTime = dateFormatter.string(from: date)
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
return dateFormatter.date(from: localTime)!
}
return dateFormatter.date(from: self)!
}
}
For whatever reason, I seem to have to flip the dateformatter 3 times to get the correct EST string output. Is there a better way to do this?
A Date is not associated with a time zone. From the Date reference:
A specific point in time, independent of any calendar or time zone.
...
A Date value encapsulate a single point in time, independent of any particular calendrical system or time zone. Date values represent a time interval relative to an absolute reference date.
So there is no such thing as “an EST date object” in the iOS SDK.
Time zones are relevant when converting to or from strings and when manipulating the calendrical components (year, month, day, hour, minute, second, etc.) of a Date.
The code you posted computes a new Date that is offset from the original date by the time offset between GMT and EST, but the new Date is not “an EST date object”.
(Incidentally, "EST" means Eastern Standard Time, and will not switch to daylight saving time during the appropriate part of the year. Use "US/Eastern" to get the appropriate time conversion depending on the day of year.)
You say you're doing this adjustment “to do some time comparison”, but you don't say what you're comparing to. It would help for you to describe what comparison you're performing.
For example, let's you want to check whether the Date is in the range 8:30 AM to 3:00 PM in the US/Eastern time zone, regardless of the day. If the input string is GMT, then you should parse it that way, as you do in your code:
let parser = DateFormatter()
parser.timeZone = TimeZone.init(identifier: "GMT")
parser.dateFormat = "yyyy-MM-dd HH:mm"
let date = parser.date(from: "2021-03-26 19:55")!
Then, use a Calendar set to the "US/Eastern" TimeZone to compute the hour and minute components of the Date:
var calendar = Calendar.current
calendar.timeZone = TimeZone(identifier: "US/Eastern")!
let components = calendar.dateComponents([.hour, .minute], from: date)
let hhmm = components.hour! * 100 + components.minute!
if (08_30 ..< 15_00).contains(hhmm) {
print("school time")
} else {
print("play time")
}

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.