Handling Julian days in C++11/14
Asked Answered
C

2

21

What is the best/easiest way to deal with Julian days in C++? I want to be able to convert between Julian days and Gregorian dates. I have C++11 and C++14. Can the <chrono> library help with this problem?

Campanulate answered 27/11, 2015 at 20:31 Comment(2)
Please, forgive my pedantic suggestion: s/ Julian date / Julian day . Similarly to Gregorian date, Julian date sounds to me as a "date in the Julian calendar". It's unfortunate that these (related but different) concepts have confusingly close names.Chronic
Good suggestion, thanks. I've edited both the question and answer.Campanulate
C
38

To convert between a Julian day and std::chrono::system_clock::time_point the first thing one needs to do is find out the difference between the epochs.

The system_clock has no official epoch, but the de facto standard epoch is 1970-01-01 00:00:00 UTC (Gregorian calendar). For convenience, it is handy to state the Julian date epoch in terms of the proleptic Gregorian calendar. This calendar extends the current rules backwards, and includes a year 0. This makes the arithmetic easier, but one has to take care to convert years BC into negative years by subtracting 1 and negating (e.g. 2BC is year -1). The Julian date epoch is -4713-11-24 12:00:00 UTC (roughly speaking).

The <chrono> library can conveniently handle time units on this scale. Additionally, this date library can conveniently convert between Gregorian dates and system_clock::time_point. To find the difference between these two epochs is simply:

constexpr
auto
jdiff()
{
    using namespace date;
    using namespace std::chrono_literals;
    return sys_days{January/1/1970} - (sys_days{November/24/-4713} + 12h);
}

This returns a std::chrono::duration with a period of hours. In C++14 this can be constexpr and we can use the chrono duration literal 12h instead of std::chrono::hours{12}.

If you don't want to use the date library, this is just a constant number of hours and can be rewritten to this more cryptic form:

constexpr
auto
jdiff()
{
    using namespace std::chrono_literals;
    return 58574100h;
}

Either way you write it, the efficiency is identical. This is just a function that returns the constant 58574100. This could also be a constexpr global, but then you have to leak your using declarations, or decide not to use them.

Next it is handy to create a Julian day clock (jdate_clock). Since we need to deal with units at least as fine as a half a day, and it is common to express julian dates as floating point days, I will make the jdate_clock::time_point a count of double-based days from the epoch:

struct jdate_clock
{
    using rep        = double;
    using period     = std::ratio<86400>;
    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
    {
        using namespace std::chrono;
        return time_point{duration{system_clock::now().time_since_epoch()} + jdiff()};
    }
};

Implementation note:

I converted the return from system_clock::now() to duration immediately to avoid overflow for those systems where system_clock::duration is nanoseconds.

jdate_clock is now a fully conforming and fully functioning <chrono> clock. For example I can find out what time it is now with:

std::cout << std::fixed;
std::cout << jdate_clock::now().time_since_epoch().count() << '\n';

which just output:

2457354.310832

This is a type-safe system in that jdate_clock::time_point and system_clock::time_point are two distinct types which one can not accidentally perform mixed arithmetic in. And yet you can still get all of the rich benefits from the <chrono> library, such as add and subtract durations to/from your jdate_clock::time_point.

using namespace std::chrono_literals;
auto jnow = jdate_clock::now();
auto jpm = jnow + 1min;
auto jph = jnow + 1h;
auto tomorrow = jnow + 24h;
auto diff = tomorrow - jnow;
assert(diff == 24h);

But if I accidentally said:

auto tomorrow = system_clock::now() + 24h;
auto diff = tomorrow - jnow;

I would get an error such as this:

error: invalid operands to binary expression
  ('std::chrono::time_point<std::chrono::system_clock, std::chrono::duration<long long,
  std::ratio<1, 1000000> > >' and 'std::chrono::time_point<jdate_clock, std::chrono::duration<double,
  std::ratio<86400, 1> > >')
auto diff = tomorrow - jnow;
            ~~~~~~~~ ^ ~~~~

In English: You can't subtract a jdate_clock::time_point from a std::chrono::system_clock::time_point.

But sometimes I do want to convert a jdate_clock::time_point to a system_clock::time_point or vice-versa. For that one can easily write a couple of helper functions:

template <class Duration>
constexpr
auto
sys_to_jdate(std::chrono::time_point<std::chrono::system_clock, Duration> tp) noexcept
{
    using namespace std::chrono;
    static_assert(jdate_clock::duration{jdiff()} < Duration::max(),
                  "Overflow in sys_to_jdate");
    const auto d = tp.time_since_epoch() + jdiff();
    return time_point<jdate_clock, std::remove_cv_t<decltype(d)>>{d};
}

template <class Duration>
constexpr
auto
jdate_to_sys(std::chrono::time_point<jdate_clock, Duration> tp) noexcept
{
    using namespace std::chrono;
    static_assert(jdate_clock::duration{-jdiff()} > Duration::min(),
                  "Overflow in jdate_to_sys");
    const auto d = tp.time_since_epoch() - jdiff();
    return time_point<system_clock, std::remove_cv_t<decltype(d)>>{d};
}

Implementation note:

I've added static range checking which is likely to fire if you use nanoseconds or a 32bit-based minute as a duration in your source time_point.

The general recipe is to get the duration since the epoch (durations are "clock neutral"), add or subtract the offset between the epochs, and then convert the duration into the desired time_point.

These will convert among the two clock's time_points using any precision, all in a type-safe manner. If it compiles, it works. If you made a programming error, it shows up at compile time. Valid example uses include:

auto tp = sys_to_jdate(system_clock::now());

tp is a jdate::time_point except that it has integral representation with the precision of whatever your system_clock::duration is (for me that is microseconds). Be forewarned that if it is nanoseconds for you (gcc), this will overflow as nanoseconds only has a range of +/- 292 years.

You can force the precision like so:

auto tp = sys_to_jdate(time_point_cast<hours>(system_clock::now()));

And now tp is an integral count of hours since the jdate epoch.

If you are willing to use this date library, one can easily use the utilities above to convert a floating point julian date into a Gregorian date, with any accuracy you want. For example:

using namespace std::chrono;
using namespace date;
std::cout << std::fixed;
auto jtp = jdate_clock::time_point{jdate_clock::duration{2457354.310832}};
auto tp = floor<seconds>(jdate_to_sys(jtp));
std::cout << "Julian day " << jtp.time_since_epoch().count()
          << " is " << tp << " UTC\n";

We use our jdate_clock to create a jdate_clock::time_point. Then we use our jdate_to_sys conversion function to convert jtp into a system_clock::time_point. This will have a representation of double and a period of hours. That isn't really important though. What is important is to convert it into whatever representation and precision you want. I've done that above with floor<seconds>. I also could have used time_point_cast<seconds> and it would have done the same thing. floor comes from the date library, always truncates towards negative infinity, and is easier to spell.

This will output:

Julian day 2457354.310832 is 2015-11-27 19:27:35 UTC

If I wanted to round to the nearest second instead of floor, that would simply be:

auto tp = round<seconds>(jdate_to_sys(jtp));
Julian date 2457354.310832 is 2015-11-27 19:27:36 UTC

Or if I wanted it to the nearest millisecond:

auto tp = round<milliseconds>(jdate_to_sys(jtp));
Julian day 2457354.310832 is 2015-11-27 19:27:35.885 UTC

Update for C++17

The floor and round functions mentioned above as part of Howard Hinnant's date library are now also available under namespace std::chrono as part of C++17.

Update for C++20

Howard Hinnant's date library was largely voted into C++20, and so jdate_clock can now be written entirely in terms of std::chrono.

Additionally there is a handy std::chrono::clock_cast feature which jdate_clock can participate in. This facilitates the conversion between time_points of different clocks, and can even help in the implementation of jdate_clock:

#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());
}

jdate_time is simply a convenience type alias written in the style of new convenience type alias provided by std::chrono. It shortens some of the signatures in the implementation of jdate_clock and makes it easier for clients to make time_points of jdate_clock with arbitrary durations.

There are two new static member functions of jdate_clock: from_sys and to_sys. These take place of the previous namespace scope functions sys_to_jdate and jdate_to_sys. from_sys and to_sys are what enables jdate_clock to participate in the std::chrono::clock_cast facility.

clock_cast looks for these static member functions and uses them to convert between jdate_clock and every other clock, chrono-defined or not, that participates in the clock_cast facility.

