🦀 Rust

Работа с датой и временем в Rust: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты ведешь дневник. Каждый день ты записываешь: «Сегодня 15 марта, 14:30. Я сделал уроки». Если бы ты просто писал «сделал уроки» без даты, ты бы быстро запутался — когда именно это было? Вчера? Неделю назад?

В программировании то же самое. Компьютеры постоянно работают со временем: когда был отправлен твой комментарий в соцсети, когда истекает срок действия пароля, через сколько минут начнется онлайн-урок. Без умения работать с датой и временем ни одно серьезное приложение не обойдется.

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

💡 Важно: В отличие от некоторых других языков, в Rust нет встроенной работы с датами в стандартной библиотеке (кроме простого замера времени). Для полноценной работы используется крейт chrono — это как установка дополнительного приложения на телефон, чтобы получить нужную функцию.

Теория: как это работает изнутри

Давай заглянем «под капот» и разберемся, как компьютер вообще понимает время.

Машинное время: Unix Timestamp

Компьютеры не думают «15 марта 2025 года». Они считают секунды. Есть точка отсчета — 1 января 1970 года, 00:00:00 UTC. Это называется Unix epoch (эпоха Unix). Любой момент времени — это количество секунд, прошедших с этой точки.

Например, число 1742000000 — это примерно 15 марта 2025 года. Компьютеру так удобнее: он просто хранит одно число, а не кучу полей (год, месяц, день, час...).

Проблема часовых поясов

Когда в Москве 15:00, в Нью-Йорке еще 7:00 утра. Если ты пишешь международное приложение, нужно хранить время в едином формате (обычно UTC), а показывать пользователю — в его часовом поясе.

Rust и крейт chrono строго разделяют:

  • Наивное время (NaiveDateTime) — просто «15 марта 2025, 14:30». Без привязки к часовому поясу. Подходит для локальных событий (например, «занятие начинается в 14:30» — без учета, где ты находишься).
  • Осознанное время (DateTime) — время с указанием часового пояса. Например, «15 марта 2025, 14:30 MSK» или «15 марта 2025, 11:30 UTC».

Високосные секунды и прочие сложности

Земля вращается неравномерно, поэтому время от времени добавляют високосные секунды. Библиотека chrono умеет с этим работать, но в обычных задачах это редко нужно.

⚠️ Подводный камень: В Rust нет встроенного типа «дата» в стандартной библиотеке. Есть только std::time::Instant (для замера интервалов) и std::time::SystemTime (для работы с системным временем). Но для удобной работы с датами (день, месяц, год) нужно подключать chrono.

Синтаксис в Rust: все формы и варианты

Для работы с датами мы используем крейт chrono. Добавь его в Cargo.toml:

[dependencies]
chrono = "0.4"

Основные типы в chrono

Тип Описание Пример
NaiveDate Дата без времени (год, месяц, день) 2025-03-15
NaiveTime Время без даты (часы, минуты, секунды) 14:30:00
NaiveDateTime Дата + время без часового пояса 2025-03-15 14:30:00
DateTime<Utc> Дата + время в UTC 2025-03-15 11:30:00 UTC
DateTime<FixedOffset> Дата + время с фиксированным смещением 2025-03-15 14:30:00 +03:00
Duration Промежуток времени (разница между датами) 5 дней, 3 часа

Основные методы

Метод Что делает Пример
Utc::now() Текущее время в UTC Utc::now()
Local::now() Текущее время в локальном часовом поясе Local::now()
NaiveDate::from_ymd_opt() Создать дату из года, месяца, дня (без паники) NaiveDate::from_ymd_opt(2025, 3, 15)
NaiveTime::from_hms_opt() Создать время из часов, минут, секунд NaiveTime::from_hms_opt(14, 30, 0)
.format() Преобразовать дату в строку по шаблону dt.format("%Y-%m-%d")
.signed_duration_since() Разница между двумя датами d2.signed_duration_since(d1)
.checked_add_signed() Прибавить Duration к дате (без паники) date.checked_add_signed(Duration::days(5))
.timestamp() Получить Unix timestamp (секунды с 1970 года) dt.timestamp()
🔍 Обрати внимание: Многие методы в chrono возвращают Option (например, from_ymd_opt). Это защита от ошибок: если ты попытаешься создать дату 30 февраля, ты получишь None, а не панику программы.

