Таймзоны. Главный вызов разработки UTasks!

Перед созданием таск-менеджера UTasks мы поставили себе амбициозные цели: сделать инструмент, который будет одновременно простым, удобным и безопасным. Но из всех технических задач, которые пришлось решать, именно работа с таймзонами оказалась одной из самых сложных.

Таймзоны. Главный вызов разработки UTasks!

Всем привет! На связи Денис, Founder&CEO UTasks, первого менеджера задач в Telegram.

Если вы еще не слышали о нас, кратко расскажу:
UTasks — это революция в мире менеджеров задач: мы создали инструмент, который превращает любой общий чат в Telegram в эффективное рабочее пространство.

Просто добавьте UTasks в общий чат и вы сможете ставить задачи и назначать встречи на его участников, отслеживать выполнение, делегировать и организовывать проекты на совершенно новом уровне не выходя из Telegram!

Подробнее о том, как это работает, читайте в моей прошлой статье.

А сегодня я хочу рассказать о главной боли и одновременно вызове, который нам удалось преодолеть — работе с таймзонами.

Как мы в UTasks поняли, что таймзоны - это сложно.

Практически с самого начала нашим таск-менеджером стали пользоваться люди из разных стран, и перед нами сразу встала задача реализовать работу в разных таймзонах.

Таймзоны — это не просто разница в часах между регионами.

Они скрывают за собой множество нюансов: смену времени в зависимости от сезона (летнее и зимнее время), исторические изменения, различия в датах перехода между странами и даже регионами внутри одной страны. А если добавить к этому необходимость учитывать пользовательские предпочтения, планирование задач на определенные даты и автоматическую синхронизацию между устройствами, масштабы проблемы становятся еще более очевидными.

Термины. Куда без НИХ?!

Таймзона — это набор из двух основных характеристик: Название (например, Moscow) и Offset — смещение по времени относительно нулевой таймзоны, измеряемое в минутах.

Пример: у Московской таймзоны название – Moscow, offset: + 3 часа от UTC, что эквивалентно -180 минут.

UTC (Universal Coordinated Time) — принятое за условное абсолютное время на Земле.

GMT (Greenwich Mean Time) — нулевая таймзона, ее offset равен 0. Хотя GMT и UTC обозначают одно и то же смещение, GMT используется в интерфейсах чаще, так как это более привычный термин. Например, в UTasks московская таймзона обозначается как GMT+3.

Таким образом, вся работа с таймзонами в UTasks происходит относительно UTC, но в интерфейсе смещения могут обозначаться относительно GMT для простоты восприятия.

Как UTasks работает с тайм-зонами

Выбор таймзоны

В UTasks управление временем и таймзонами реализовано с учетом:

1. Таймзоны устройства пользователя;

2. Выбранной в настройках UTasks таймзоны.

Пользователь использует тайм-зону устройства

Если пользователь использует таймзону устройства (системную таймзону), UTasks автоматически синхронизируется с часовой зоной, установленной на устройстве.

Пример: Пользователь находится в Москве с системной таймзоной, его устройство будет использовать московское время (GMT +3). Если он поедет в Новосибирск, UTasks подхватит новое значение из настроек телефона.

Пользователь выбирает таймзону в настройках UTasks

Пользователь может выбрать (а может и не выбирать) конкретную таймзону в настройках UTasks, которая сохранится неизменной, независимо от того, где находится его устройство и как пользователь перемещается по таймзонам.

Пример: Пользователь работает по московскому времени, но находится в командировке во Владивостоке (GMT +10). Он может установить в приложении таймзону Москва (GMT +3), чтобы задачи отображались в выбранной зоне, независимо от реального местоположения. В этом случае ему придется жить по двум таймзонам и обе держать в голове, чтобы не запутаться, но мы не будем завидовать.

Время задачи

К управлению временем, связанному с выбором системных настроек или настроек приложения, добавляются сложности, связанные со временем задачи.

Если задача имеет только дату исполнения и не имеет точного времени, то для пользователей, живущих и в Москве, и во Владивостоке, и в Лиссабоне она установится на один и тот же день – например, понедельник 28 октября.

Если же в задаче указывается время исполнения, то пользователи из Москвы и Владивостока увидят ее в своих календарях в разное время, так как Москва и Владивосток имеют разные часовые пояса, а у пользователя из Лиссабона к тому же произойдет переход на летнее/зимнее время.

Самый простой кейс

Предположим (просто нафантазируем всякие небылицы), что Денис живёт в Казани, где действует фиксированный часовой пояс UTC+3 без перехода на летнее или зимнее время.

Денис заходит в UTasks, и система:

- считывает его таймзону с устройства;

