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

C++ Convert Unix time (nanoseconds) to readable datetime in local timezone


I have a Unix time (nanoseconds since epoch) and I would like to go back and forth between this and a readable string in a specifiable timezone (using TZ strings), with nanoseconds preserved. I am using C++17 but willing to migrate to C++20 if it would make things much easier.

Example:

uint64_t unix_time = 1669058870115719258;
std::string datetime = convert_unix_to_datetime(unix_time, "America/Detroit");
std::cout << datetime << "\n";
std::cout << convert_datetime_to_unix_time(datetime) << "\n";

Desired output:

2021-11-21 14:27:50.115719258
1669058870115719258

Apologies if this is a basic question--I have searched around the site and tried to implement solutions but keep getting confused by different C++ versions, whether the datetime is in UTC or local timezone, different types like chrono::time_point vs. time_t etc. Hoping to find a simple solution here.


Solution

  • Using C++20, this is very easy. Using C++11/14/17 it is harder but doable with a free, open-source time zone library.

    Here is what it looks like with C++20:

    #include <chrono>
    #include <cstdint>
    #include <format>
    #include <iostream>
    #include <sstream>
    #include <string>
    
    std::string
    convert_unix_to_datetime(std::uint64_t unix_time, std::string const& tz)
    {
        using namespace std;
        using namespace chrono;
    
        sys_time<nanoseconds> tp{nanoseconds{unix_time}};
        return format("{:%F %T} ", zoned_time{tz, tp}) + tz;
    }
    

    The first line converts the uint64_t, first into a nanoseconds chrono::duration, and then into a nanoseconds-precision chrono::time_point based on system_clock. chrono::system_clock uses Unix Time as its measure: http://eel.is/c++draft/time.clock.system#overview-1

    The second line creates a chrono::zoned_time from the time zone name, and the sys_time timepoint. A zoned_time is a simple pairing of these two objects from which you can extract a local time. When formatted, it is the local time that is printed. Here I've used "%F %T" as the format which will output with the syntax: YYYY-MM-DD HH:MM:SS.fffffffff. Many formatting flags are available..

    Finally I've added the time zone name to the timestamp. This is done so that the parsing function can recover the time zone name as it is not passed in as one of the parameters to convert_datetime_to_unix_time.

    std::uint64_t
    convert_datetime_to_unix_time(std::string s)
    {
        using namespace std;
        using namespace chrono;
    
        istringstream in{std::move(s)};
        in.exceptions(ios::failbit);
        std::string tz_name;
        local_time<nanoseconds> tp;
        in >> parse("%F %T %Z", tp, tz_name);
        zoned_time zt{tz_name, tp};
        return zt.get_sys_time().time_since_epoch().count();
    }
    

    The first line moves the string into a istringstream as the parsing must use a stream. I've set the stream to throw an exception if there is a syntax error in the input s. If you would rather check for failbit manually, remove the line that sets failbit to throw an exception.

    Next arguments are default constructed which will be parsed into:

    std::string tz_name;
    local_time<nanoseconds> tp;
    

    Here it is important to use the type local_time as opposed to sys_time because it is the local time that was formatted out. Use of the local_time type informs the library how to apply the UTC offset under the zoned_time constructor after parse.

    The parse can pick up the time zone name with the %Z flag, and will place it in the last argument to parse.

    Construct the zoned_time with the time zone and time point.

    Finally, the sys_time (Unix Time) can be extracted from the zoned_time with the .get_sys_time() member function. This will have precision nanoseconds since the input local_time has precision nanoseconds. The underlying integral count of nanoseconds is extracted with .time_since_epoch().count().

    Using your example driver, this will output:

    2022-11-21 14:27:50.115719258 America/Detroit
    1669058870115719258
    

    As I write this, only the latest MSVC tools fully implement this part of C++20.


    If you need to use the free, open-source time zone library, a few minor changes are needed:

    This will by default download a copy of the IANA time zone database, though there are ways to avoid that on non-Windows platforms (described in the installation instructions).


    Response to Tom's Answer

    Which starts off with:

    Since we're looking at a date and time output that specifies seconds (and nanoseconds) it is worth pointing out that mixing sys_time or zoned_time with civil date and time representations is inherently approximate, the current error being 27 seconds...

    And ends with:

    Aside: IMHO, utc_clock should be dropped...

    std::chrono::system_clock is a simple wrapper around the same clock that the C time() function references, which in turn is a simple wrapper around an OS clock. It turns out in practice that all these clocks measure Unix Time: A count of time since 1970-01-01 00:00:00 UTC, excluding leap seconds.

    C++20 introduces std::chrono::utc_clock so that if need be, you can subtract two points in time and take into account the leap seconds that (if any) that system_clock ignores.

    For example, here is a short program that finds the number of elapsed seconds from 2010-01-01 00:00:00 EST to 2020-01-01 00:00:00 EST where EST is the abbreviation for the time zone America/New_York:

    #include <chrono>
    #include <iostream>
    
    int
    main()
    {
        using namespace std::chrono;
        using namespace std;
        
        auto tz = locate_zone("America/New_York");
        auto t0 = tz->to_sys(local_days{January/1/2010});
        auto t1 = tz->to_sys(local_days{January/1/2020});
        seconds without_leap_seconds = t1 - t0;
        auto with_leap_seconds = clock_cast<utc_clock>(t1)
                               - clock_cast<utc_clock>(t0);
        cout << "Seconds from the start of 2010 to the start of 2020 in New York:\n";
        cout << "without leap seconds = " << without_leap_seconds << '\n';
        cout << "with    leap seconds = " << with_leap_seconds << '\n';
    }
    

    The program starts with local times indicating the New Year in New York, and then converts those to sys_time, which is the C++20 name for time points associated with system_clock, and that is specified to model Unix Time.

    The program computes the difference in sys_time, and then also converts both time points to utc_time which is specified to include leap seconds, and the difference is computed again.

    The output of this program is:

    Seconds from the start of 2010 to the start of 2020 in New York:
    without leap seconds = 315532800s
    with    leap seconds = 315532803s
    

    Note that the value without leap seconds is a multiple of 86400s, and the value with leap seconds is 3s greater. This is consistent with this table of leap seconds.

    Working demo.

    I write this to demonstrate how C++20 can be used to correctly account for leap seconds, even when the input is civil date and time representations.