Примеры кода — от простого к сложному

Пример 1: Получаем текущее время

// Подключаем крейт chrono в проект
use chrono::{Utc, Local};

fn main() {
    // Получаем текущее время в UTC (всемирное координированное время)
    let now_utc = Utc::now();
    println!("Сейчас в UTC: {}", now_utc);

    // Получаем текущее время в локальном часовом поясе (там, где ты)
    let now_local = Local::now();
    println!("Сейчас локальное время: {}", now_local);
}

Объяснение: Utc::now() возвращает текущий момент времени в UTC. Local::now() — в твоем часовом поясе. Вывод будет выглядеть примерно так: 2025-03-15 11:30:00 UTC и 2025-03-15 14:30:00 +03:00.

Пример 2: Создаем конкретную дату

use chrono::{NaiveDate, NaiveTime, NaiveDateTime};

fn main() {
    // Создаем дату 15 марта 2025 года
    // from_ymd_opt возвращает Option - безопасный способ
    let date = NaiveDate::from_ymd_opt(2025, 3, 15)
        .expect("Ошибка: такой даты не существует!");

    println!("Дата: {}", date); // Выведет: 2025-03-15

    // Создаем время 14:30:00
    let time = NaiveTime::from_hms_opt(14, 30, 0)
        .expect("Ошибка: такого времени не существует!");

    println!("Время: {}", time); // Выведет: 14:30:00

    // Объединяем дату и время в один объект
    let datetime = NaiveDateTime::new(date, time);
    println!("Полная дата и время: {}", datetime);
    // Выведет: 2025-03-15 14:30:00
}

Объяснение: from_ymd_opt — безопасный конструктор. Если передать, например, 30 февраля, он вернет None. .expect() — это способ сказать: «Если ошибка, останови программу и покажи сообщение».

Пример 3: Форматирование даты

use chrono::{Utc, DateTime};

fn main() {
    let now = Utc::now();

    // Форматируем дату в разных стилях
    // %Y - год (4 цифры), %m - месяц (2 цифры), %d - день (2 цифры)
    println!("Стандартный формат: {}", now.format("%Y-%m-%d"));
    // Выведет: 2025-03-15

    // Русский стиль: день.месяц.год
    println!("Русский формат: {}", now.format("%d.%m.%Y"));
    // Выведет: 15.03.2025

    // Полный формат с днем недели и временем
    // %A - полное название дня недели, %H - часы, %M - минуты, %S - секунды
    println!("Полный формат: {}", now.format("%A, %d %B %Y %H:%M:%S"));
    // Выведет: Saturday, 15 March 2025 11:30:00

    // Только время
    println!("Только время: {}", now.format("%H:%M:%S"));
    // Выведет: 11:30:00
}

Объяснение: Метод .format() принимает строку-шаблон. Символы с % — это плейсхолдеры, которые заменяются на реальные значения. Например, %d — день месяца (01-31), %B — полное название месяца (March).

Пример 4: Разница между датами

use chrono::{NaiveDate, Duration};

fn main() {
    // Создаем две даты
    let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); // 1 января
    let end = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();  // 15 марта

    // Вычисляем разницу в днях
    // signed_duration_since возвращает Duration (может быть отрицательным)
    let diff = end.signed_duration_since(start);

    // Duration хранит дни, часы, минуты, секунды
    println!("Прошло дней: {}", diff.num_days()); // 73 дня

    // Можно получить разницу в часах
    println!("Прошло часов: {}", diff.num_hours()); // 1752 часа

    // Разница в неделях
    println!("Прошло недель: {}", diff.num_weeks()); // 10 недель

    // Обратная разница (от start к end)
    let reverse_diff = start.signed_duration_since(end);
    println!("Обратная разница в днях: {}", reverse_diff.num_days());
    // Выведет: -73 (отрицательное число)
}