now() can simply clock_cast from system_clock::now() to return the current time.

from_sys simply subtracts the given system_clock-based time_point and the Julian epoch: -4713-11-24 12:00:00 UTC. The return type must be at least as fine as hours since the epoch has a precision of hours.

However, there is a complication: Since the epoch is so far in the past, some common measures, such as sys_time<nanoseconds> overflow this far back. So a constexpr test is done to see if the sys_time<Duration> will overflow. If it does, a new Duration is subbed in that is known to not overflow.

to_sys can reuse the epoch in from_sys by using clock_cast to find the Julian date at the system_clock epoch: clock_cast<jdate_clock>(sys_days{}). This is subtracted from the Julian date to find the time since the system_clock epoch.

The client code can now use the generic clock_cast in place of the less generic jdate_to_sys API:

using namespace std::chrono;

auto jtp = jdate_clock::time_point{jdate_clock::duration{2457354.310832}};
auto tp = round<milliseconds>(clock_cast<system_clock>(jtp));
std::cout << "Julian day " << jtp.time_since_epoch()
          << " is " << tp << " UTC\n";

Output:

Julian date 2457354.310832d is 2015-11-27 19:27:35.885 UTC

And finally note that although jdate_clock knows nothing about std::chrono::tai_clock, clock_cast can still convert to and from it as well.

auto jtp = jdate_clock::time_point{jdate_clock::duration{2457354.310832}};
auto tp = round<milliseconds>(clock_cast<tai_clock>(jtp));
std::cout << "Julian day " << jtp.time_since_epoch()
          << " is " << tp << " TAI\n";

Output:

Julian day 2457354.310832d is 2015-11-27 19:28:11.885 TAI
Campanulate answered 27/11, 2015 at 20:31 Comment(2)
+1 to the question. I haven't for the answer because it recalls to me some traits of sponsorship. The fact that you also provide a pure C++ version evens the average rating, though. Just my objective view.Clubby
@black: No problem. Fwiw, the date library (sponsorship) is free, open-source, MIT license, github repository. And here are the public-domain algorithms the date library is based on: howardhinnant.github.io/date_algorithms.html (in case you would rather build your own date library).Campanulate
P
1

Thank you Howard for providing these helpful examples. I ended up using slightly modified versions of the sys_to_jdate and jdate_to_sys functions in order to successfully compile using MSVC/C++17. The original forms did compile using clang 12.0.0.12000032 and gcc 8.3.1.

template <class Duration>
constexpr
auto
sys_to_jdate_v2(std::chrono::time_point<std::chrono::system_clock, Duration> tp) noexcept
{
    static_assert(jdate_clock::duration{jdiff()} < Duration::max(),
                  "Overflow in sys_to_jdate");
    const auto d = jdate_clock::duration{tp.time_since_epoch() + jdiff()};
    return jdate_clock::time_point{d};
}

template <class Duration>
constexpr
auto
jdate_to_sys_v2(std::chrono::time_point<jdate_clock, Duration> tp) noexcept
{
    static_assert(jdate_clock::duration{-jdiff()} > Duration::min(),
                  "Overflow in jdate_to_sys");
    const auto d = std::chrono::duration_cast<std::chrono::system_clock::duration>(tp.time_since_epoch() - jdiff());
    return std::chrono::system_clock::time_point{d};
}

The above modifications made the following compile error disappear:

C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.28.29333\include\chrono(182,23): error C2338: duration must be an instance of std::duration

(apparently provoked by the last line of jdate_to_sys)

I am very new to chrono and date APIs, so would welcome input as to the correctness of my fixes.

Pantile answered 16/9, 2022 at 9:30 Comment(3)
Removing the const from the declaration of d also works. It turns out that you may have stumbled over a bug in the standards spec, or in one of the std::lib implementations. I'm not sure yet. Investigating....Campanulate
It looks like the bug is in my answer and llvm's libc++ where I tested my answer. I've updated sys_to_jdate and jdate_to_sys with remove_cv_t to fix the bug.Campanulate
@HowardHinnant thank you investigating and proposing a fix. I have confirmed that the updated code compiles using clang 12.0.0.12000032, gcc 8.3.1, and MSVC 14.28.29333.Pantile

© 2022 - 2024 — McMap. All rights reserved.