Absolute UTC offset in Swift - swift

My task is to create a string containing UTC offset during DST and during summer time (example: UTC+1UTC+2 or UTC+1UTC+1 if there is no DST for a region). My function looks the following:
extension TimeZone {
public func utcOffset(for date: Date = Date()) -> String {
var currentTimeOffest = self.secondsFromGMT(for: date)
if isDaylightSavingTime() {
currentTimeOffest -= Int(daylightSavingTimeOffset(for: date))
}
let currentInHours = Int(currentTimeOffest / 3_600)
let hoursSymbol: String = currentInHours > 0 ? "+" : ""
let daylightOffset = TimeInterval(currentTimeOffest) + self.daylightSavingTimeOffset(for: date)
let daylightInHours = Int(daylightOffset / 3_600)
let daylightSymbol: String = daylightInHours > 0 ? "+" : ""
return "UTC\(hoursSymbol)\(currentInHours)UTC\(daylightSymbol)\(daylightInHours)"
}
}
It works well and I've written tests for it. All is good but after recent DST changes in multiple countries the tests started failing, even though I pass a specific date to calculate the offset for:
func testUtcOffset() {
let date: Date = Date(timeIntervalSince1970: 1_557_482_400) //May 10, 2019 10:00:00 AM
let warsaw = TimeZone.init(identifier: "Europe/Warsaw")! //eastern hemisphere, with DST
XCTAssertEqual(warsaw.utcOffset(for: date), "UTC+2UTC+3")
let shanghai = TimeZone.init(identifier: "Asia/Shanghai")! //eastern hemisphere, without DST
XCTAssertEqual(shanghai.utcOffset(for: date), "UTC+8UTC+8")
let barbados = TimeZone.init(identifier: "America/Barbados")! //western hemisphere, without DST
XCTAssertEqual(barbados.utcOffset(for: date), "UTC-4UTC-4")
let bermuda = TimeZone.init(identifier: "Atlantic/Bermuda")! //western hemisphere, with DST
XCTAssertEqual(bermuda.utcOffset(for: date), "UTC-4UTC-3")
let gmt = TimeZone.init(identifier: "GMT")! //GMT, without DST
XCTAssertEqual(gmt.utcOffset(for: date), "UTC0UTC0")
let lisbon = TimeZone.init(identifier: "Europe/Lisbon")! //GMT, with DST
XCTAssertEqual(lisbon.utcOffset(for: date), "UTC+1UTC+2")
}
2 weeks ago, the warsaw and lisbon timezones started failing, today bermuda. Any ideas what might be wrong?

A few things:
In your tests, you have the offsets for Warsaw and Lisbon an hour off. Warsaw is UTC+1 during standard time, and UTC+2 during daylight saving time. Lisbon is UTC+0 during standard time, and UTC+1 during daylight time.
From your comment, it seems you're looking for the standard offset and the daylight offset. However, the standard offset isn't necessarily the same as the current offset. The current offset might include daylight saving time, or not.
According to these docs, the secondsFromGMT function returns the difference including the daylight adjustment if one is in effect. Thus you should not be adjusting for that yourself.
It doesn't seem to make sense to be asking the daylightSavingTimeOffset function for the offset on a date when daylight saving time doesn't apply. You might get better results just using secondsFromGMT for two different dates in the current year. A common approach is to get the offsets for January 1st and July 1st. Whichever is smaller is the standard time, the other is the daylight time. Keep in mind they may be the same if DST is not used, and they will be inverted between northern and southern hemisphere time zones.
Even with the above approach, this sort of algorithm ignores a lot of the complexities of time zones. Consider that some time zones have changed their standard time at different points in their history. Such an algorithm might mistake that as a daylight saving time change.
A point of concern: Once you have your string generated, such as "UTC+1UTC+2", how will the external API you know from that alone which set of daylight saving time rules to apply? Since daylight saving time starts and stops at different dates and times in different parts of the world, it's likely that the wrong dates could be used when interpreting the offsets.

Related

Swift configuring timezone with identifier changes GMT value

