I would like to convert back and forth between the Julian calendar and
<chrono>
's civil calendar. What is the best way to do that?
Creating a user-written calendar to interoperate with the civil calendar in
<chrono>
can be relatively easy.
At its core, the user-written calendar needs:
sys_days
.time_zone
, explicit conversions to and from
local_days
.Anything else you build into your user-written calendar is gravy. Obvious additions might include:
To keep things short and simple, I'll skip almost all of the niceties in the
demo of a Julian calendar below. I'll show only what is necessary to interoperate
with <chrono>
and then demonstrate that interoperability by showing how to use Julian calendar to compute the date of orthodox Easter (orthodox Easter is usually not the same day as Easter).
namespace julian
{
class year_month_day
{
std::int16_t y_;
std::uint8_t m_;
std::uint8_t d_;
public:
constexpr year_month_day(int y, int m, int d) noexcept;
constexpr year_month_day(std::chrono::sys_days dp) noexcept;
constexpr explicit year_month_day(std::chrono::local_days dp) noexcept;
constexpr operator std::chrono::sys_days() const noexcept;
constexpr explicit operator std::chrono::local_days() const noexcept;
constexpr int year() const noexcept;
constexpr int month() const noexcept;
constexpr int day() const noexcept;
};
} // namespace julian
The above is analogous to std::chrono::year_month_day
but I've placed it into
namespace julian
to differentiate it. The core functionality for <chrono>
interoperability is these four functions:
constexpr year_month_day(std::chrono::sys_days dp) noexcept;
constexpr explicit year_month_day(std::chrono::local_days dp) noexcept;
constexpr operator std::chrono::sys_days() const noexcept;
constexpr explicit operator std::chrono::local_days() const noexcept;
The implementation of everything else (such as the y, m, d constructor) is obvious.
There are two algorithms necessary to implement these 4 conversions. They are
derived in detail in
chrono-Compatible Low-Level Date Algorithms.
The linked document refers to them with the names days_from_julian
and
julian_from_days
. Here, they are renamed to operator std::chrono::sys_days()
and year_month_day(std::chrono::sys_days dp)
respectively.
constexpr
inline
year_month_day::year_month_day(std::chrono::sys_days dp) noexcept
{
auto const z = dp.time_since_epoch().count() + 719470;
auto const era = (z >= 0 ? z : z - 1460) / 1461;
auto const doe = static_cast<unsigned>(z - era * 1461); // [0, 1460]
auto const yoe = (doe - doe/1460) / 365; // [0, 3]
auto const y = static_cast<int>(yoe) + era * 4;
auto const doy = doe - 365*yoe; // [0, 365]
auto const mp = (5*doy + 2)/153; // [0, 11]
d_ = static_cast<std::uint8_t>(doy - (153*mp+2)/5 + 1); // [1, 31]
m_ = static_cast<std::uint8_t>(mp < 10 ? mp+3 : mp-9); // [1, 12]
y_ = static_cast<std::int16_t>(y + (m_ <= 2));
}
constexpr
inline
year_month_day::operator std::chrono::sys_days() const noexcept
{
int const y = y_ - (m_ <= 2);
unsigned const m = m_;
unsigned const d = d_;
auto const era = (y >= 0 ? y : y-3) / 4;
auto const yoe = static_cast<unsigned>(y - era * 4); // [0, 3]
auto const doy = (153*(m > 2 ? m-3 : m+9) + 2)/5 + d-1; // [0, 365]
auto const doe = yoe * 365 + doy; // [0, 1460]
using namespace std::chrono;
return sys_days{days{era * 1461 + static_cast<int>(doe) - 719470}};
}
See chrono-Compatible Low-Level Date Algorithms for a detailed explanation of all of the "magic numbers" in these algorithms.
Note the use of the using-declaration in operator std::chrono::sys_days()
is placed
such that its scope is minimized as much as possible: to just the return statement.
The local_days
conversions use the exact same math as the sys_days
conversions, and can thus be implemented in terms of the sys_days
conversions:
constexpr
inline
year_month_day::year_month_day(std::chrono::local_days dp) noexcept
: year_month_day{std::chrono::sys_days{dp.time_since_epoch()}}
{
}
constexpr
inline
year_month_day::operator std::chrono::local_days() const noexcept
{
using namespace std::chrono;
return local_days{sys_days{*this}.time_since_epoch()};
}
And that's it! With just this much you can write conversions to and from both
std::chrono::year_month_day
and std::chrono::year_month_weekday
:
using namespace std::literals;
auto civil_date = 2025y/10/30;
julian::year_month_day julian_date{civil_date};
std::cout << "civil date : " << civil_date << '\n'; // 2025-10-30
std::cout << "julian date : " << julian_date.year() << '-'
<< julian_date.month() << '-'
<< julian_date.day() << '\n'; // 2025-10-17
assert(civil_date == std::chrono::year_month_day{julian_date});
The explicit conversions between std::chrono::year_month_day
and
julian::year_month_day
implicitly bounce off of sys_days
under the hood.
One can now build on this tool to easily compute the civil date for orthodox Easter.
namespace julian
{
// Precondition: y >= 0y
std::chrono::sys_days
paschal_full_moon(std::chrono::year y)
{
int yr{y};
auto shifted_epact = (14 + 11*(yr % 19)) % 30;
auto jy = yr - (yr == 0);
return std::chrono::sys_days{julian::year_month_day{jy, 4, 19}} -
std::chrono::days{shifted_epact};
}
} // namespace julian
The above formula for the
paschal full moon
is taken from
"Calendrical Calculations, 3rd edition"
by Dershowitz and Reingold. I have put it inside namespace julian
to distinguish it
from the Gregorian
paschal full moon
shown here.
Note that not only have I not used a using-declaration, I redundantly scoped
year_month_day
with julian::
to emphasize which parts are julian
and which
parts are chrono
. Otherwise it is very easy to get confused, and even to
introduce ambiguities.
Also note that the return type is sys_days
. You can think of sys_days
as an
agnostic calendar: neither civil nor Julian. Yes, if you parse or format
sys_days
, you'll get the civil textual representation. But sys_days
is just
a simple count of days under the hood, and can thus be considered the
canonical calendar.
Orthodox Easter is now easily written as the Sunday after the Julian paschal full moon:
// Precondition: y >= 0y
std::chrono::sys_days
orthodox_easter(std::chrono::year y)
{
using namespace std::chrono;
auto x = sys_days{julian::paschal_full_moon(y)} + days{1};
return x + (Sunday - weekday{x});
}
The return has type sys_days
(the canonical calendar) and can be implicitly converted
to either the civil or Julian calendar (or any other calendar).
I should also note that this works because the cycle of the 7 weekdays is the same for
both the civil and Julian calendars. Indeed, one can think of weekday
as yet another
calendar, independent of both the civil and Julian calendars. And the cycle of the seven days has remained unbroken since prior to the inception of the Julian calendar. The cycle remained unbroken even during the transition from the Julian to the civil calendar.
If this were not true, it could still work but would probably need both std::chrono::weekday
and julian::weekday
. But as it is, these are the same type because both have the exact same meaning.