Search code examples
c++c++20c++-chrono

How to safely clock_cast days?


I'm using HowardHinnant/date in lieu of the new C++20 calendar/timezone facilities that are not yet available in Clang/GCC. My question applies equally to both implementations: How do I safely clock_cast time_points having days duration?

When I try:

using namespace date; // or using std::chrono in C++20
tai_time<days> tai{days{42}};
sys_days sys = clock_cast<std::chrono::system_clock>(tai);

I get a "no viable conversion" error for the last statement. It turns out the result uses a duration that is common_type<days, seconds>, which comes from utc_clock::to_sys() that's used in the conversion. The common duration type is std::chrono::seconds, so it's normal that it can't be directly converted to a days duration.

I can get it to compile if I use an explicit duration_cast:

using namespace date; // or using std::chrono in C++20
tai_time<days> tai{days{42}};
auto casted = clock_cast<std::chrono::system_clock>(tai);
sys_days sys{std::chrono::duration_cast<days>(casted.time_since_epoch())};

... but I'm worried that the result might by off by a day due to the truncation (especially for dates preceding the epoch). Is this the correct way to do what I'm trying to do? Should I be using floor instead of duration_cast?

Why is there even a std::common_type_t<Duration, std::chrono::seconds> in utc_clock::to_sys() anyway? Shouldn't it simply return the same duration type?


Solution

  • The reason that the clock_cast insists on at least seconds precision is because the offset between the epochs of system_clock and tai_clock has a precision of seconds:

    auto diff = sys_days{} - clock_cast<system_clock>(tai_time<days>{});
    cout << diff << " == " << duration<double, days::period>{diff} << '\n';
    

    Output:

    378691210s == 4383.000116d
    

    So a days precision cast would be lossy. Here's another way of looking at it:

    cout << clock_cast<tai_clock>(sys_days{2021_y/June/1}) << '\n';
    

    Output:

    2021-06-01 00:00:37
    

    I.e. TAI is currently 37 seconds ahead of system_clock in calendrical terms.

    If all you want is the date, I recommend round<days>(result):

    cout << round<days>(clock_cast<tai_clock>(sys_days{2021_y/June/1})) << '\n';
    

    Output:

    2021-06-01 00:00:00
    

    One could conceivably use floor in one direction and ceil in the other, but that would be very error prone. round will do fine since the offset from an integral number of days is currently only 37 seconds and growing quite slowly.