Дата и время в Rust с крейтом chrono
Программы постоянно имеют дело со временем. «Сколько это заняло?» «Когда нужно оплатить счёт?» «Сколько дней осталось до поездки?» Rust делит такие вопросы на два вида.
Модуль std::time из стандартной библиотеки — это секундомер: он измеряет, сколько прошло времени. Про календари, месяцы и дни недели он ничего не знает. Для работы с календарём почти каждый проект на Rust использует крейт chrono. Эта страница начинается с секундомера, а потом переходит к календарю.
Замеряем прошедшее время с Instant
Заголовок раздела «Замеряем прошедшее время с Instant»Instant::now() снимает показание с часов компьютера. Позже .elapsed() говорит, сколько времени прошло с этого момента, в виде Duration:
use std::time::{Duration, Instant};
fn main() { let start = Instant::now();
let mut total: u64 = 0; for i in 0..1_000_000 { total += i; }
let elapsed = start.elapsed(); println!("total = {total}"); println!("заняло {:?}", elapsed); println!("заняло {} мс", elapsed.as_millis()); println!("меньше 5 секунд? {}", elapsed < Duration::from_secs(5));}Вывод мы здесь специально не показываем: время на каждом компьютере и при каждом запуске разное. Ты увидишь что-то вроде заняло 6.862793ms. Нажми кнопку два раза и сравни.
Instant годится только для замеров. Его нельзя напечатать как дату, и он идёт только вперёд. Для замера времени работы кода это ровно то, что нужно: на него никак не влияет, если кто-то переведёт системные часы.
Длительности
Заголовок раздела «Длительности»Duration — это отрезок времени, например «90 секунд». Его создают из секунд или миллисекунд, длительности можно складывать и сравнивать:
use std::time::Duration;
fn main() { let short = Duration::from_millis(1500); let long = Duration::from_secs(90);
println!("{:?}", short); println!("{:?}", long); println!("{} мс", short.as_millis()); println!("{:.1} с", short.as_secs_f64());
let total = short + long; println!("всего: {:?}", total); println!("{} мин {} с", long.as_secs() / 60, long.as_secs() % 60); println!("{}", short < long);}Вывод:
1.5s90s1500 мс1.5 свсего: 91.5s1 мин 30 сtrueФормат {:?} печатает длительность с английскими единицами (s, ms) — так её выводит сам Rust.
Duration встретится тебе во многих местах: std::thread::sleep(Duration::from_millis(100)) приостанавливает поток, а тайм-ауты в сетевых библиотеках тоже принимают Duration.
Время на настенных часах: SystemTime
Заголовок раздела «Время на настенных часах: SystemTime»SystemTime — это время на часах компьютера, тех, что видны в углу экрана. Обычно с ним делают одно: спрашивают, сколько прошло с эпохи Unix — 1 января 1970 года (UTC). Это число называют меткой времени Unix (Unix timestamp):
use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); let since_1970 = now .duration_since(UNIX_EPOCH) .expect("часы выставлены раньше 1970 года"); println!("Секунд с 1 января 1970: {}", since_1970.as_secs());}Число зависит от того, когда ты запускаешь программу, поэтому вывода здесь тоже нет. Почему duration_since возвращает Result? Потому что системные часы могут ошибаться или их могут перевести назад. По этой же причине для замеров кода нужно брать Instant, а не SystemTime.
Вот почти и всё, что умеет std::time. Что такое «март» или «пятница», он понятия не имеет. Для этого нужен chrono.
Добавляем chrono
Заголовок раздела «Добавляем chrono»Добавь крейт в проект (если это для тебя новое, см. Cargo и крейты):
cargo add chrono[dependencies]chrono = "0.4"В Rust Playground chrono тоже есть, так что все кнопки ниже работают. У chrono два семейства типов:
- Наивные (naive) типы (
NaiveDate,NaiveTime,NaiveDateTime) — это даты и время без часового пояса. «14 марта 2025» или «09:30», записанные в календаре. В повседневном коде чаще всего используют их. DateTime<Tz>— это момент времени с часовым поясом, напримерDateTime<Utc>илиDateTime<Local>.
Создаём даты из готовых значений
Заголовок раздела «Создаём даты из готовых значений»NaiveDate::from_ymd_opt(year, month, day) создаёт дату. Он возвращает Option, потому что не каждое сочетание чисел — настоящая дата:
use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); println!("{date}"); println!("год {}, месяц {}, день {}", date.year(), date.month(), date.day());
let time = NaiveTime::from_hms_opt(9, 30, 0).unwrap(); let meeting = date.and_time(time); println!("{meeting}");
let other = date.and_hms_opt(17, 45, 10).unwrap(); println!("{other}");
let impossible = NaiveDate::from_ymd_opt(2025, 2, 30); println!("{:?}", impossible);}Вывод:
2025-03-14год 2025, месяц 3, день 142025-03-14 09:30:002025-03-14 17:45:10None30 февраля не бывает, поэтому ты получаешь None, а не сломанную дату. Для готовых значений, которые ты набрал сам, .unwrap() подходит. Для значений от пользователя обработай None.
Строка use chrono::Datelike подключает методы .year(), .month() и .day(). (Timelike делает то же самое для .hour() и .minute().)
Текущие дата и время
Заголовок раздела «Текущие дата и время»Utc::now() даёт текущий момент в UTC. Local::now() — в часовом поясе самого компьютера:
use chrono::{Local, Utc};
fn main() { let utc_now = Utc::now(); let local_now = Local::now();
println!("UTC: {}", utc_now); println!("Местное: {}", local_now); println!("Сегодня: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive(); println!("Как NaiveDate: {today}");}В выводе будет сегодняшняя дата, так что он меняется каждый день. Серверы Playground работают в UTC, поэтому там обе строки покажут одинаковое время. .date_naive() отбрасывает часовой пояс и время и оставляет простую NaiveDate. Хорошая привычка: получай «сейчас» один раз, на краю программы, а потом передавай дату в свои функции. Так их легко тестировать — ты увидишь это в примере со сроками.
Форматируем даты
Заголовок раздела «Форматируем даты».format() принимает шаблон. Каждый код с % заменяется частью даты. %Y — год, %m — номер месяца, %d — день:
use chrono::NaiveDate;
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); let dt = date.and_hms_opt(9, 5, 0).unwrap();
println!("{}", date.format("%Y-%m-%d")); println!("{}", date.format("%d.%m.%Y")); println!("{}", date.format("%A, %B %-d, %Y")); println!("{}", date.format("%a %d %b")); println!("{}", dt.format("%H:%M")); println!("{}", dt.format("%-I:%M %p"));
let file_name = date.format("report-%Y%m%d.txt").to_string(); println!("{file_name}");}Вывод:
2025-03-1414.03.2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMreport-20250314.txtНазвания дней недели и месяцев chrono по умолчанию печатает по-английски. Если нужны русские названия, проще всего взять номер месяца или дня недели и подставить слово из своего массива.
Коды, которые нужны чаще всего: %Y — год, %m — месяц (01–12), %d — день (01–31), %H — час (00–23), %M — минута, %S — секунда, %A — название дня недели, %B — название месяца. Знак - после % убирает ведущий ноль, так что %-d печатает 5, а не 05. .format() возвращает значение, которое можно напечатать. Вызови .to_string(), когда нужна String.
Превращаем текст в дату
Заголовок раздела «Превращаем текст в дату»NaiveDate::parse_from_str делает обратное: читает текст по тому же шаблону с %. Он возвращает Result, потому что текст может не подойти под шаблон:
use chrono::{NaiveDate, NaiveDateTime};
fn main() { let a = NaiveDate::parse_from_str("2025-03-14", "%Y-%m-%d"); println!("{:?}", a);
let b = NaiveDate::parse_from_str("14.03.2025", "%d.%m.%Y"); println!("{:?}", b);
let c: Result<NaiveDate, _> = "2025-03-14".parse(); println!("{:?}", c);
let d = NaiveDateTime::parse_from_str("2025-03-14 09:30", "%Y-%m-%d %H:%M"); println!("{:?}", d);
for text in ["2025-13-01", "March 14", "2025-03"] { match NaiveDate::parse_from_str(text, "%Y-%m-%d") { Ok(date) => println!("Разобрано: {date}"), Err(e) => println!("Не удалось разобрать {text:?}: {e}"), } }}Вывод:
Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14T09:30:00)Не удалось разобрать "2025-13-01": input is out of rangeНе удалось разобрать "March 14": input contains invalid charactersНе удалось разобрать "2025-03": premature end of inputДля стандартной формы YYYY-MM-DD можно просто вызвать .parse(). 13-го месяца не бывает, поэтому первый неудачный пример — «вне допустимого диапазона» (out of range). Сообщения об ошибках chrono пишет по-английски: «во вводе недопустимые символы» и «ввод закончился слишком рано». Они короткие, но достаточно понятные.
Арифметика с датами
Заголовок раздела «Арифметика с датами»Прибавить дни к дате вручную на удивление трудно: в месяцах разное число дней, а ещё есть високосные годы. chrono делает это за тебя. Используй checked_add_days с Days, checked_add_months с Months или прибавляй TimeDelta:
use chrono::{Days, Months, NaiveDate, TimeDelta};
fn main() { let start = NaiveDate::from_ymd_opt(2025, 1, 31).unwrap();
let plus_30 = start.checked_add_days(Days::new(30)).unwrap(); println!("через 30 дней: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap(); println!("7 дней назад: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap(); println!("через месяц: {next_month}");
let week_later = start + TimeDelta::weeks(1); println!("через неделю: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap(); let later = dt + TimeDelta::hours(2); println!("через 2 часа: {later}");}Вывод:
через 30 дней: 2025-03-027 дней назад: 2025-01-24через месяц: 2025-02-28через неделю: 2025-02-07через 2 часа: 2025-02-01 01:30:00Посмотри на строку «через месяц»: 31 января плюс один месяц — это 28 февраля, потому что 31 февраля не существует. chrono выбирает последний допустимый день. Методы checked_ возвращают Option, который будет None, только если результат выйдет за (очень большой) диапазон лет chrono. А прибавление 2 часов к 23:30 правильно переносит нас на следующий день.
Дни недели и разница между двумя датами
Заголовок раздела «Дни недели и разница между двумя датами».weekday() (из Datelike) сообщает день недели. Если вычесть одну дату из другой, получится TimeDelta, а .num_days() превращает его в число:
use chrono::{Datelike, NaiveDate, Weekday};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap(); let day = date.weekday(); println!("{date} — это {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun); println!("выходной? {weekend}");
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); let end = NaiveDate::from_ymd_opt(2025, 12, 25).unwrap(); let gap = end - start; println!("дней: {}, полных недель: {}", gap.num_days(), gap.num_weeks()); println!("в обратную сторону: {} дней", (start - end).num_days());
let check_in = start.and_hms_opt(14, 0, 0).unwrap(); let check_out = NaiveDate::from_ymd_opt(2025, 1, 4).unwrap().and_hms_opt(11, 0, 0).unwrap(); println!("в отеле: {} ч", (check_out - check_in).num_hours());}Вывод:
2025-03-15 — это Sat (Saturday)выходной? trueдней: 358, полных недель: 51в обратную сторону: -358 днейв отеле: 69 чWeekday — это перечисление с вариантами от Mon до Sun, так что по нему можно делать match. Заметь, что разница бывает отрицательной, если вторая дата позже первой. Это удобно: отрицательное число оставшихся дней означает «просрочено».
Пример: сколько дней до срока
Заголовок раздела «Пример: сколько дней до срока»Соберём всё вместе. Функция принимает «сегодня» как параметр, а не вызывает Local::now() внутри. Поэтому вывод ниже всегда одинаковый, а функцию легко тестировать. В настоящей программе ты передавал бы Local::now().date_naive().
use chrono::NaiveDate;
fn days_left(today: NaiveDate, deadline: NaiveDate) -> String { let days = (deadline - today).num_days(); match days { 0 => String::from("срок сегодня"), 1 => String::from("срок завтра"), d if d > 1 => format!("осталось дней: {d}"), -1 => String::from("просрочено на 1 день"), d => format!("просрочено дней: {}", -d), }}
fn main() { let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [ ("Оплатить аренду", "2025-06-10"), ("Отправить отчёт", "2025-06-11"), ("Продлить паспорт", "2025-07-01"), ("Вернуть книгу", "2025-06-09"), ("Счёт от стоматолога", "2025-06-03"), ];
println!("Сегодня {}", today.format("%d.%m.%Y")); for (name, date_text) in tasks { let deadline = NaiveDate::parse_from_str(date_text, "%Y-%m-%d").unwrap(); println!("{:<20} {} {}", name, deadline.format("%d.%m"), days_left(today, deadline)); }}Вывод:
Сегодня 10.06.2025Оплатить аренду 10.06 срок сегодняОтправить отчёт 11.06 срок завтраПродлить паспорт 01.07 осталось дней: 21Вернуть книгу 09.06 просрочено на 1 деньСчёт от стоматолога 03.06 просрочено дней: 7Попробуй поменять today на другую дату и запустить снова. {:<20} дополняет название пробелами до 20 символов, чтобы колонки стояли ровно (см. Вывод на экран). Ширина считается в символах, а не в байтах, поэтому русские названия выравниваются без проблем.
chrono — не единственный вариант. Крейт time — популярная альтернатива поменьше, а более новый крейт jiff особенно аккуратно обращается с часовыми поясами и переходом на летнее время. Здесь мы используем chrono, потому что он самый распространённый и есть в Playground.
Частые ошибки
Заголовок раздела «Частые ошибки»Забыть use chrono::Datelike. Без этой строки .year(), .month() и .weekday() не работают. Ошибка говорит, что метод «приватный» (private), и это сбивает с толку, но строка help подсказывает настоящее решение:
error[E0624]: method `year` is private --> src/main.rs:5:25 |5 | println!("{}", date.year()); | ^^^^ private method | = help: items from traits can only be used if the trait is in scopehelp: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it |1 + use chrono::Datelike; |Компилятор объясняет, что методы трейта доступны, только когда трейт в области видимости, и предлагает добавить use chrono::Datelike;.
Перепутать %m и %M. %m — это месяц, %M — минута. Если у значения есть и дата, и время, неправильный код не даст ошибки. Он просто молча напечатает минуты:
use chrono::NaiveDate;
fn main() { let meeting = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap().and_hms_opt(9, 5, 0).unwrap(); println!("{}", meeting.format("%Y-%M-%d")); // неправильно: минуты println!("{}", meeting.format("%Y-%m-%d")); // правильно: месяц}Вывод:
2025-05-142025-03-14Замерять код через SystemTime. Системные часы могут прыгать (например, когда синхронизируются через интернет). Для замеров используй Instant, а SystemTime или chrono — только когда нужна настоящая дата.
Вызывать Local::now() глубоко внутри логики. Тогда результаты меняются каждый день, а тесты писать трудно. Передавай дату параметром, как это делает days_left.
std::time::Instantизмеряет прошедшее время;Duration— это отрезок времени;SystemTime— настенные часы (подходят для меток времени Unix).- Для календаря добавь
chrono. ИспользуйNaiveDate/NaiveDateTimeдля дат без часового пояса иUtc::now()/Local::now()для текущего момента. from_ymd_optвозвращаетOption, аparse_from_str—Result, потому что не всякий ввод — настоящая дата..format("%Y-%m-%d")превращает дату в текст с помощью кодов%. Запомни:%m— месяц,%M— минута.- Прибавляй время через
checked_add_days,checked_add_monthsили+ TimeDelta. Вычти одну дату из другой, чтобы получитьTimeDelta, и вызови.num_days(). - Передавай «сегодня» в функции, а не читай часы внутри них.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Каким типом замерить, сколько времени выполняется кусок кода?
2. Что вернёт NaiveDate::from_ymd_opt(2025, 2, 30)?
3. Что означает %M в строке формата chrono?
4. Ты вычитаешь две NaiveDate: deadline - today. Как получить число дней?
5. Сколько будет 31 января 2025 плюс Months::new(1) в chrono?
Результат: 0 / 5
Что почитать
Заголовок раздела «Что почитать»- std::time — стандартная библиотека Rust (на английском) —
Instant,DurationиSystemTimeс дополнительными примерами. - Документация chrono — docs.rs (на английском) — полное руководство по всем типам и методам chrono.
- Коды форматирования chrono — docs.rs (на английском) — полный список кодов
%дляformatиparse_from_str. - Date and Time — Rust Cookbook (на английском) — короткие рецепты для частых задач с датами и временем.
- Документация jiff — docs.rs (на английском) — более новый крейт для дат и времени с хорошей поддержкой часовых поясов, если захочешь сравнить.