Объяснение: signed_duration_since возвращает Duration — объект, который хранит промежуток времени. Методы .num_days(), .num_hours() и т.д. позволяют получить разницу в нужных единицах. Если первая дата позже второй, разница будет отрицательной.

Пример 5: Прибавляем и вычитаем время

use chrono::{NaiveDate, Duration};

fn main() {
    let today = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();

    // Прибавляем 5 дней
    let future = today.checked_add_signed(Duration::days(5))
        .expect("Ошибка при прибавлении дней");
    println!("Через 5 дней: {}", future); // 2025-03-20

    // Вычитаем 2 недели
    let past = today.checked_sub_signed(Duration::weeks(2))
        .expect("Ошибка при вычитании недель");
    println!("2 недели назад: {}", past); // 2025-03-01

    // Прибавляем 3 месяца (используем специальный метод)
    // Duration не умеет работать с месяцами (они разной длины)
    // Для месяцев используем отдельные функции
    let months_later = today.checked_add_months(chrono::Months::new(3))
        .expect("Ошибка при прибавлении месяцев");
    println!("Через 3 месяца: {}", months_later); // 2025-06-15

    // Прибавляем 1 год
    let year_later = today.checked_add_months(chrono::Months::new(12))
        .expect("Ошибка при прибавлении года");
    println!("Через год: {}", year_later); // 2026-03-15
}

Объяснение: checked_add_signed и checked_sub_signed — безопасные методы для прибавления/вычитания Duration. Они возвращают Option, потому что результат может выйти за пределы допустимых дат. Для месяцев используется checked_add_months, так как месяцы имеют разную длину (28-31 день).

Пример 6: Работа с часовыми поясами

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

fn main() {
    // Текущее время в UTC
    let utc_now = Utc::now();
    println!("Время в UTC: {}", utc_now);

    // Создаем часовой пояс для Москвы (UTC+3)
    let moscow_offset = FixedOffset::east_opt(3 * 3600).unwrap(); // 3 часа в секундах
    let moscow_time = utc_now.with_timezone(&moscow_offset);
    println!("Время в Москве: {}", moscow_time);

    // Создаем часовой пояс для Нью-Йорка (UTC-5, зимнее время)
    let ny_offset = FixedOffset::west_opt(5 * 3600).unwrap();
    let ny_time = utc_now.with_timezone(&ny_offset);
    println!("Время в Нью-Йорке: {}", ny_time);

    // Конвертируем локальное время в UTC
    let local_now = Local::now();
    let local_as_utc = local_now.with_timezone(&Utc);
    println!("Локальное время как UTC: {}", local_as_utc);

    // Разница во времени между городами
    let diff = moscow_time.signed_duration_since(ny_time);
    println!("Разница Москва-Нью-Йорк: {} часов", diff.num_hours());
    // Выведет: 8 часов (3 - (-5) = 8)
}

Объяснение: FixedOffset::east_opt() создает смещение к востоку от UTC (положительное), west_opt() — к западу (отрицательное). Аргумент — количество секунд смещения. .with_timezone() конвертирует время из одного часового пояса в другой.

Пример 7: Парсинг даты из строки

use chrono::{NaiveDateTime, DateTime, Utc, NaiveDate};

