c++c++-chrono

std::chrono::time_point is slightly off


If I do the following:

auto time = std::chrono::system_clock::duration::zero();
std::cout << time.count() << std::endl;
time += std::chrono::years(2024 - 1970);
std::cout << time.count() << std::endl;

It comes out to be Monday, January 1, 2024 2:16:48 AM, not 12 AM. I want to represent historic calendar dates. year_month_day and hh_mm_ss are available for use, but I enjoy having the ability of getting time_point::time_since_epoch(). I need this because it is a convenient way of storing date and time data in a database as a single int64. Is there a way to get the best of both worlds?

As I see it my only options are to handle the timings myself and keep using std::chrono, but this defeats the purpose of using std::chrono, or to use chrono::year_month_day and chrono::hh_mm_ss and deal with the database in a different way.


Solution

  • See this answer for a more in-depth description of chronological computations vs calendrical computations.

    But in short, this:

    auto time = std::chrono::system_clock::duration::zero();
    std::cout << time.count() << std::endl;
    time += std::chrono::years(2024 - 1970);
    std::cout << time.count() << std::endl;
    

    is a chronological computation. I characterize it as such because it adds a regular unit to a count of regular units. A single std::chrono::years is nothing more than 31'556'952 seconds which is the precise length of the average year in the civil calendar (other calendars will have years of different average length).

    And in truth, I would not even count the above code as necessarily well posed because it clearly intends that time be a point in time, and yet it is is declared as a duration. A better way to perform this chronological computation would be:

    Demo:

    using namespace std::literals;
    std::chrono::system_clock::time_point time{};  // zero-initialize
    std::cout << time << '\n';   // 1970-01-01 00:00:00.000000000
    time += 2024y - 1970y;
    std::cout << time << '\n';   // 2024-01-01 02:16:48.000000000
    

    Now time is a time_point. One can still get the internal count out of it if desired:

    std::cout << time.time_since_epoch().count() << '\n';
    

    To perform a calendrical computation one must convert to a calendrical system (there are several), perform the computation in the calendrical system, and then (if desired) convert back to the chronological system.

    For example:

    Demo:

    std::chrono::system_clock::time_point time{};  // zero-initialize
    std::cout << time << '\n';   // 1970-01-01 00:00:00.000000000
    std::chrono::year_month_day ymd = std::chrono::floor<std::chrono::days>(time);
    time = std::chrono::sys_days{ymd + (2024y - 1970y)};
    std::cout << time << '\n';   // 2024-01-01 00:00:00.000000000
    

    As a demonstration of using an alternative calendrical computation consider this:

    std::chrono::year_month_weekday ymd = std::chrono::floor<std::chrono::days>(time);
    time = std::chrono::sys_days{ymd + (2024y - 1970y)};
    std::cout << time << '\n';   // 2024-01-04 00:00:00.000000000
    

    Now instead of adding 54 years to Jan 1, 1970 one is adding 54 years to the first Thursday of Jan 1970 to get the first Thursday of Jan 2024. Indeed year_month_weekday is truly a full demonstration of an alternate calendar. User written calendars modeling the Julian, Chinese or Islamic calendars could also be used, and likely give different results as well.


    I discourage falling back to the C timing API unless you need to do so to interface with legacy code. C++20 and forward has a superset of the date/time functionality of the old C API. It is also much safer and easier to use.

    If you identify functionality in the C timing API that you aren't sure how to do in modern <chrono>, just ask here on stackoverflow and I'm sure someone will be glad to show you how to do it.