Работа с временными зонами в Rust: полный гайд для бэкенд-разработчика

Онлайн-тренажер Python для начинающих

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

Начать курс

Введение: почему время — это головная боль

В любом серьезном проекте на Rust, будь то веб-сервер на Actix, парсер логов или система мониторинга, рано или поздно встает вопрос: как правильно работать с временными зонами в Rust. Казалось бы, что может быть проще? Взял timestamp, вывел на экран — и готово. Но реальность жестока: один забытый часовой пояс превращает логи в кашу, а база данных начинает врать.

В этой статье мы разберем, как экосистема Rust решает проблему времени. Вы узнаете, почему chrono стал стандартом де-факто, как не сойти с ума с UTC и Local, и как обрабатывать экзотические часовые пояса вроде «Asia/Yekaterinburg» без боли. Поехали!

Почему стандартная библиотека Rust не тянет?

В std::time есть SystemTime, Instant и Duration. Этого хватает для замеров производительности и простых таймеров. Но когда речь заходит о работе с временными зонами в Rust, стандартная библиотека пасует: в ней нет ни парсинга строк вида "2024-03-15T10:30:00+05:00", ни информации о часовых поясах, ни перевода из UTC в местное время.

Именно здесь на сцену выходит крейт chrono. Это швейцарский нож для дат и времени в Rust. Установим его в Cargo.toml:

[dependencies]
chrono = { version = "0.4", features = ["serde"] }

Флаг serde нужен для сериализации/десериализации — пригодится, если вы работаете с JSON API.

Основы chrono: UTC, Local и наивное время

В chrono есть три главных типа для представления момента времени:

  • DateTime<Utc> — время в UTC. Золотой стандарт для серверов.
  • DateTime<Local> — местное время системы. Удобно для логов на десктопе.
  • NaiveDateTime — "наивное" время без часового пояса. Опасно, но иногда нужно.

Давайте посмотрим, как получить текущее время в разных зонах:

use chrono::{Utc, Local, DateTime};

fn main() {
    let utc_now: DateTime<Utc> = Utc::now();
    println!("Сейчас в UTC: {}", utc_now);

    let local_now: DateTime<Local> = Local::now();
    println!("Сейчас локально: {}", local_now);
}

Запустите этот код — и вы увидите разницу. Обратите внимание: DateTime — это обобщенный тип, который параметризуется часовым поясом. Это значит, что компилятор не даст вам случайно сложить время из UTC и Local без явного преобразования. Безопасность типов на страже времени!

Конвертация между часовыми поясами

Самая частая задача — перевести UTC в местное время пользователя. Допустим, ваш сервер хранит всё в UTC, а клиент находится в часовом поясе Asia/Yekaterinburg (UTC+5). Как это сделать?

Используйте метод with_timezone:

use chrono::{Utc, Local, TimeZone, DateTime};

fn main() {
    let utc_time: DateTime<Utc> = Utc::now();
    
    // Конвертируем в местное время системы
    let local_time: DateTime<Local> = utc_time.with_timezone(&Local);
    println!("UTC: {} -> Local: {}", utc_time, local_time);
    
    // Если нужен конкретный пояс — используем FixedOffset
    let ekb_offset = chrono::FixedOffset::east_opt(5 * 3600).unwrap();
    let ekb_time: DateTime<FixedOffset> = utc_time.with_timezone(&ekb_offset);
    println!("UTC: {} -> EKB: {}", utc_time, ekb_time);
}

Обратите внимание: FixedOffset::east_opt возвращает Option, потому что не любое смещение валидно (например, 25 часов — это перебор). Всегда обрабатывайте None.

А что если у вас работа с временными зонами в Rust сложнее, чем просто фиксированный сдвиг? Например, нужно учитывать летнее время? Тут на помощь приходит крейт chrono-tz.

chrono-tz: умные часовые пояса

Крейт chrono-tz использует базу данных IANA (она же Olson). Это те самые имена вроде "America/New_York", "Europe/Moscow". Установите его:

[dependencies]
chrono = "0.4"
chrono-tz = "0.8"

Теперь мы можем конвертировать время с учетом DST (daylight saving time):

use chrono::{Utc, DateTime};
use chrono_tz::Tz;

fn main() {
    let utc_time: DateTime<Utc> = Utc::now();
    
    // Парсим часовой пояс из строки (можно брать из конфига или от клиента)
    let tz: Tz = "America/New_York".parse().unwrap();
    let ny_time = utc_time.with_timezone(&tz);
    println!("UTC: {} -> Нью-Йорк: {}", utc_time, ny_time);
    
    // Проверяем, действует ли летнее время
    if let Some(offset) = ny_time.offset() {
        println!("Смещение: {} часов", offset.local_minus_utc() / 3600);
    }
}

Этот код сам учтет, что в марте часы переводят вперед, а в ноябре — назад. Очень удобно для международных проектов.

Парсинг дат из строк: не дайте себя обмануть

В реальной жизни даты приходят из JSON, CSV, логов. Парсинг временных зон в Rust — это отдельное искусство. chrono поддерживает форматы по стандарту ISO 8601 и кастомные форматы через format! макрос.

Самый простой случай — строка вида "2024-03-15T10:30:00Z" (Z означает UTC):

use chrono::{DateTime, Utc};

fn main() {
    let date_str = "2024-03-15T10:30:00Z";
    let parsed: DateTime<Utc> = date_str.parse().unwrap();
    println!("Распарсено: {}", parsed);
}

Если строка содержит смещение ("+05:00" или "-07:00"), chrono сам определит тип:

use chrono::{DateTime, FixedOffset};

fn main() {
    let date_str = "2024-03-15T15:30:00+05:00";
    let parsed: DateTime<FixedOffset> = date_str.parse().unwrap();
    println!("Смещение: {}", parsed.offset().local_minus_utc());
}

А если формат нестандартный? Используйте NaiveDateTime::parse_from_str:

use chrono::NaiveDateTime;

fn main() {
    let date_str = "2024/03/15 10:30:00";
    let fmt = "%                

Блоги

Рекомендаций по книгам