fn main() {
    // Парсим дату из строки в формате "ГГГГ-ММ-ДД ЧЧ:ММ:СС"
    let date_str = "2025-03-15 14:30:00";
    let parsed = NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S")
        .expect("Не удалось распарсить дату");
    println!("Распарсено: {}", parsed);

    // Парсим дату в американском формате "ММ/ДД/ГГГГ"
    let us_date = "03/15/2025";
    let parsed_us = NaiveDate::parse_from_str(us_date, "%m/%d/%Y")
        .expect("Не удалось распарсить американскую дату");
    println!("Американский формат: {}", parsed_us);

    // Парсим дату с часовым поясом
    let with_tz = "2025-03-15T14:30:00+03:00";
    let parsed_tz = DateTime::parse_from_rfc3339(with_tz)
        .expect("Не удалось распарсить дату с часовым поясом");
    println!("С часовым поясом: {}", parsed_tz);

    // Обработка ошибок парсинга
    let bad_date = "15.03.2025"; // Русский формат
    match NaiveDate::parse_from_str(bad_date, "%Y-%m-%d") {
        Ok(date) => println!("Успешно: {}", date),
        Err(e) => println!("Ошибка парсинга: {}", e),
    }
    // Выведет ошибку, так как формат не совпадает
}

Объяснение: parse_from_str — мощный метод для преобразования строки в дату. Первый аргумент — строка, второй — формат. Если строка не соответствует формату, возвращается ошибка. parse_from_rfc3339 — специальный метод для парсинга дат в формате RFC 3339 (стандартный интернет-формат).

Пример 8: Таймер и замер времени

use std::time::{Instant, Duration};
use std::thread::sleep;

fn main() {
    // Засекаем время начала
    let start = Instant::now();

    // Имитируем какую-то работу (спим 2 секунды)
    println!("Начинаем работу...");
    sleep(Duration::from_secs(2));

    // Засекаем время окончания
    let end = Instant::now();

    // Вычисляем разницу
    let elapsed = end.duration_since(start);

    println!("Прошло: {:?}", elapsed);
    println!("Миллисекунд: {}", elapsed.as_millis());
    println!("Микросекунд: {}", elapsed.as_micros());
    println!("Наносекунд: {}", elapsed.as_nanos());

    // Проверяем, прошло ли больше 1 секунды
    if elapsed > Duration::from_secs(1) {
        println!("Работа заняла больше 1 секунды!");
    }

    // Создаем свой Duration
    let five_seconds = Duration::from_secs(5);
    println!("5 секунд = {} миллисекунд", five_seconds.as_millis());
}

Объяснение: Instant — тип из стандартной библиотеки Rust для замера интервалов времени. Он не привязан к календарю, только к «моментам». duration_since вычисляет разницу между двумя моментами. Duration — это промежуток времени, который можно создать из секунд, миллисекунд и т.д.

Сравнение с Python: чем похоже и чем отличается

Аспект Python (datetime) Rust (chrono)
Получение текущего времени datetime.now() Utc::now() или Local::now()
Создание даты date(2025, 3, 15) NaiveDate::from_ymd_opt(2025, 3, 15).unwrap()
Форматирование date.strftime("%Y-%m-%d") date.format("%Y-%m-%d")
Парсинг из строки datetime.strptime("2025-03-15", "%Y-%m-%d") NaiveDate::parse_from_str("2025-03-15", "%Y-%m-%d")
Разница между датами date2 - date1 (возвращает timedelta) date2.signed_duration_since(date1) (возвращает Duration)
Часовые пояса pytz или zoneinfo FixedOffset или Utc
Безопасность Может выбросить исключение при неверной дате Возвращает Option или Result — нет исключений
Производительность Хорошая Отличная (Rust — системный язык)

Главные отличия:

  • Безопасность: В Rust почти все методы возвращают Option или Result. В Python — исключения. Rust заставляет тебя обрабатывать возможные ошибки на этапе компиляции.
  • Явность: В Rust нужно явно указывать, работаешь ли ты с наивным временем или с часовым поясом. В Python можно случайно смешать.
  • Синтаксис: В Python методы вызываются через точку от объекта (date.strftime()), в Rust — тоже через точку, но названия методов длиннее и описательнее.
  • Стандартная библиотека: В Python datetime встроен. В Rust нужно подключать внешний крейт chrono.

Типичные ошибки и подводные камни

1. Забыл подключить chrono

// ОШИБКА: chrono не подключен
fn main() {
    let now = Utc::now(); // Ошибка компиляции!
}

Решение: Добавь chrono = "0.4" в Cargo.toml и use chrono::*; в начале файла.

2. Использование небезопасных конструкторов

// ОШИБКА: from_ymd может вызвать панику
let date = NaiveDate::from_ymd(2025, 2, 30); // Паника! 30 февраля не существует

// ПРАВИЛЬНО: используй from_ymd_opt
let date = NaiveDate::from_ymd_opt(2025, 2, 30);
match date {
    Some(d) => println!("Дата: {}", d),
    None => println!("Такой даты не существует!"),
}

3. Путаница между наивным и осознанным временем

// ОШИБКА: смешивание типов
let naive = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
let utc = Utc::now();
// let diff = utc.signed_duration_since(naive); // Ошибка типов!

// ПРАВИЛЬНО: конвертируй в один тип
let naive_dt = naive.and_hms_opt(0, 0, 0).unwrap();
let utc_dt = utc.date_naive();
let diff = utc_dt.signed_duration_since(naive);
println!("Разница: {} дней", diff.num_days());

4. Игнорирование часовых поясов при парсинге

// ОШИБКА: парсим строку с часовым поясом как наивную дату
let s = "2025-03-15T14:30:00+03:00";
// let dt = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%:z"); // Ошибка!

// ПРАВИЛЬНО: используй DateTime::parse_from_rfc3339
let dt = DateTime::parse_from_rfc3339(s).unwrap();
println!("{}", dt);

5. Выход за границы при арифметике

// ОШИБКА: прибавление большого количества дней может выйти за пределы
let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap();
// let future = date + Duration::days(1_000_000); // Может переполниться

// ПРАВИЛЬНО: используй checked_add_signed
let future = date.checked_add_signed(Duration::days(1_000_000));
match future {
    Some(d) => println!("Будущее: {}", d),
    None => println!("Слишком далеко в будущее!"),
}

6. Неправильный формат строки

// ОШИБКА: формат не соответствует строке
let s = "15.03.2025";
let date = NaiveDate::parse_from_str(s, "%Y-%m-%d"); // Ошибка!

// ПРАВИЛЬНО: используй правильный формат
let date = NaiveDate::parse_from_str(s, "%d.%m.%Y").unwrap();
println!("{}", date);

Лучшие практики и советы профессионалов

1. Всегда используй UTC для хранения

Храни даты и время в UTC в базе данных. Конвертируй в локальный часовой пояс только при показе пользователю. Это избавит от головной боли при переходе на летнее/зимнее время и при работе с пользователями из разных стран.

2. Используй безопасные методы с Option

Всегда используй from_ymd_opt, checked_add_signed и другие методы, возвращающие Option. Обрабатывай случай None явно. Это защитит программу от паники при неверных данных.

3. Не изобретай велосипед

Используй готовые методы chrono для форматирования, парсинга и арифметики. Не пытайся вручную вычислять количество дней в месяце или учитывать високосные годы — библиотека сделает это за тебя.

4. Документируй часовые пояса

Если твоя функция принимает дату, всегда указывай в документации, в каком часовом поясе она ожидает данные. Это спасет других разработчиков (и тебя самого) от путаницы.

5. Тестируй граничные случаи

Обязательно тестируй:

  • Переход через високосный год (2024-02-28 + 1 день = 2024-02-29)
  • Переход на летнее время (в часовых поясах, где оно есть)
  • Крайние даты (год 1, год 9999)
  • Отрицательные разницы (когда начальная дата позже конечной)

6. Используй chrono::serde для сериализации

Если ты работаешь с JSON или другими форматами данных, подключи chrono/serde в Cargo.toml:

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

Это позволит автоматически сериализовать и десериализовать даты.

7. Для высокоточной работы используй std::time::Instant

Если тебе нужно замерить производительность кода (сколько времени выполняется функция), используй std::time::Instant, а не chrono. Instant быстрее и точнее для интервалов.

Вопросы для самопроверки

  1. Вопрос: Какой тип в chrono используется для хранения даты и времени без часового пояса?

    Ответ: