Махинации с часовыми поясами в Swift


И как они могут свести вас с ума

Наступает время, когда неизбежно приходится работать с Calendar. Иногда это просто, но имейте в виду, что это может быть очень сложно 😅.

Так что давайте немного повеселимся с этим 👍.

Предположим, вам нужно сравнить два разных значения Date.

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)
Войдите в полноэкранный режим Выйти из полноэкранного режима

Как мы и предполагали, в результате мы получим "дата1 раньше по времени, чем дата2". Отлично! 😃

Что же будет, если мы используем один и тот же день, но разные значения TimeZone и хотим сравнить начало дня? (Только вы можете ответить, зачем вам это нужно, но просто предположим, что нужно 😅)

Допустим, мы хотим узнать, кто первым встретил Новый год в 1971 году — Берлин или Нью-Йорк.

...
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)
Вход в полноэкранный режим Выйти из полноэкранного режима

Ну это было легко 😁 В результате мы получаем…

Подождите, что??? "дата1 позже по времени, чем дата2" 😳🤯😱 Как такое вообще возможно? День в Берлине начинается раньше, чем день в Нью-Йорке, должно было быть "date1 раньше по времени, чем date2" !!!

Так что давайте расследуем это. 🧐

Во-первых, давайте выведем несколько startTimes, чтобы сравнить их. Так как мы использовали 1 января, мы можем быть уверены, что это не проблема, потому что дата раньше 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)")
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

Это дает нам следующий список:

Дата Часовой пояс
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 Почему мы снова вернулись в 1970 год?
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

Хорошо, первые строки до UTC+0 выглядят как ожидалось. Но почему остальное неправильно?

Решение довольно простое. Было слишком много (или, скорее, слишком мало) преобразований TimeZone!

Когда мы использовали часовой пояс для Нью-Йорка UTC-8, мы использовали "1971-01-01 00:00:00" в качестве значения даты. Но это в UTC+0! Дата в UTC-8 на самом деле "1970-12-31 18:00:00 -0800". Когда мы вызываем startOfDay, мы получаем "1970-12-31 00:00:00 -0800", которая находится в UTC+0 "1970-12-31 08:00:00 +0000".
Это означает, что результат на самом деле правильный, но наша функция — нет 🙈.

Так что давайте исправим это 💪.

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)
Войдите в полноэкранный режим Выйти из полноэкранного режима

Теперь мы получим "дата1 раньше по времени, чем дата2" в качестве результата, как и ожидалось. 😁
Ключом к решению здесь является то, что мы не использовали date напрямую, а сдвинули его на смещение конкретного часового пояса и UTC, таким образом, мы получаем startOfDay в нужном нам часовом поясе.

Вывод: Не связывайтесь с часовыми поясами!

Полный код можно найти здесь.

Оцените статью
devanswers.ru
Добавить комментарий