Search code examples
c++epochc++-chrono

How do I use std::chrono to extract time units out of epoch time?


I get the epoch time in ms from my API and would like to extract the year, month, etc. from it (also compare two dates, add hours/days/months, etc.).

I have watched Howard Hinnant "A chrono Tutorial" and tried cppreference, but I can't quite get it to work.

Here is all I have so far:

enum class DatePrecision { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MILLI };
long long et = 1693322440062;
std::chrono::milliseconds ms{ et };
std::chrono::time_point<std::chrono::system_clock> tp(ms);

int getTimeUnit(const std::chrono::time_point tp, const DatePrecision dp) { 
  /* .?. */ 
}

Where do I go from here? tp only offers me time_since_epoch as a function on it and as I said, I can't get the example from cppreference to work, especially when using utc_time instead of system_time. Extracting yeah/month/day would be a great start, but I would prefer

Thank you in advance for your help!


Solution

  • So this is pretty easy to do, but I don't recommend it. The reason I don't recommend it is that it is best to stay within the chrono type system so that the compiler can help you find any logic errors at compile-time. Returning these values as an int erases the type safety that chrono provides you.

    Ok, enough with the lecture. Here's how you can do it in C++20 (assuming your vendor has implemented all of the chrono parts of C++20):

    #include <chrono>
    #include <iostream>
    
    enum class DatePrecision { YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, MILLI };
    
    int
    getTimeUnit(const std::chrono::system_clock::time_point tp, const DatePrecision dp)
    { 
        using namespace std;
        using namespace std::chrono;
    
        auto tp_days = floor<days>(tp);
        year_month_day ymd = tp_days;
        hh_mm_ss hms{floor<milliseconds>(tp - tp_days)};
        switch (dp)
        {
        case DatePrecision::YEAR:
            return int{ymd.year()};
        case DatePrecision::MONTH:
            return unsigned{ymd.month()};
        case DatePrecision::DAY:
            return unsigned{ymd.day()};
        case DatePrecision::HOUR:
            return hms.hours().count();
        case DatePrecision::MINUTE:
            return hms.minutes().count();
        case DatePrecision::SECOND:
            return hms.seconds().count();
        case DatePrecision::MILLI:
            return hms.subseconds().count();
        }
    }
    
    int main()
    {
        using namespace std;
        using namespace std::chrono;
    
        long long et = 1693322440062;
        std::chrono::milliseconds ms{ et };
        std::chrono::time_point<std::chrono::system_clock> tp(ms);
    
        cout << tp << '\n';
        cout << getTimeUnit(tp, DatePrecision::YEAR) << '\n';
        cout << getTimeUnit(tp, DatePrecision::MONTH) << '\n';
        cout << getTimeUnit(tp, DatePrecision::DAY) << '\n';
        cout << getTimeUnit(tp, DatePrecision::HOUR) << '\n';
        cout << getTimeUnit(tp, DatePrecision::MINUTE) << '\n';
        cout << getTimeUnit(tp, DatePrecision::SECOND) << '\n';
        cout << getTimeUnit(tp, DatePrecision::MILLI) << '\n';
    }
    

    Output:

    2023-08-29 15:20:40.062000
    2023
    8
    29
    15
    20
    40
    62
    

    Explanation:

    You need to switch on each desired field for the proper syntax to extract it from the time_point. Note that std::chrono::time_point is a template, not a type. I presumed you meant std::chrono::system_clock::time_point (or equivalently std::chrono::time_point<std::chrono::system_clock>) in the getTimeUnit signature.

    Also note that time_points based on system_clock have the semantics of UTC, not local time.

    For the "date fields", first truncate the time_point to days precision with floor<days>, and then convert that to a year_month_day. The year_month_day type has getters for year, month and day, which subsequently have explicit conversions to integral types.

    For the "time of day fields", subtract the day-precision time_point from the original to get the time of day as a duration. Then the type hh_mm_ss transforms that duration into a {hours, minutes, seconds, subseconds} data structure with getters for each field.

    If you don't have the C++20 chrono bits available to you, you can still use this code by taking advantage of my free, open-source, header-only C++20 chrono preview library.

    Just add #include "date/date.h" and using namespace date; in appropriate places, and the rest of the syntax is the same. This will obviously easily port to C++20 when you are able to migrate to it.

    Disclaimer: I've thrown this together, and it can probably be refactored to be a little bit neater. But hopefully this will get you started.


    Now with time zones

    Using this a bit more, it is indeed problematic that this not aware of time zones (or is using the wrong one, I'm UTC+1). Is there any quick fix to consider these?

    There's a few ways to interpret this, but I think I know what you mean. :-)

    1. The input is UTC (Unix Time) and you want the output in the current local time zone.
    2. The input is a local time point and you want the output units in UTC.
    3. The input is a local time point and you want the output units in local time.

    The third is trivial, and won't change the results. Though to be consistent, the input should use local_time instead of sys_time as the input (e.g. std::chrono::local_time<std::chrono::milliseconds> as the first parameter).

    The second would be somewhat unusual, but is certainly easy. It would be only a minor modification of the first choice (which I'm assuming you want).

    I'm assuming you mean the first. Here is a slight modification of my original answer that outputs the units in a time zone of your choosing, and defaults to your computer's local time zone:

    int
    getTimeUnit(const std::chrono::system_clock::time_point tp_utc,
                const DatePrecision dp,
                const std::chrono::time_zone* tz = std::chrono::current_zone())
    { 
        using namespace std;
        using namespace std::chrono;
    
        auto tp_local = tz->to_local(tp_utc);
        auto tp_days = floor<days>(tp_local);
        year_month_day ymd{tp_days};
        hh_mm_ss hms{floor<milliseconds>(tp_local - tp_days)};
        switch (dp)
        {
        case DatePrecision::YEAR:
            return int{ymd.year()};
        case DatePrecision::MONTH:
            return unsigned{ymd.month()};
        case DatePrecision::DAY:
            return unsigned{ymd.day()};
        case DatePrecision::HOUR:
            return hms.hours().count();
        case DatePrecision::MINUTE:
            return hms.minutes().count();
        case DatePrecision::SECOND:
            return hms.seconds().count();
        case DatePrecision::MILLI:
            return hms.subseconds().count();
        }
    }
    

    Besides some variable renaming (for clarity), there's really only two changes:

    1. You first have to convert the input UTC time point to a local time point with auto tp_local = tz->to_local(tp_utc);.

    2. The conversion from the days-precision time_point to year_month_day is now an explicit conversion as opposed to an implicit one: year_month_day ymd{tp_days};. Explicit conversion syntax would have also worked in the original code.

    Everything else is exactly the same, even including how you use it:

    int h = getTimeUnit(tp, DatePrecision::HOUR);
    

    However if you would like to specify the time zone instead of get the local one, that can be done like this:

    int h = getTimeUnit(tp, DatePrecision::HOUR,
                        std::chrono::locate_zone("America/New_York"));