библиотека — дата и времяlibrary — date and time

календарь и часовые пояса в chronocalendar and time zones in chrono

Календарные типы (year, month, day, year_month_day) и часовые пояса (time_zone, zoned_time) поверх базового <chrono> из C++11 — 2024y/January/15d вместо ручной арифметики с epoch-секундами.Calendar types (year, month, day, year_month_day) and time zones (time_zone, zoned_time) built on top of C++11's base <chrono> — 2024y/January/15d instead of hand-rolled arithmetic on epoch seconds.

часто используетсяcommonly used зависит от системной tzdatadepends on the system tzdata частый вопрос: что нового поверх duration/time_pointcommon question: what's new on top of duration/time_point

Что этоWhat it is

Базовый <chrono> из C++11 (см. cpp11/chrono.html) даёт duration и time_point — промежутки и моменты времени относительно системных часов, но без понятия "год/месяц/день" или "часовой пояс". C++20 добавляет поверх этого фундамента календарную семантику: std::chrono::year, std::chrono::month, std::chrono::day — типобезопасные обёртки над числом (а не голый int), их комбинация std::chrono::year_month_day — представление календарной даты, а также std::chrono::time_zone/std::chrono::zoned_time — конвертация UTC в локальное время конкретного часового пояса и обратно, с учётом перехода на летнее время.The base <chrono> from C++11 (see cpp11/chrono.html) gives you duration and time_point — spans and moments of time relative to system clocks, but with no notion of "year/month/day" or "time zone." C++20 adds calendar semantics on top of that foundation: std::chrono::year, std::chrono::month, std::chrono::day — type-safe wrappers over a number (not a bare int) — combined into std::chrono::year_month_day, a calendar date, plus std::chrono::time_zone/std::chrono::zoned_time — converting UTC to a specific time zone's local time and back, accounting for daylight saving transitions.

до C++20 — C-API или голые числаbefore C++20 — the C API or raw numbers
struct tm t{};
t.tm_year = 2024 - 1900;   // год отсчитывается от 1900, не типобезопасно// year offset from 1900, not type-safe
t.tm_mon  = 0;          // январь — это 0, а не 1// January is 0, not 1
t.tm_mday = 15;
setenv("TZ", "Europe/Moscow", 1);  // глобальное состояние, гонка в многопоточном коде// global state, a race in multithreaded code
mktime(&t);
C++20 — типобезопасный календарьC++20 — a type-safe calendar
using namespace std::chrono;

auto date = 2024y/January/15d;   // year_month_day, читаемо и типобезопасно// year_month_day, readable and type-safe

auto tz = locate_zone("Europe/Moscow");
zoned_time zt{tz, system_clock::now()};  // без глобального состояния, без гонок// no global state, no races

Зачем ввелиWhy it was introduced

До C++20 работа с календарными датами и часовыми поясами в стандартной библиотеке C++ отсутствовала вовсе — <chrono> из C++11 сознательно ограничивался длительностями и точками времени относительно системных часов:Before C++20, calendar dates and time zones simply didn't exist in the C++ standard library — C++11's <chrono> deliberately limited itself to durations and time points relative to system clocks:

01

C-API для дат — не типобезопасен и опирается на глобальное состояниеThe C API for dates isn't type-safe and relies on global state

localtime, strftime, mktime из <ctime> работают с struct tm, где поля — голые int со странными соглашениями (год от 1900, месяц от 0), а выбор часового пояса — через глобальную переменную окружения setenv("TZ", ...), разделяемую всем процессом. В многопоточном коде смена часового пояса для одного потока непредсказуемо влияет на все остальные — источник трудноуловимых багов.localtime, strftime, mktime from <ctime> work with struct tm, whose fields are bare ints with odd conventions (year offset from 1900, month starting at 0), and the time zone is selected through a global environment variable, setenv("TZ", ...), shared by the whole process. In multithreaded code, changing the time zone for one thread unpredictably affects every other one — a source of hard-to-track bugs.

02

Сторонние библиотеки закрывали пробел годамиThird-party libraries filled the gap for years

В отсутствие стандартного решения проекты годами использовали Howard Hinnant's date library — реализацию календарных типов и часовых поясов поверх <chrono>, задолго до стандартизации ставшую де-факто стандартом в сообществе. Именно она стала прототипом при принятии C++20 — та же история, что и у {fmt} (→ std::format) и boost::filesystem (→ std::filesystem): библиотека сначала доказывает конструкцию в проде, потом входит в стандарт почти без изменений API.Absent a standard solution, projects relied for years on Howard Hinnant's date library — an implementation of calendar types and time zones on top of <chrono> that became a de facto community standard long before standardization. It became the direct prototype adopted into C++20 — the same story as {fmt} (→ std::format) and boost::filesystem (→ std::filesystem): a library proves the design in production first, then enters the standard with almost no API changes.

03

