Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request - Julian Date Conversion (interesting) #849

Open
drankinatty opened this issue Oct 14, 2024 · 5 comments
Open

Feature Request - Julian Date Conversion (interesting) #849

drankinatty opened this issue Oct 14, 2024 · 5 comments

Comments

@drankinatty
Copy link

Howard, I recently ran across the need to convert from Gregorian Calendar to Julian Date and back working with various orbital data and ephemerides from JPL1. I checked the std::chrono library, but didn't find anything Julian Date related. This is really something that would make a good addition to the library, although it may not be the area that gets the most use. It's critical that it be something contained in a language library as the nuances are significant and that is what makes it something that shouldn't be reinvented by the user each time the conversion is needed, there is just far too much room for error.

The conversions are straight-forward, but the nuances are in providing an implementation that allows the user to choose which adoption of the Gregorian calendar to use when converting from Julian Date, e.g. the Catholic Church adoption in 1582 resulting in a 10-day reset for accumulated leap seconds, or say the adoption by Great Britain and the Colonies in 1752 resulting in an 11-day reset. The implementation providing a user-choice would be similar to what is done choosing the pseudo rando number generation engine, e.g. Mersenne twister, etc..

I don't know if you've considered it and decided against it or not, so I thought I'd suggest it as a feature. A few links that provide a good overview of the conversion are:

Not a giant high-priority issue, but something that would make a nice and much appreciated addition. (if you already have it in the library somewhere and I missed it, where is it squirreled-away?)

footnotes:

  1. JPL Planetary and Lunar Ephemerides
@HowardHinnant
Copy link
Owner

I should probably add this to the Examples and Recipes section. There is a SO answer on this that has evolved over the years. The C++20 version is really quite nice. And if you have C++20 that should be the preferred version. To make it work this this library (pre-C++20) one just needs to throw in date:: in a few places.

Here is the C++20 code from the SO answer answer in full:

#include <chrono>

struct jdate_clock;

template <class Duration>
    using jdate_time = std::chrono::time_point<jdate_clock, Duration>;

struct jdate_clock
{
    using rep        = double;
    using period     = std::chrono::days::period;
    using duration   = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<jdate_clock>;

    static constexpr bool is_steady = false;

    static time_point now() noexcept;

    template <class Duration>
    static
    auto
    from_sys(std::chrono::sys_time<Duration> const& tp) noexcept;

    template <class Duration>
    static
    auto
    to_sys(jdate_time<Duration> const& tp) noexcept;
};

template <class Duration>
auto
jdate_clock::from_sys(std::chrono::sys_time<Duration> const& tp) noexcept
{
    using namespace std;
    using namespace chrono;
    auto constexpr epoch = sys_days{November/24/-4713} + 12h;
    using ddays = std::chrono::duration<long double, days::period>;
    if constexpr (sys_time<ddays>{sys_time<Duration>::min()} < sys_time<ddays>{epoch})
    {
        return jdate_time{tp - epoch};
    }
    else
    {
        // Duration overflows at the epoch.  Sub in new Duration that won't overflow.
        using D = std::chrono::duration<int64_t, ratio<1, 10'000'000>>;
        return jdate_time{round<D>(tp) - epoch};
    }
}

template <class Duration>
auto
jdate_clock::to_sys(jdate_time<Duration> const& tp) noexcept
{
    using namespace std::chrono;
    return sys_time{tp - clock_cast<jdate_clock>(sys_days{})};
}

jdate_clock::time_point
jdate_clock::now() noexcept
{
    using namespace std::chrono;
    return clock_cast<jdate_clock>(system_clock::now());
}

Here is how I had to tweak it to use this library under C++17:

#include "date/tz.h"
#include <chrono>

struct jdate_clock;

template <class Duration>
    using jdate_time = std::chrono::time_point<jdate_clock, Duration>;

struct jdate_clock
{
    using rep        = double;
    using period     = date::days::period;
    using duration   = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<jdate_clock>;

    static constexpr bool is_steady = false;

    static time_point now() noexcept;

    template <class Duration>
    static
    auto
    from_sys(date::sys_time<Duration> const& tp) noexcept;

    template <class Duration>
    static
    auto
    to_sys(jdate_time<Duration> const& tp) noexcept;
};

template <class Duration>
auto
jdate_clock::from_sys(date::sys_time<Duration> const& tp) noexcept
{
    using namespace date;
    using namespace std;
    using namespace chrono;
    auto constexpr epoch = sys_days{November/24/-4713} + 12h;
    using ddays = std::chrono::duration<long double, days::period>;
    if constexpr (sys_time<ddays>{sys_time<Duration>::min()} < sys_time<ddays>{epoch})
    {
        return jdate_time<decltype(tp - epoch)>{tp - epoch};
    }
    else
    {
        // Duration overflows at the epoch.  Sub in new Duration that won't overflow.
        using D = std::chrono::duration<int64_t, ratio<1, 10'000'000>>;
        return jdate_time<decltype(round<D>(tp) - epoch)>{round<D>(tp) - epoch};
    }
}

template <class Duration>
auto
jdate_clock::to_sys(jdate_time<Duration> const& tp) noexcept
{
    using namespace date;
    using namespace std::chrono;
    return sys_time<Duration>{tp - clock_cast<jdate_clock>(sys_days{})};
}

jdate_clock::time_point
jdate_clock::now() noexcept
{
    using namespace date;
    using namespace std::chrono;
    return clock_cast<jdate_clock>(system_clock::now());
}

Here is how you would use it to print out the current time as a double:

    // Current julian day time
    auto now = jdate_clock::now();
    std::cout << now.time_since_epoch().count() << '\n';