- фиксирует смещение (offset) для этой таймзоны, которое составляет "минус 180 минут". (То есть, чтобы перевести локальное время пользователя в UTC, нужно вычесть 180 минут. На первый взгляд, такое представление может показаться непривычным, ведь обычно люди мыслят таймзонами в формате "GMT+3". Однако в программировании принято работать с локальным временем пользователя и преобразовывать его в UTC для хранения).

Денис ставит задачу на 10 ноября в 10:00 утра по локальному времени. Чтобы сохранить эту задачу, UTasks считывает сохраненный ранее offset (-180 минут), далее преобразует локальное время (10:00) в UTC, вычитая offset.

В результате дата сохраняется как 10 ноября, 07:00 по UTC.

Почему данные сохраняются в UTC?

Хранение всех временных данных в UTC упрощает обработку, особенно в приложениях, где пользователи взаимодействуют из разных часовых поясов, а именно:

1. Уведомления легко отправляются с учетом таймзон всех участников;

2. Один и тот же временной интервал корректно отображается для пользователей из разных регионов;

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

Итак, когда Денис возвращается в UTasks, чтобы посмотреть на задачу, данные из сервера приходят в формате UTC (10 ноября, 07:00). UTasks учитывает offset (-180 минут) и прибавляет его обратно. В результате Денис видит задачу на 10 ноября в 10:00, что соответствует его локальному времени.

Рассмотрим здесь же, как видят задачу в UTasks коллеги Дениса, живущие в разных таймзонах.

ДАНО: Денис ставит задачу на всю команду 10 ноября в 10:00 утра по Московскому времени.

ВОПРОС: В какое время начнется встреча у каждого из участников, если: Лёша живет в Калининграде; Камиль живет в Астане; Лейсан находится в отпуск на Мальдивах.

Время исполнения задачи: 10 ноября, 10:00 (GMT+3). UTasks сохраняет время в UTC:10:00 (МСК) - 3 часа = 07:00 UTC.
Теперь конвертируем 07:00 UTC в локальное время для каждого участника:

Лёша (Калининград, GMT+2): 07:00 UTC + 2 часа = 09:00 по Калининграду.

Камиль (Астана, GMT+6): 07:00 UTC + 6 часов = 13:00 по Астане.

Лейсан (Мальдивы, GMT+5): 07:00 UTC + 5 часов = 12:00 по Мальдивам (самое пекло).

Простой алгоритм работы с локальной таймзоной устройства и датами функционирует корректно до тех пор, пока системная таймзона пользователя совпадает с локальной и не меняется.

Кейс посложнее

Cитуация усложняется, если пользователь в настройках приложения выбирает таймзону, отличную от системной.

Предположим, Денис уже переехал в Лиссабон, завтракает паштеиш-де-ната, ужинает портвейном, ходит на курсы самообороны, но мыслями еще в Казани: продает гараж и пустые трехлитровые банки под закатки (50 штук), и в настройках UTasks вручную выбирает таймзону Москвы (UTC+3), чтобы вести все задачи по привычному Московскому времени.

Когда Денис находится в Лиссабоне, но создает задачу в UTasks, например, на «через неделю, в 10:00», он делает это с ожиданием, что время указано в таймзоне Москвы.

Однако реальность такова, что:

1. Все операции с датами в коде происходят в контексте системной таймзоны устройства, то есть в данном случае — Лиссабона (UTC+1);

2. Если использовать простой алгоритм, при сохранении задачи UTasks обработает дату, исходя лишь из системной таймзоны (Лиссабон).

Это приведет к неправильным расчетам.

По простому алгоритму расчет бы пошел таким путем:

Денис ставит задачу на 10:00 по Москве ---> Offset Лиссабона (+1 летом) вычитается, и задача отправляется на сервер как 09:00 по UTC (12:00 по Москве) вместо ожидаемого 07:00 по UTC (выбранные пользователем 10:00 по Москве) ---> На сервере задача сохраняется с неверным временем, хотя в приложении будет показывать вроде бы верные 10:00.

В результате уведомления и отображение времени в будущем собьются: уведомление придет позже, чем ожидал Денис, например, в 12:00 по Москве вместо 10:00.

Чтобы избежать подобных ошибок, мы в UТasks внедрили дополнительный шаг:

1. UТasks вычисляет разницу между системной таймзоной устройства (Москва) и пользовательской таймзоной из настроек (Лиссабон). В нашем примере разница составляет 2 часа (UTC+3 − UTC+1).

2. При обработке даты UТasks учитывает эту разницу и корректирует локальное время перед сохранением: Денис указал 10:00 по Москве, локальное время устройства (Лиссабон) корректируется: 10:00 − 2 часа = 08:00 по Лиссабону.

3. UТasks использует скорректированное время и переводит его в UTC. В итоге на сервер отправляется 07:00 UTC, то есть 10:00 по Москве, что соответствует ожиданиям Дениса.