I have the following playground code:
let dateFormatter = ISO8601DateFormatter()
let zurichTimeZone = TimeZone(identifier: "Europe/Zurich")
dateFormatter.timeZone = zurichTimeZone
// dateFormatter.timeZone = .init(abbreviation: "GMT+1:00")
// prints timezone +1
print(dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(100400))))
// prints timezone +2
print(dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(1665957700))))
The first output is "1970-01-02T04:53:20+01:00" and the second output is "2022-10-17T00:01:40+02:00". Why does the GMT value change? If use "Europe/Kalingrad" as the identifier, I will get +3 from the first and +2 from the second print. The problem won't appear when I'm setting the timezone with "dateFormatter.timeZone = .init(abbreviation: "GMT+1:00")". I also can't find this behaviour when I'm using "Pacific/Guam".
The named TimeZones take daylight saving time (DST) into consideration, while the ones created by a GMT offset do not.
Zurich is GMT+1 in winter time, but GMT+2 in summer time.
The 2 dates you create are on different sides of the clock change, so 1 uses summer time, while the other uses winter time. This is where the 1h time difference using the 2 different timezones is coming from.

DateFormatter returns previous day

I've looked around and people have had problems with different years, random and changing results, and nil dates, but nothing like what I have, so I am asking here. Note I am in Playground right now.
I am taking strings in the format of "yyyy-mm-dd" and converting them to a different date format. Here is the code:
let example = "2001-11-03"
let dateFormatterInput = ISO8601DateFormatter()
dateFormatterInput.formatOptions = [.withFullDate, .withDashSeparatorInDate]
let date = dateFormatterInput.date(from: example)
let dateFormatterOutput = DateFormatter()
dateFormatterOutput.dateFormat = "MMMM dd, yyyy"
let output = dateFormatterOutput.string(from: date!)
The sidebar in Playground shows that the first reference to the previous day's date happens on the let date line. Also, this behavior happens on every date I've tried. In this example, it returns "November 2, 2001." I've tried different months, days, and years (1900s and 2000s) and it gives me the same result every time.
What am I doing wrong?
The key thing here is that ISO8601DateFormatter by default thinks that the time zone of your date string is GMT:
ISO8601DateFormatter.timeZone:
The time zone used to create and parse date representations. When unspecified, GMT is used.
However, the timeZone of DateFormatter by default (and also the side bar of the playground) assumes your device's local time zone:
DateFormatter.timeZone
The time zone for the receiver. If unspecified, the system time zone is used.
If your system time zone has a negative UTC offset on the start of the day 2001-11-03 UTC, then when seen from your time zone, that moment is actually in the day 2001-11-02. Hence the output you see.
Assuming you don't care about the actual value of date, and just care about the final string output, you can just set the timeZone of DateFormatter to GMT:
dateFormatterOutput.timeZone = TimeZone(identifier: "GMT")
Side note: You should also set locale when using a fixed format to avoid localisation issues:
dateFormatterOutput.locale = Locale(identifier: "en_US_POSIX")
Or better, just use one of the built-in, locale sensitive formats instead:
dateFormatterOutput.dateStyle = .medium
dateFormatterOutput.timeStyle = .none

Why my Cloud records Time isn't the same when I load it into my app

In a training project to learn the cloudKit capacity I record a date via a Datepicker and I save it into a cloudKit data base. I use the French format "DD/MM/YYYY HH:MM". Everything work fine and the date which is entered by the user is the same format in the data base.
Now I would like to work with this date in an other view in my app. So I load it into an array from cloudKit and I would like to compare it with the current time.
let recordTime = LastMealRecords[LastMealRecords.count - 1]
let currentSavedTime = (recordTime.object(forKey: "Timming"))
let diffComponents = Calendar.current.dateComponents([.hour], from: currentSavedTime as! Date, to: Now)
let intervals = diffComponents.hour
print(Now)
print(currentSavedTime)
So here the print show :
Now : DD/MM/YYYY HH:MM (which is the correct local time !)
currentSavedTime : DD/MM/YYY HH:MM (but HH:MM is not the same value as the one which is save on the Cloud data base. In fact it seem to be the UTC time cause there is 2h difference in less like the Local and UTC time in France... )
Question : How could I fixe this matter ? I'm trying to find the same value which is saved on the cloud..
Thanks for your help !
You can't make changes to the time saved at server end, all you can do is manipulate your current time accordingly
// Return time zone used by the system right away
let timeZone = NSTimeZone.system
// Returns the difference in seconds between the server and GMT at a given date.
let timeZoneOffset = timeZone.secondsFromGMT(for: currentDate) / 3600
print(timeZoneOffset, "hours offset for timezone", timeZone)

