Перейти к содержимому

Дата и время в Rust с крейтом chrono

Программы постоянно имеют дело со временем. «Сколько это заняло?» «Когда нужно оплатить счёт?» «Сколько дней осталось до поездки?» Rust делит такие вопросы на два вида.

Модуль std::time из стандартной библиотеки — это секундомер: он измеряет, сколько прошло времени. Про календари, месяцы и дни недели он ничего не знает. Для работы с календарём почти каждый проект на Rust использует крейт chrono. Эта страница начинается с секундомера, а потом переходит к календарю.

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.5s
90s
1500 мс
1.5 с
всего: 91.5s
1 мин 30 с
true
▶ Попробуй сам

Формат {:?} печатает длительность с английскими единицами (s, ms) — так её выводит сам Rust.

Duration встретится тебе во многих местах: std::thread::sleep(Duration::from_millis(100)) приостанавливает поток, а тайм-ауты в сетевых библиотеках тоже принимают Duration.

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.

Добавь крейт в проект (если это для тебя новое, см. Cargo и крейты):

Terminal window
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, день 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Попробуй сам

30 февраля не бывает, поэтому ты получаешь 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-14
14.03.2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
report-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-02
7 дней назад: 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 scope
help: 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-14
2025-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