Продвинутая работа с датами и временем в Rust: от NaiveDateTime до Timezone
Работа с датами и временем — одна из самых частых задач в backend-разработке, логировании, финансовых системах и обработке данных. В Rust стандартная библиотека предоставляет базовый модуль std::time, но для серьёзных проектов его недостаточно. На продвинутом уровне мы обращаемся к крейту chrono — де-факто стандарту для работы с датами и временем. В этой статье мы разберём сложные сценарии: работа с часовыми поясами, парсинг нестандартных форматов, манипуляции с длительностями и оптимизация производительности.
1. Установка и базовая структура chrono
Добавьте зависимость в Cargo.toml:
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
Основные типы chrono для продвинутой работы:
NaiveDateTime— дата-время без часового пояса (локальное).DateTime<Tz>— дата-время с привязкой к часовому поясу (например,Utc,Local,FixedOffset).Duration— интервал времени (отличается отstd::time::Duration, поддерживает отрицательные значения).TimeDelta— синоним дляDurationв новых версиях.
Пример создания текущего момента в UTC и локальном времени:
use chrono::{Utc, Local, DateTime};
fn main() {
let utc_now: DateTime<Utc> = Utc::now();
let local_now: DateTime<Local> = Local::now();
println!("UTC: {}", utc_now);
println!("Local: {}", local_now);
}
2. Работа с часовыми поясами (Timezone)
Одна из мощных возможностей chrono — поддержка Olson timezone database через крейт chrono-tz. Это позволяет работать с именами зон, такими как "Europe/Moscow" или "America/New_York".
Установите:
[dependencies]
chrono = "0.4"
chrono-tz = "0.8"
Пример конвертации времени между зонами:
use chrono::{Utc, DateTime, TimeZone};
use chrono_tz::Europe::Moscow;
fn main() {
let utc_time: DateTime<Utc> = Utc::now();
let moscow_time = utc_time.with_timezone(&Moscow);
println!("UTC: {}", utc_time);
println!("Moscow: {}", moscow_time);
}
Важный момент: всегда храните время в UTC в базе данных, а конвертируйте в локальное только при отображении пользователю. Это предотвращает путаницу с летним/зимним временем.
2.1. Работа с FixedOffset
Если вам известен фиксированный сдвиг (например, +03:00), используйте FixedOffset:
use chrono::{FixedOffset, DateTime, Utc};
fn main() {
let offset = FixedOffset::east_opt(3 * 3600).unwrap(); // +03:00
let now_utc: DateTime<Utc> = Utc::now();
let now_offset = now_utc.with_timezone(&offset);
println!("With offset +03:00: {}", now_offset);
}
3. Парсинг и форматирование дат (продвинутые техники)
Стандартный парсинг через parse_from_str требует точного формата. Но в реальных проектах форматы могут различаться (например, логи от разных систем). Рассмотрим несколько подходов.
3.1. Множественные форматы с try_parse
Используйте перебор форматов:
use chrono::NaiveDateTime;
fn parse_flexible(date_str: &str) -> Option<NaiveDateTime> {
let formats = [
"%Y-%m-%d %H:%M:%S",
"%d/%m/%Y %H:%M",
"%Y%m%dT%H%M%S",
"%b %d %Y %I:%M:%S%p",
];
for fmt in &formats {
if let Ok(dt) = NaiveDateTime::parse_from_str(date_str, fmt) {
return Some(dt);
}
}
None
}
fn main() {
let dates = vec![
"2024-03-15 14:30:00",
"15/03/2024 14:30",
"20240315T143000",
"Mar 15 2024 02:30:00PM",
];
for d in dates {
if let Some(parsed) = parse_flexible(d) {
println!("Parsed: {}", parsed);
}
}
}
3.2. Использование chrono::format::StrftimeItems
Для кастомного форматирования с динамическими элементами:
use chrono::{NaiveDateTime, format::StrftimeItems};
fn main() {
let dt = NaiveDateTime::parse_from_str("2024-03-15 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
let fmt = StrftimeItems::new("%A, %d %B %Y %H:%M");
let formatted = dt.format_with_items(fmt);
println!("Formatted: {}", formatted); // Friday, 15 March 2024 14:30
}
4. Манипуляции с Duration и TimeDelta
В продвинутой работе часто требуется вычитать даты, добавлять рабочие дни или вычислять разницу в секундах с учётом часовых поясов.
4.1. Работа с Duration (chrono)
use chrono::{Duration, Utc, DateTime};
fn main() {
let now = Utc::now();
let two_weeks_ago = now - Duration::weeks(2);
let in_3_days = now + Duration::days(3);
println!("Two weeks ago: {}", two_weeks_ago);
println!("In 3 days: {}", in_3_days);
// Разница между датами
let diff = now - two_weeks_ago;
println!("Difference in seconds: {}", diff.num_seconds());
println!("Difference in days: {}", diff.num_days());
}
4.2. Вычисление возраста (Age calculation)
use chrono::{NaiveDate, Local};
fn calculate_age(birth_date: NaiveDate) -> u32 {
let today = Local::now().date_naive();
let age = today.year() - birth_date.year();
// Проверка, был ли уже день рождения в этом году
if today.ordinal() < birth_date.ordinal()