Год/месяц/день как типы, а не числа — компилятор ловит перепутанный порядокYear/month/day as types, not numbers — the compiler catches a swapped order

Отдельные типы year, month, day вместо трёх int устраняют классическую путаницу американского (месяц/день/год) и европейского (день/месяц/год) порядка при вызове конструктора — компилятор не даст перепутать местами год и день, если это разные типы, а не позиционные числа.Separate year, month, day types instead of three ints eliminate the classic confusion between the American (month/day/year) and European (day/month/year) argument order — the compiler won't let you swap year and day if they're distinct types rather than positional numbers.

Как это работает под капотомHow it works under the hood

year, month, day — лёгкие обёртки над int/unsigned с литералами-суффиксами (y для года, d для дня; месяцы — именованные constexpr-объекты вроде January). Оператор / перегружен так, чтобы комбинация year/month/day в любом разумном порядке (2024y/January/15d или 15d/January/2024y) собирала year_month_day — это не арифметика в обычном смысле, а декларативная сборка даты. year_month_day внутри хранит компоненты по отдельности, но конвертируется в sys_days (специализацию time_point с точностью до дня) для арифметики вроде "прибавить 10 дней".year, month, day are lightweight wrappers over int/unsigned with literal suffixes (y for a year, d for a day; months are named constexpr objects like January). operator/ is overloaded so that combining year/month/day in any sensible order (2024y/January/15d or 15d/January/2024y) assembles a year_month_day — this isn't arithmetic in the usual sense, it's declarative date assembly. year_month_day stores its components separately internally, but converts to sys_days (a time_point specialization with day precision) for arithmetic like "add 10 days."

#include <chrono>
#include <iostream>

using namespace std::chrono;

auto date = 2024y/January/15d;      // year_month_day// year_month_day
if (date.ok()) {                    // проверка валидности — обязательна// validity check — mandatory
    sys_days sd = date;              // перевод в time_point для арифметики// convert to a time_point for arithmetic
    auto nextWeek = sd + days{7};
    year_month_day back = nextWeek;  // обратно в календарное представление// back to calendar representation
}

// часовые пояса: конвертация UTC -> локальное время без глобального TZ// time zones: UTC -> local time conversion, no global TZ
const time_zone* tz = locate_zone("Europe/Moscow");
zoned_time zt{tz, system_clock::now()};
std::cout << zt.get_local_time() << "\n";  // с учётом летнего времени автоматически// DST accounted for automatically

Арифметика напрямую с year_month_day тоже возможна для месяцев и лет — компоненты year/month поддерживают +/- с years{}/months{}, автоматически справляясь с переносом через границу года. Но такая арифметика — не то же самое, что арифметика в днях: "прибавить месяц" к 31 января не значит "прибавить 30-31 день", а значит "тот же день следующего месяца" — что не всегда существует (см. грабли ниже), поэтому .ok() после такой операции обязателен.Arithmetic directly on year_month_day is also possible for months and years — the year/month components support +/- with years{}/months{}, automatically handling the year-boundary carry. But this kind of arithmetic isn't the same as day-based arithmetic: "add a month" to January 31 doesn't mean "add 30-31 days," it means "the same day next month" — which doesn't always exist (see the gotcha below), so calling .ok() afterward is mandatory.

using namespace std::chrono;

auto jan31 = 2024y/January/31d;
auto nextMonth = jan31.year()/(jan31.month() + months{1})/jan31.day();
// nextMonth — это 2024-02-31, которого не существует// nextMonth is 2024-02-31, which doesn't exist
if (!nextMonth.ok()) {
    // типичная обработка — "прижать" день к последнему валидному дню месяца// the typical fix — clamp the day to the month's last valid day
    nextMonth = nextMonth.year()/nextMonth.month()/last;
    // year_month_day_last -> year_month_day: 2024-02-29 (високосный год)// year_month_day_last -> year_month_day: 2024-02-29 (a leap year)
}

auto plusYear = (jan31.year() + years{1})/jan31.month()/jan31.day();
// 2025-01-31 — перенос через границу года работает без ручной проверки// 2025-01-31 — carrying across the year boundary needs no manual check

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Вычисление возраста по дате рожденияComputing age from a birth date

Разница в годах между двумя датами, с поправкой на то, наступил ли уже день рождения в текущем году — простое вычитание годов даёт неверный результат, если день рождения ещё не наступил.The difference in years between two dates, adjusted for whether the birthday has already occurred this year — a plain subtraction of years gives the wrong result if the birthday hasn't happened yet.

using namespace std::chrono;

int age_years(year_month_day birth, year_month_day today) {
    int age = int(today.year()) - int(birth.year());
    if (today.month() < birth.month() ||
        (today.month() == birth.month() && today.day() < birth.day())) {
        --age;   // день рождения в этом году ещё не наступил// the birthday hasn't happened yet this year
    }
    return age;
}

auto birth = 1990y/June/20d;
auto today = 2026y/July/27d;
age_years(birth, today);   // 36 — день рождения в этом году уже прошёл// 36 — the birthday already passed this year
02

Форматирование даты для показа пользователюFormatting a date for display to a user

std::format поддерживает chrono-типы напрямую через спецификаторы формата, знакомые по strftime — без ручной сборки строки из отдельных полей year_month_day.std::format supports chrono types directly through format specifiers familiar from strftime — no manual string assembly from year_month_day's individual fields.

using namespace std::chrono;

auto date = 2024y/January/15d;
std::string s1 = std::format("{}", date);              // "2024-01-15" — формат по умолчанию// "2024-01-15" — the default format
std::string s2 = std::format("{:%d.%m.%Y}", date);      // "15.01.2024" — свой шаблон, как у strftime// "15.01.2024" — a custom pattern, strftime-style
03

Который час в другом часовом поясеWhat time is it in another time zone

Конвертация текущего момента времени между двумя часовыми поясами — например, "если в Лондоне сейчас столько-то, сколько сейчас в Токио" — через общий системный момент времени, без ручной арифметики со смещениями.Converting the current moment in time between two time zones — for instance, "if it's this time in London, what time is it in Tokyo" — via a shared system time point, with no manual offset arithmetic.

using namespace std::chrono;

const time_zone* london = locate_zone("Europe/London");
const time_zone* tokyo  = locate_zone("Asia/Tokyo");

zoned_time londonNow{london, system_clock::now()};
zoned_time tokyoNow{tokyo, londonNow.get_sys_time()};  // тот же момент, другой пояс// the same instant, a different zone

std::format("London: {}, Tokyo: {}", londonNow, tokyoNow);
04

Типичная ошибка — невалидная дата без проверки .ok()A common mistake — an invalid date with no .ok() check

Создание даты из пользовательского ввода (например, из формы с тремя отдельными полями год/месяц/день) без проверки, что комбинация вообще существует в календаре.Building a date from user input (say, a form with three separate year/month/day fields) without checking that the combination even exists on the calendar.

using namespace std::chrono;

auto bogus = 2024y/February/31d;   // 31 февраля — компилируется, но невалидно// February 31 — compiles, but is invalid
if (!bogus.ok()) {
    // обязательная проверка перед использованием — иначе тихо неверный результат// a mandatory check before use — otherwise a silently wrong result
    throw std::invalid_argument("invalid calendar date");
}

ГраблиGotchas

Часовые пояса зависят от системной базы IANA tzdataTime zones depend on the system's IANA tzdata database

time_zone/zoned_time не содержат правила часовых поясов внутри самой стандартной библиотеки — они читают базу данных IANA tzdata, установленную в системе (или предоставленную библиотекой отдельно, если реализация идёт таким путём). Код с zoned_time может вести себя по-разному на системах с устаревшей tzdata — например, если страна недавно изменила правила перехода на летнее время, а база на конкретной машине ещё не обновлена.time_zone/zoned_time don't carry time-zone rules inside the standard library itself — they read the IANA tzdata database installed on the system (or bundled separately by the implementation, if it takes that route). Code using zoned_time can behave differently on systems with stale tzdata — for instance, if a country recently changed its DST transition rules and the database on a given machine hasn't caught up yet.

Не все комбинации год/месяц/день валидны — нужно проверять .ok()Not every year/month/day combination is valid — check .ok()

Ничто не мешает написать 2024y/February/30d — 30 февраля не существует, но конструктор year_month_day не бросает исключение по умолчанию, а создаёт объект в невалидном состоянии. Метод .ok() нужно вызывать явно перед использованием даты в арифметике или конвертации — пропуск этой проверки не UB, но приводит к тихо неверным результатам далее по цепочке вычислений.Nothing stops you from writing 2024y/February/30d — February 30 doesn't exist, but the year_month_day constructor doesn't throw by default; it builds an object in an invalid state. .ok() has to be called explicitly before using the date in arithmetic or conversion — skipping this check isn't UB, but it leads to silently wrong results further down the calculation chain.

Поддержка часовых поясов отставала от остального C++20 дольше обычногоTime-zone support lagged behind the rest of C++20 longer than usual

Календарные типы (year_month_day и родственные) появились в стандартных библиотеках относительно быстро, но конкретно часть с часовыми поясами (time_zone, zoned_time, привязка к tzdata) была неполной в некоторых реализациях существенно дольше остального стандарта — проверять поддержку в конкретном тулчейне перед тем, как полагаться на неё, стоит отдельно от проверки базовых календарных типов.The calendar types (year_month_day and friends) landed in standard libraries relatively quickly, but the time-zone piece specifically (time_zone, zoned_time, the tzdata binding) remained incomplete in some implementations noticeably longer than the rest of the standard — checking support in your specific toolchain is worth doing separately from checking the base calendar types.