c++c++20c++-chrono

How do I create a Julian calendar that will interoperate with chrono?


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?


Solution

  • 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:

    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.