Почему это работает?

Все даты хранятся на сервере в UTC, что исключает ошибки при синхронизации данных между пользователями в разных таймзонах. UТasks корректно обрабатывает разницу между системной таймзоной устройства и пользовательскими настройками, чтобы избежать рассинхронизации времени и ошибок с уведомлениями.

Самый сложный кейс со сменами оффсета

Логика становится еще сложнее, когда происходит смена оффсета внутри одной таймзоны из-за перехода между летним и зимним временем.

Рассмотрим это на примере Лиссабона, где переход на зимнее время происходит 27 октября в 02:00 по местному летнему времени.

Денис 10 октября (когда в Лиссабоне еще действует летнее время) купил билеты на концерт Beyonce 10 ноября (когда Лиссабон уже перейдет на зимнее время) и ставит себе задачу в UTasks на 10 ноября 10:00 «Купить цветы для Бейонсе».

Здесь возникают вопросы:

1. Что Денис подразумевает под временем: 10:00 зимнего времени (UTC+0) или 10:00 летнего времени (UTC+1)?

2. Как UTasks должен учесть смену оффсета, чтобы корректно интерпретировать и сохранить данные?

3. Прилично ли дарить Бейонсе букет за 20 евро или лучше накануне сэкономить на портвейне и раскошелиться на 25?

Смена оффсетов зависит от текущих правил таймзон, которые часто меняются (например, правительство может отменить переход на летнее время или изменить его график). Это делает невозможным заранее предсказать точное поведение без актуальной информации.

Решение: использование системного времени устройства

Операционная система устройства знает актуальные правила перехода между летним и зимним временем. Вместо того чтобы самостоятельно хранить и обновлять базу данных таймзон, UTasks может использовать ОС для выполнения следующих шагов:

1. Запрос форматированного времени с учетом таймзоны. При выборе даты в календаре (например, 10:00 10 ноября) UTasks обращается к системе с запросом: «Напечатай дату для таймзоны Лиссабон».

Если запрос сделан для 10 октября (летнее время), система вернет: 10:00 10 октября UTC+1. Если запрос сделан для 28 октября (зимнее время), система вернет: 10:00 28 октября UTC+0. Это работает, потому что система автоматически учитывает актуальный оффсет таймзоны на дату, запрошенную пользователем.

2. Извлечение оффсета из ответа системы. Форматированный результат от системы содержит оффсет (+1 для летнего времени, 0 для зимнего). UTasks извлекает этот оффсет и использует его в расчетах.

3. Корректировка времени для отправки на сервер. После получения оффсета UTasks вычисляет разницу между системной таймзоной устройства (например, Москва, UTC+3) и пользовательской таймзоной из настроек (Лиссабон).На основании этой разницы корректируется время: Денис выбирает 10:00 10 ноября, локальное время устройства — Москва (UTC+3). Приложение прибавляет разницу: 10:00 - 3 + 0 = 07:00 UTC и на сервер отправляется время в UTC, уже скорректированное для пользовательской таймзоны и ее актуального оффсета.

Чтобы поддерживать актуальность оффсетов, UTasks регулярно сверяет текущий оффсет системной таймзоны с оффсетом в пользовательских настройках. Если оффсет изменился (например, 28 октября при переходе на зимнее время), приложение обновляет настройки и продолжает использовать новые значения.

Поэтому, когда Денис 10 октября ставит задачу на 10:00 10 ноября, UTasks предполагает, что пользователь осведомлен о переходе на зимнее время и подразумевает зимний оффсет (UTC+0).

При сохранении задачи UTasks корректно применяет оффсет, актуальный для 10 ноября, чтобы гарантировать, что время останется точным.

Выводы

Работа с таймзонами оказалась одной из самых сложных задач в разработке UTasks, но и принесла бесценный опыт.

Основной вывод: хранение всех данных в UTC — это надежный фундамент, позволяющий обеспечить синхронность между пользователями в разных регионах.

Однако для создания действительно удобного интерфейса нужно предусмотреть множество нюансов: корректное отображение времени с учетом локальной и пользовательской таймзоны, своевременные уведомления и поддержку правил перехода между сезонами.

Наше решение — это баланс между автоматизацией (учет системного времени устройства) и гибкостью (возможность задавать таймзону вручную).

И ещё: наш разработчик Лёша регулярно прокачивает эту мышцу, экспериментируя с настройками, он постоянно переставляет таймзоны в своем приложении, чтобы вручную сверять корректность механизма, что позволяет нам не только оттачивать работу UTasks, но и выявлять тонкие моменты, которые невозможно учесть без живого опыта.

Переходите по ссылке и ставьте задачи с удовольствием! Welcome!

7
Начать дискуссию