Here is how you would convert it to the Gregorian calendar:

    // Convert to sys_time
    auto now_utc = clock_cast<system_clock>(now);
    std::cout << now_utc << '\n';

To convert it to the Julian calendar it is convenient to use this user-written Julian calendar:

    // Convert to the Julian calendar
    auto sd = floor<days>(now_utc);
    auto tod = now_utc - sd;
    julian::year_month_day jymd{sd};
    std::cout << jymd << ' ' << hh_mm_ss{tod} << '\n';

This all just output for me:

2.4606e+06
2024-10-14 15:22:32.434346
2024-10-01 15:22:32.434346

Porting it to earlier than C++17 would take a little more work.

By casting it as a clock that can convert to and from sys_time via the clock_cast facility one can interoperate with the entire chrono (date) library, including time zones, leap seconds, and even other user written calendars such as the ISO week-based calendar, or the Islamic calendar.

@drankinatty
Copy link
Author

drankinatty commented Oct 14, 2024

That is perfect. I had a sneaking suspicion you had traveled this road before. I can't believe I missed the SO answer in jumping down this rabbit hole. Thank you very much!

And thankfully, after moving to Tumbleweed on openSUSE, we now have c++23 available on all boxes (except for a couple Raspberry Pi boxes still running Buster)

@drankinatty
Copy link
Author

drankinatty commented Oct 15, 2024

One question, in your Gregorian calendar output example:

// Convert to sys_time
auto now_utc = clock_cast<system_clock>(now);
std::cout << now_utc << '\n';

Doesn't now_utc also need the selection duration().count() in order to be deduced from some epoch? Without it g++ cannot figure out the template to use with std::cout. Compiled with g++ -Wall -std=c++23 -o tst-jdate-iostream tst-jdate.cpp, the compiler says, e.g.

tst-jdate.cpp: In function ‘int main()’:
tst-jdate.cpp:83:25: error: no match for ‘operator<<’ (operand types are ‘std::basic_ostream<char>’ and ‘std::chrono::time_point<std::chrono::_V2::system_clock, std::chrono::duration<double, std::ratio<3600> > >’)
   82 |   std::cout << "jd  : " << now.time_since_epoch().count() << "\n"
      |   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   83 |             << "utc : " << now_utc << "\n";
      |             ~~~~~~~~~~~ ^~ ~~~~~~~
      |             |              |
      |             |              std::chrono::time_point<std::chrono::_V2::system_clock, std::chrono::duration<double, std::ratio<3600> > >
      |             std::basic_ostream<char>
In file included from /usr/include/c++/14/iostream:41,
                 from tst-jdate-c++20.cpp:4:
/usr/include/c++/14/ostream:116:7: note: candidate: ‘std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ostream_type& (*)(__ostream_type&)) [with _CharT = char; _Traits = std::char_traits<char>; __ostream_type = std::basic_ostream<char>]’
  116 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))
      |       ^~~~~~~~
/usr/include/c++/14/ostream:116:36: note:   no known conversion for argument 1 from ‘std::chrono::time_point<std::chrono::_V2::system_clock, std::chrono::duration<double, std::ratio<3600> > >’ to ‘std::basic_ostream<char>::__ostream_type& (*)(std::basic_ostream<char>::__ostream_type&)’ {aka ‘std::basic_ostream<char>& (*)(std::basic_ostream<char>&)’}
  116 |       operator<<(__ostream_type& (*__pf)(__ostream_type&))
      |                  ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/14/ostream:125:7: note: candidate: ‘std::basic_ostream<_CharT, _Traits>::__ostream_type& std::basic_ostream<_CharT, _Traits>::operator<<(__ios_type& (*)(__ios_type&)) [with _CharT = char; _Traits = std::char_traits<char>; __ostream_type = std::basic_ostream<char>; __ios_type = std::basic_ios<char>]’
...
<snip of lots and lots of candidates...>

The output above is with g++ (SUSE Linux) 14.2.1 20241007

Measuring from now_utc.time_since_epoch().count() allows deduction.

To handle the different Gregorian calendar discontinuities depending upon which adoption is being used in a given setting for the conversion from Julian Day back to Gregorian calendar date I suppose separate clock implementations, or another member variable holding a value indicating which adoption to use would be needed that could handle the different adoption date and times by adding or subtracting the accumulated leap second resets to the conversion from Julian back to Gregorian calendar. I'll definitively have to read a bit more to learn how best to do that.

It's remarkable how easily the library handles the new calendar just by defining a new epoch.

@HowardHinnant
Copy link
Owner

Ah, my bad.

The problem is that the C++ 20 spec doesn't deal well with time_points that have a floating point representation such as double. The fix is to cast it to integral:

auto now_utc = round<nanoseconds>(clock_cast<system_clock>(now));

nanoseconds is integral based, and round will convert to the nearest nanosecond in this case. You could give any precision you desire (seconds, milliseconds, whatever).

I didn't pick this up because I've added an extension to my library to handle double-based time points.

Demo: https://gcc.godbolt.org/z/xj5KnvsfY

@HowardHinnant
Copy link
Owner

HowardHinnant commented Oct 15, 2024

On switching between Julian and Gregorian I have't coded anything up. But I'm imagining something along the lines of:

    std::map<std::string, sys_days> change{{"Italy", October/15/1582},
                                           {"England", September/14/1752},
                                           ...};
    std::string country = ...
    sys_days date = ...
    if (date < change[country])
        // use Julian
    else
        // use Gregorian

It'd be really cool if you can figure out how to make that map constexpr.

And if you need to deal with Sweden, I think you'll have to create a Swedish calendar. That one is much more complex than switching on a given date.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants