.. Copyright (c) 2026 Tobias Erbsland - Erbsland DEV. https://erbsland.dev SPDX-License-Identifier: Apache-2.0 .. index:: single: Dates; Working with Dates single: Time; Calendar Dates single: Date; Validation and Arithmetic ****************** Working with Dates ****************** A calendar date identifies a day without choosing a clock time or a time zone. :cpp:class:`Date ` represents that value as a year, a month, and a day in the Gregorian calendar. It lets you compare dates and move through the calendar while accounting for different month lengths and leap years. This page explains how to construct and validate dates, access their calendar fields, and convert them for display or storage. You will also learn how date calculations handle month ends and the limits of the supported calendar. A Calendar Date on Its Own ========================== ``Date`` uses the proleptic Gregorian calendar: the familiar Gregorian leap-year rules apply throughout its supported range, including dates before the calendar was historically adopted. Years run from ``0`` through ``9999``, so the first date is ``0000-01-01`` and the last is ``9999-12-31``. There are no negative years, and the library does not model a country's historical change of calendar. A leap year has a February 29: years divisible by four are leap years, except century years unless they are also divisible by 400. For example, 2000 is a leap year and 2100 is not; year 0 follows the same rule. The Core epoch is ``0000-01-01``. A day count of zero therefore identifies that date, rather than the Unix epoch of ``1970-01-01``. The last date has day count ``3,652,424``. Keeping this epoch in mind matters when you exchange numeric dates with another library or file format. A date does not identify an instant. It identifies a calendar day without specifying a time within that day or the zone in which it is interpreted. When you need a clock reading as well, combine it with :doc:`working_with_times` or choose :cpp:class:`DateTime ` for a dated instant with zone information. Constructing a Date =================== Named Parts Make Construction Readable -------------------------------------- The constructor takes :cpp:class:`Year `, :cpp:class:`Month `, and :cpp:class:`Day ` values. Because the fields have different types, you can write them in year-month-day, day-month-year, or year-day-month order without confusing a month with a day. Ordinary copying and moving preserve the same date; default construction produces an **invalid date**. That default is useful when a field has not yet been supplied, but it does not capture today's date. For the date of a current instant, obtain a ``DateTime`` and select its UTC or displayed date according to your task. :cpp:func:`fromParts() ` is convenient when only part of the date is specified. Its month defaults to January and its day to the first, so supplying just a year selects January 1. The :cpp:func:`fromPartsOrThrow() ` variant rejects an impossible combination with :cpp:class:`OutOfRangeError `. The distinction between a part's range and a complete date is worth understanding before using these constructors. A ``Month`` clamps its input to ``1..12``; a ``Day`` clamps to ``1..31``; a ``Year`` clamps to ``0..9999``. After that, ``Date`` checks whether the resulting day exists in the chosen month and year. Thus month 13 becomes December before construction, while February 31 produces an invalid date. .. erbsland-demo:: :source: time/Date/CreateTyped.cpp :exec: time/date --demo CreateTyped :sha256: 20d6897ddd209fd0e54ba5c16546c8828df9c7c6ea4d60fff51e94cce0a35a0c .. code-block:: cpp /// Build observation dates from named calendar parts. /// @notest{Compiled and executed documentation demo.} void createTyped() { const auto observation = "Observation de la Lune"_el; const auto year = el::Year{2028}; const auto month = el::Month::february(); const auto day = el::Day{29}; // Named parts make all three constructor orders unambiguous. const auto date = el::Date{year, month, day}; const auto dayFirst = el::Date{day, month, year}; const auto yearDayMonth = el::Date{year, day, month}; const auto startOfYear = el::Date::fromParts(year); const auto startOfMonth = el::Date::fromParts(year, month); const auto checked = el::Date::fromPartsOrThrow(year, month, day); el::io::printLine(el::StringFormat{"{}: {}"_el}.build(observation, date)); el::io::printLine( el::StringFormat{"Constructor orders agree: {}"_el}.build(date == dayFirst && date == yearDayMonth)); el::io::printLine( el::StringFormat{"Year start: {}; month start: {}; checked: {}"_el}.build(startOfYear, startOfMonth, checked)); // Typed parts clamp their input before the date checks the combination. const auto clampedMonth = el::Date{year, el::Month{13}, el::Day{1}}; const auto impossible = el::Date{year, month, el::Day{31}}; el::io::printLine( el::StringFormat{"Clamped month: {}; February 31 valid: {}"_el}.build(clampedMonth, impossible.isValid())); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mObservation de la Lune: 2028-02-29 Constructor orders agree: true Year start: 2028-01-01; month start: 2028-02-01; checked: 2028-02-29 Clamped month: 2028-12-01; February 31 valid: false␛[0m .. erbsland-demo-end:: Checking Supplied Fields ------------------------ When your input consists of raw integers, you usually want to reject an out-of-range field instead of silently clamping it. :cpp:func:`fromYearMonthDay() ` checks the raw values and the calendar combination together, returning an invalid date if either check fails. :cpp:func:`fromYearMonthDayOrThrow() ` performs the same validation and throws ``OutOfRangeError`` on failure. Choose the returning form when a missing or rejected value is part of ordinary input handling, and the throwing form when your caller needs a clear failure path. The typed ``fromPartsOrThrow()`` factory checks the combination of already constructed parts. It cannot recover a raw value that a part has already clamped. For example, passing ``Month{13}`` to this factory still passes December. :cpp:func:`fromDaysSinceEpoch() ` restores a date from a Core day count. It accepts ``0..3,652,424``; any count outside that range produces an invalid date, rather than a saturated one. For calendar boundaries, :cpp:func:`firstDay() ` and :cpp:func:`lastDay() ` accept a year, or a year and month. They save you from calculating the final day of February yourself. :cpp:func:`epoch() ` and :cpp:func:`first() ` both return ``0000-01-01``; :cpp:func:`last() ` returns ``9999-12-31``. These are valid dates, distinct from a default-constructed invalid value. .. erbsland-demo:: :source: time/Date/CreateChecked.cpp :exec: time/date --demo CreateChecked :sha256: 97b6105141ea7a8082eda63bfaa361a89180cc68ceeaf837082da225ac4d0959 .. code-block:: cpp /// Validate supplied dates and choose calendar boundaries. /// @notest{Compiled and executed documentation demo.} void createChecked() { // Validate raw fields before they become clamped calendar parts. const auto supplied = el::Date::fromYearMonthDay(2028, 2, 29); const auto rejected = el::Date::fromYearMonthDay(2028, 13, 1); el::io::printLine(el::StringFormat{"Supplied date: {}; month 13 valid: {}"_el}.build(supplied, rejected.isValid())); try { const auto invalid = el::Date::fromYearMonthDayOrThrow(2027, 2, 29); el::io::printLine(el::StringFormat{"Accepted: {}"_el}.build(invalid)); } catch (const el::OutOfRangeError &) { el::io::printLine("Rejected February 29 in a common year."_el); } try { const auto invalid = el::Date::fromPartsOrThrow(el::Year{2028}, el::Month::february(), el::Day{31}); el::io::printLine(el::StringFormat{"Accepted: {}"_el}.build(invalid)); } catch (const el::OutOfRangeError &) { el::io::printLine("Rejected February 31 from typed parts."_el); } const auto restored = el::Date::fromDaysSinceEpoch(supplied.toDaysSinceEpoch()); const auto beforeEpoch = el::Date::fromDaysSinceEpoch(el::Days{-1}); el::io::printLine( el::StringFormat{"Restored: {}; negative day count valid: {}"_el}.build(restored, beforeEpoch.isValid())); el::io::printLine( el::StringFormat{"Epoch: {}; first: {}; last: {}"_el}.build( el::Date::epoch(), el::Date::first(), el::Date::last())); const auto year = el::Year{2028}; const auto month = el::Month::february(); el::io::printLine( el::StringFormat{"Year boundaries: {} to {}"_el}.build(el::Date::firstDay(year), el::Date::lastDay(year))); el::io::printLine( el::StringFormat{"Month boundaries: {} to {}"_el}.build( el::Date::firstDay(year, month), el::Date::lastDay(year, month))); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mSupplied date: 2028-02-29; month 13 valid: false Rejected February 29 in a common year. Rejected February 31 from typed parts. Restored: 2028-02-29; negative day count valid: false Epoch: 0000-01-01; first: 0000-01-01; last: 9999-12-31 Year boundaries: 2028-01-01 to 2028-12-31 Month boundaries: 2028-02-01 to 2028-02-29␛[0m .. erbsland-demo-end:: Checking and Inspecting a Date ============================== :cpp:func:`isValid() ` tells you whether a date represents an existing calendar day. For example, February 29 is valid in 2028 and invalid in 2027. :cpp:func:`exists() ` checks a combination of typed parts directly when you do not need to retain the date. Dates support equality and chronological ordering, so you can sort dates or check whether a new date is later than an existing one. All invalid dates compare equal to one another and sort before every valid date, including the epoch. That ordering makes sorting well-defined, but it does not make an invalid value an earlier calendar day. Check validity before using a value in a calculation that requires an existing calendar day. :cpp:func:`isFirst() ` and :cpp:func:`isLast() ` identify the two supported boundaries. They let you detect the supported boundaries during navigation or after arithmetic that may have saturated. A boundary result alone does not prove saturation: a calculation can reach the first or last date exactly. The saturation query in the calculation section distinguishes these cases. Accessing Calendar Fields ------------------------- The :cpp:func:`year() `, :cpp:func:`month() `, and :cpp:func:`day() ` accessors return typed parts. :cpp:func:`dayOfYear() ` is one-based, ranging from 1 to 365 or 366. :cpp:func:`dayOfWeek() ` returns a :cpp:class:`DayOfWeek ` with Monday numbered zero and Sunday numbered six. Its named values and text output are often clearer than passing the numeric weekday around. When you need year, month, and day together, :cpp:func:`parts() ` returns a :cpp:struct:`DateParts ` with named fields. One extraction avoids repeating the calendar conversion performed by the separate date-field accessors. The parts also make reconstruction straightforward: pass ``parts.year``, ``parts.month``, and ``parts.day`` to the constructor. .. erbsland-demo:: :source: time/Date/Inspect.cpp :exec: time/date --demo Inspect :sha256: e129edb0f8c0d6b0d4472f3ca78f7899bb0c7713116faace184498067fdf571d .. code-block:: cpp /// Compare dates and extract fields for a calendar view. /// @notest{Compiled and executed documentation demo.} void inspect() { const auto date = el::Date::fromYearMonthDayOrThrow(2028, 2, 29); const auto invalid = el::Date{}; el::io::printLine( el::StringFormat{"Valid: {}; invalid sorts first: {}; invalid dates equal: {}"_el}.build( date.isValid(), invalid < el::Date::first(), invalid == el::Date{})); el::io::printLine(el::StringFormat{"Next observation is later: {}"_el}.build(date < date.next())); el::io::printLine( el::StringFormat{"At first boundary: {}; at last boundary: {}"_el}.build( el::Date::first().isFirst(), el::Date::last().isLast())); el::io::printLine( el::StringFormat{"Date exists: {}"_el}.build( el::Date::exists(el::Year{2028}, el::Month::february(), el::Day{29}))); // An invalid date's fallback fields cannot replace its validity check. const auto fallback = invalid.parts(); el::io::printLine( el::StringFormat{"Invalid fallback: {}-{}-{}; still invalid: {}"_el}.build( fallback.year.toValue(), fallback.month.toValue(), fallback.day.toValue(), !invalid.isValid())); // Extract related fields together when preparing a calendar view. const auto parts = date.parts(); el::io::printLine( el::StringFormat{"Year: {}; month: {}; day: {}"_el}.build( parts.year.toValue(), parts.month.toValue(), parts.day.toValue())); el::io::printLine( el::StringFormat{"Individual fields: {} / {} / {}"_el}.build( date.year().toValue(), date.month().toValue(), date.day().toValue())); el::io::printLine( el::StringFormat{"Day of year: {}; weekday: {} (index {})"_el}.build( date.dayOfYear().toValue(), date.dayOfWeek().toString(), date.dayOfWeek().toValue())); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mValid: true; invalid sorts first: true; invalid dates equal: true Next observation is later: true At first boundary: true; at last boundary: true Date exists: true Invalid fallback: 0-1-1; still invalid: true Year: 2028; month: 2; day: 29 Individual fields: 2028 / 2 / 29 Day of year: 60; weekday: Tuesday (index 1)␛[0m .. erbsland-demo-end:: For an invalid date, the accessors return fallback fields: year 0, January, day 1, day-of-year 1, and Saturday. ``parts()`` likewise returns the epoch's year, month, and day. Those fields are not evidence of validity, and reconstructing them would turn an invalid value into a valid epoch date. Keep the original validity check alongside any extraction from optional input. Converting a Date for Display or Storage ======================================== :cpp:func:`toString() ` and the default :cpp:func:`toIsoString() ` both produce ``YYYY-MM-DD``. The zero padding makes the result consistent for display and sorting as text. Both return an empty string for an invalid date. For numeric storage, :cpp:func:`toDaysSinceEpoch() ` returns a typed :cpp:type:`Days ` amount, using the Core epoch explained above. Invalid dates produce ``Days{-1}``. Retain the typed value while working inside the library, and extract its integer only when an external format requires one. You can ask ``toIsoString()`` for a compact date without separators or reduce its :cpp:enum:`DateTimePrecision ` to a year or month. Reduced output is a presentation choice: ``2028-02`` no longer communicates a particular day even though the original object still stores one. See :ref:`time-iso-output` for the formatting types and the shared ISO output controls. .. erbsland-demo:: :source: time/Date/Convert.cpp :exec: time/date --demo Convert :sha256: 7727deb63864fda174ac9eca5982b0677b2b8db4e9df12493059b8889d44422a .. code-block:: cpp /// Convert observation dates to text and epoch day counts. /// @notest{Compiled and executed documentation demo.} void convert() { const auto date = el::Date::fromYearMonthDayOrThrow(2028, 2, 29); el::io::printLine(el::StringFormat{"Display: {}; default ISO: {}"_el}.build(date.toString(), date.toIsoString())); el::io::printLine(el::StringFormat{"Days since Core epoch: {}"_el}.build(date.toDaysSinceEpoch().toRawValue())); el::io::printLine(el::StringFormat{"Compact ISO: {}"_el}.build(date.toIsoString(el::IsoTimeFormatFlags{}))); el::io::printLine( el::StringFormat{"Month precision: {}"_el}.build( date.toIsoString(el::cDefaultDateFormat, el::DateTimePrecision::Month))); const auto invalid = el::Date{}; el::io::printLine( el::StringFormat{"Invalid display: '{}'; invalid day count: {}"_el}.build( invalid.toString(), invalid.toDaysSinceEpoch().toRawValue())); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mDisplay: 2028-02-29; default ISO: 2028-02-29 Days since Core epoch: 740771 Compact ISO: 20280229 Month precision: 2028-02 Invalid display: ''; invalid day count: -1␛[0m .. erbsland-demo-end:: Moving Through the Calendar =========================== You can move a date forward or backward by a number of days, months, or years. :cpp:func:`added() ` accepts ``Days``, :cpp:type:`Months `, or :cpp:type:`Years ` and returns a new date. :cpp:func:`add() ` changes the existing date instead. Negative amounts move backward, so subtraction uses the same methods with a negative amount. A day count moves by that many calendar days. Months and years move the calendar fields and then clamp a day that does not exist in the destination month. January 31, 2028 plus one month becomes February 29; February 29 plus one year becomes February 28, 2029. This is a successful calendar operation, including for :cpp:func:`addedOrThrow() ` and :cpp:func:`addOrThrow() `. The throwing forms reject leaving the supported range, rather than ordinary month-end adjustment. Because a shorter month can lose the original day number, adding a month and then subtracting a month need not restore the original date. In the calculation demo, adding one month to January 31 and then subtracting it returns January 29. For a recurrence that must always select the month's final day, calculate that day with ``lastDay()`` for each occurrence instead of carrying forward an already adjusted day. :cpp:func:`next() ` and :cpp:func:`previous() ` move one day when called without an argument. Their weekday overloads find the next or previous requested weekday, always excluding the starting date. If today is Friday, asking for the next Friday advances by seven days. :cpp:func:`daysTo() ` measures the signed calendar-day distance to another date. ``start.daysTo(finish)`` is positive when ``finish`` is later and negative when it is earlier. This is a date distance; measuring elapsed time between zoned instants is a separate task. .. erbsland-demo:: :source: time/Date/Calculate.cpp :exec: time/date --demo Calculate :sha256: ffaed126afb37cefefb1d407ae835472a8a1e6c3a44fa56323bf66059edfd363 .. code-block:: cpp /// Move observation dates by days, months, or years. /// @notest{Compiled and executed documentation demo.} void calculate() { const auto observation = el::Date::fromYearMonthDayOrThrow(2028, 1, 31); const auto nextMonth = observation.added(el::Months{1}); el::io::printLine( el::StringFormat{"One month later: {}; one month back: {}"_el}.build( nextMonth, nextMonth.added(el::Months{-1}))); el::io::printLine(el::StringFormat{"Next year from leap day: {}"_el}.build(nextMonth.addedOrThrow(el::Years{1}))); el::io::printLine(el::StringFormat{"Seven days later: {}"_el}.build(observation.addedOrThrow(el::Days{7}))); auto revised = observation; revised.add(el::Months{1}); revised.addOrThrow(el::Days{2}); revised.addOrThrow(el::Years{1}); el::io::printLine(el::StringFormat{"Revised date: {}"_el}.build(revised)); el::io::printLine( el::StringFormat{"Previous day: {}; next day: {}"_el}.build(observation.previous(), observation.next())); el::io::printLine( el::StringFormat{"Next Friday: {}; previous Friday: {}"_el}.build( observation.next(el::DayOfWeek::friday()), observation.previous(el::DayOfWeek::friday()))); el::io::printLine( el::StringFormat{"Days forward: {}; days backward: {}"_el}.build( observation.daysTo(nextMonth).toRawValue(), nextMonth.daysTo(observation).toRawValue())); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mOne month later: 2028-02-29; one month back: 2028-01-29 Next year from leap day: 2029-02-28 Seven days later: 2028-02-07 Revised date: 2029-03-02 Previous day: 2028-01-30; next day: 2028-02-01 Next Friday: 2028-02-04; previous Friday: 2028-01-28 Days forward: 29; days backward: -29␛[0m .. erbsland-demo-end:: Date Arithmetic Saturation and Overflow --------------------------------------- Ordinary ``added()`` and ``add()`` operations **saturate**: a result before ``0000-01-01`` becomes the first date, and a result after ``9999-12-31`` becomes the last date. This keeps a calendar value in range, but it can hide that a requested change was too large. :cpp:func:`wouldAddSaturate() ` checks the proposed day, month, or year change without modifying the date. It returns false if the change reaches a boundary exactly or only adjusts the day within a shorter month. The ``...OrThrow()`` variants throw :cpp:class:`OverflowError ` when the result would leave the range. An in-place throwing operation leaves the date unchanged on that failure. This lets your caller handle an unsupported date change as an error instead of accepting a boundary date. There is a deliberate difference between arithmetic and one-day navigation at the boundaries. ``Date::last().added(Days{1})`` stays at the last date, whereas ``Date::last().next()`` is invalid. Similarly, ``Date::first().previous()`` is invalid. That lets a day-by-day traversal detect when it has finished. The weekday overloads instead use saturating arithmetic; near a boundary, their result may therefore be a boundary date whose weekday is not the requested one. .. erbsland-demo:: :source: time/Date/Boundaries.cpp :exec: time/date --demo Boundaries :sha256: fe80e0aebf59d347ee154d494351efa2f64affd802efe3e2516f39c91a49c5bf .. code-block:: cpp /// Choose saturation or explicit failure at date boundaries. /// @notest{Compiled and executed documentation demo.} void boundaries() { auto end = el::Date::last(); el::io::printLine(el::StringFormat{"Adding a day would saturate: {}"_el}.build(end.wouldAddSaturate(el::Days{1}))); end.add(el::Days{1}); el::io::printLine(el::StringFormat{"Saturated result: {}"_el}.build(end)); try { end.addOrThrow(el::Months{1}); } catch (const el::OverflowError &) { el::io::printLine( el::StringFormat{"Rejected a month beyond the supported range; date remains {}."_el}.build(end)); } el::io::printLine(el::StringFormat{"First minus one year: {}"_el}.build(el::Date::first().added(el::Years{-1}))); el::io::printLine( el::StringFormat{"Next beyond last valid: {}; previous before first valid: {}"_el}.build( end.next().isValid(), el::Date::first().previous().isValid())); const auto invalid = el::Date{}; el::io::printLine( el::StringFormat{"Invalid remains invalid: {}; invalid saturation query: {}; invalid distance: {}"_el}.build( invalid.addedOrThrow(el::Days{1}).isValid(), invalid.wouldAddSaturate(el::Days{1}), invalid.daysTo(end).toRawValue())); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mAdding a day would saturate: true Saturated result: 9999-12-31 Rejected a month beyond the supported range; date remains 9999-12-31. First minus one year: 0000-01-01 Next beyond last valid: false; previous before first valid: false Invalid remains invalid: false; invalid saturation query: false; invalid distance: 0␛[0m .. erbsland-demo-end:: Invalid dates remain invalid through arithmetic and navigation, including the throwing arithmetic forms. ``wouldAddSaturate()`` returns false for an invalid date, and ``daysTo()`` returns zero if either date is invalid. A zero distance therefore needs a validity check before you interpret it as “the same day.” Related Clock, Interval, and Instant Types ========================================== :doc:`working_with_times` adds a clock reading and shows how to carry crossed days back into a date. The :doc:`overview` helps you choose the remaining types: ``Duration`` and ``TimeDelta`` for fixed intervals, :cpp:class:`CalendarDelta ` for combined calendar changes, and ``DateTime`` or :cpp:class:`Timestamp ` when an event must identify an instant. For the exact signatures and individual contracts, see :doc:`../../reference/time/date_and_time`.