DEV Community

Katharina Gopp
Katharina Gopp

Posted on

Timezone Shenanigans in Swift

And how they can drive you crazy

There comes a time when it is inevitable to work with Calendar. Sometimes it is easy, but be aware, it can be very tricky 😅

So let's have some fun with it 👍

Assume you have to compare two different Date values.

let date1 = Date(timeIntervalSince1970: 0) // 1970-01-01 00:00:00 
let date2 = Date(timeIntervalSince1970: 60 * 60 * 24) // 1970-01-02 00:00:00 

func compareDates(date1: Date, date2: Date){
    switch date1 {
    case date2:
        print("date1 and date2 represent the same point in time")
    case ...date2:
        print("date1 is earlier in time than date2")
    case date2...:
        print("date1 is later in time than date2")
    default:
        return
    }
}
compareDates(date1: date1, date2: date2)
Enter fullscreen mode Exit fullscreen mode

As we would assume, we get as result "date1 is earlier in time than date2". Nice! 😃

So what happens, if we use the same day but different TimeZone values and want to compare the start of the day? (Only you can answer why you would need that, but just assume you do 😅)

Let's just say, we want to know if Berlin or New York City celebrated New Year's Day in 1971 first.

...
func startOfDayIn(date: Date, timeZone: TimeZone) -> Date {
    var calendar = Calendar.current
    calendar.timeZone = timeZone
    return calendar.startOfDay(for: date)
}

let date = Date(timeIntervalSince1970: 60 * 60 * 24 * 365) // 1971-01-01 00:00:00
let timeZone1 = TimeZone(secondsFromGMT: 60 * 60 * 1)! // Berlin
let start1 = startOfDayIn(date: date, timeZone: timeZone1)
let timeZone2 = TimeZone(secondsFromGMT: 60 * 60 * -8)! // New York City
let start2 = startOfDayIn(date: date, timeZone: timeZone2)

compareDates(date1: start1, date2: start2)
Enter fullscreen mode Exit fullscreen mode

Well that was easy 😁 We get as a result..

Wait, what?? "date1 is later in time than date2" 😳🤯😱 How is that even possible? The day in Berlin starts earlier than the day in New York, it should have been "date1 is earlier in time than date2" !!!

So let's investigate this. 🧐

First, let's print a few startTimes to compare them. Since we used January 1st, we can be sure that it's not a problem because the date is before year 1970.

(-12...12).reversed().forEach { deviation in
    let timeZone = TimeZone(secondsFromGMT: 60 * 60 * deviation)!
    let start = startOfDayIn(date: date, timeZone: timeZone)

    print(start, "| UTC\(deviation >= 0 ? "+" : "")\(deviation)")
}
Enter fullscreen mode Exit fullscreen mode

This gives us the following list:

Date Timezone
1970-12-31 12:00:00 +0000 UTC+12
1970-12-31 13:00:00 +0000 UTC+11
1970-12-31 14:00:00 +0000 UTC+10
1970-12-31 15:00:00 +0000 UTC+9
1970-12-31 16:00:00 +0000 UTC+8
1970-12-31 17:00:00 +0000 UTC+7
1970-12-31 18:00:00 +0000 UTC+6
1970-12-31 19:00:00 +0000 UTC+5
1970-12-31 20:00:00 +0000 UTC+4
1970-12-31 21:00:00 +0000 UTC+3
1970-12-31 22:00:00 +0000 UTC+2
1970-12-31 23:00:00 +0000 UTC+1
1971-01-01 00:00:00 +0000 UTC+0
1970-12-31 01:00:00 +0000 UTC-1 Why are we back in 1970 again??
1970-12-31 02:00:00 +0000 UTC-2
1970-12-31 03:00:00 +0000 UTC-3
1970-12-31 04:00:00 +0000 UTC-4
1970-12-31 05:00:00 +0000 UTC-5
1970-12-31 06:00:00 +0000 UTC-6
1970-12-31 07:00:00 +0000 UTC-7
1970-12-31 08:00:00 +0000 UTC-8
1970-12-31 09:00:00 +0000 UTC-9
1970-12-31 10:00:00 +0000 UTC-10
1970-12-31 11:00:00 +0000 UTC-11
1970-12-31 12:00:00 +0000 UTC-12

Ok, the first lines until UTC+0 look as expected. But why is the rest wrong?

The solution is rather simple. There were too many (or rather too few) TimeZone conversions!

When we used the timezone for New York UTC-8, we used "1971-01-01 00:00:00" as date value. But this is in UTC+0! The date in UTC-8 is actually "1970-12-31 18:00:00 -0800". When we call startOfDay, we get "1970-12-31 00:00:00 -0800" which is in UTC+0 "1970-12-31 08:00:00 +0000".
This means, the result is actually correct, but our function is not 🙈

So let's fix it 💪

func adjustedStartOfDayIn(date: Date, timeZone: TimeZone) -> Date {
    var calendar = Calendar.current
    calendar.timeZone = timeZone
    let correctDay = date.addingTimeInterval(TimeInterval(-calendar.timeZone.secondsFromGMT()))
    return calendar.startOfDay(for: correctDay)
}

let correctStart1 = adjustedStartOfDayIn(date: date, timeZone: timeZone1)
let correctStart2 = adjustedStartOfDayIn(date: date, timeZone: timeZone2)

compareDates(date1: correctStart1, date2: correctStart1)
Enter fullscreen mode Exit fullscreen mode

Now we get "date1 is earlier in time than date2" as result, just as expected. 😁
The key for the solution here is that we didn't use date directly but shifted it by the offset of the specific timezone and UTC, so we get the startOfDay in the timezone we want to use.

Conclusion: Don't mess with timezones!

You can find the complete code here.

Top comments (2)

Collapse
 
sruhleder profile image
Sebastian Ruhleder

Great article! Timezones can be very confusing 😅

Collapse
 
katharinagopp profile image
Katharina Gopp

Thank you 😊