Введение: почему время — это головная боль
В любом серьезном проекте на 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 = "%