Swift - How to create a date object containing just the time

I trying to create a date object just containing the time of 1 second past midnight.
I believe the following should work but it just keeps returning nil.
let dateTime = Date()
let timeFormatter = DateFormatter()
timeFormatter.dateFormat = "HH:mm:ss"
let time = timeFormatter.date(from: "00:00:01")
print("Time: \(time!)")
Can someone tell me what i'm doing wrong!
Thanks
Let Calendar do the math, this is more reliable, you aren't using the current date (dateTime) anyway.
let midnight = Calendar.current.startOfDay(for: Date())
let oneSecondAfterMidnight = Calendar.current.date(byAdding: .second, value: 1, to: midnight)
This works even if midnight doesn't exist due to daylight saving change.
Date is not a "date" in any meaningful way. It's a specific point in time, independent of any calendar or location. What you want to express is a point on a calendar: "one second" past an arbitrary calendar point we call "midnight." That's done with DateComponents.
var dc = DateComponents()
dc.hour = 0
dc.minute = 0
dc.second = 1
This is the second second of the first minute of the first hour (00:00:01) of an arbitrary day on an arbitrary calendar, which is what you've described.
More precisely, it's "zero hours, zero minutes, and one second," which is only "one second after midnight" if you add it to some "midnight." But beyond that, there is no independent "time" type. Those things only have meaning when applied to a Calendar.
(Keep in mind that due to DST change in some parts of world, such as Iran, there are sometimes two midnights in the same day. So when you ask for this kind of thing, you need to be very clear what you mean. Do you want every second after midnight or just the first one on a given day?)

Picking a date with swift UI automation

In the app I'm testing there is a date picker I'm trying to automate. The wheel defaults to tomorrow and I'm attempting to change it to today's date but 2 minutes from now. Below is the code I'm using to attempt this.
app.pickerWheels.element(boundBy: 0).adjust(toPickerWheelValue: "Today")
app.pickerWheels.element(boundBy: 1).adjust(toPickerWheelValue: "1")
app.pickerWheels.element(boundBy: 2).adjust(toPickerWheelValue: "00")
(In the actual code I'm using variables and not hard coding these string)
This code works for the second and third wheel (hours and minutes) but for the first wheel it won't set the value. The test will fail and not continue past that point.
I have also tried passing today's date instead of just "Today" with the same results.
You can use the DateFormatter class with Date to accomplish this.
// Initialize the date formatter. Set the timeZone and format. I chose hours and minutes.
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.timeZone = NSTimeZone.local
dateFormatter.dateFormat = "HH:mm"
// Initialize the Date instance using a time interval since now.
let d: Date = Date(timeIntervalSinceNow: 2 * 60)
print("Current Time = \(dateFormatter.string(from: Date())), Two-Minutes-From-Now = \(dateFormatter.string(from: d))")
Output: Current Time = 23:57, Two-Minutes-From-Now = 23:59
A Date is stored as a time interval since January 1st, 1970. You can manipulate the date by adding or subtracting seconds from it. Here, I added 2 * 60 or two 60-second minutes to the current time interval (a large value represented in a double). This points to two minutes in the future.
Now, if you print the date without the formatter, it will just display the current time with no regard to your time zone. So if you want it to be accurate to your time zone, you need to set that in the formatter first. Note that it doesn't change the time, just its representation to you.