c++c++-chronotime-t

C++ Read time_t in to different timezone then extract year/month....nanoseconds


I'm extracting the year/month/day/hours/min/sec/nanoseconds from a source containing nanoseconds since Epoch, using the answer to the below question:

Extract year/month/day etc. from std::chrono::time_point in C++

However, my input is a different timezone. Below is the code I have so far.

  1. How do I convert the below to read from a different timezone?
  2. Do I need to convert before I perform the duration_casts? Otherwise the number of hours/mins/secs could be wrong?

I'm using C++17, Clang, Linux and prefer standard libraries. Will be moving to C++20 in a few months and I suspect that would simplify the answer.

using namespace std;
using namespace std::chrono;
using Clock = high_resolution_clock;
using TimePoint = time_point<Clock>;

const nanoseconds nanosecondsSinceEpoch(nanosecondsSinceEpochTS);
const Clock::duration since_epoch = nanosecondsSinceEpoch;
const TimePoint time_point_sinc_epoch(since_epoch);

using days = duration<int, ratio_multiply<hours::period, ratio<24> >::type>;

system_clock::time_point now = time_point_sinc_epoch;  // Do I need to handle timezone here before duration_cast?
system_clock::duration tp = now.time_since_epoch();
days d = duration_cast<days>(tp);
tp -= d;
hours h = duration_cast<hours>(tp);
tp -= h;
minutes m = duration_cast<minutes>(tp);
tp -= m;
seconds s = duration_cast<seconds>(tp);
tp -= s;

const uint64_t nanosSinceMidnight = tp.count();

time_t tt = system_clock::to_time_t(now);
tm utc_tm = *gmtime(&tt);                    // Presumably this needs to change

std::cout << utc_tm.tm_year + 1900 << '-';
std::cout << utc_tm.tm_mon + 1 << '-';
std::cout << utc_tm.tm_mday << ' ';
std::cout << utc_tm.tm_hour << ':';
std::cout << utc_tm.tm_min << ':';
std::cout << utc_tm.tm_sec << '\n';

Solution

  • Since your input and output are in the same timezone, the timezone itself becomes irrelevant. This subsequently makes this problem very easy. One simply converts the count of nanoseconds into the desired fields. I recommend one short public domain helper function to convert the count of days into a {y, m, d} data structure.

    #include <chrono>
    #include <iostream>
    #include <tuple>
    
    // Returns year/month/day triple in civil calendar
    // Preconditions:  z is number of days since 1970-01-01 and is in the range:
    //                   [numeric_limits<Int>::min(), numeric_limits<Int>::max()-719468].
    template <class Int>
    constexpr
    std::tuple<Int, unsigned, unsigned>
    civil_from_days(Int z) noexcept
    {
        static_assert(std::numeric_limits<unsigned>::digits >= 18,
                 "This algorithm has not been ported to a 16 bit unsigned integer");
        static_assert(std::numeric_limits<Int>::digits >= 20,
                 "This algorithm has not been ported to a 16 bit signed integer");
        z += 719468;
        const Int era = (z >= 0 ? z : z - 146096) / 146097;
        const unsigned doe = static_cast<unsigned>(z - era * 146097);          // [0, 146096]
        const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;  // [0, 399]
        const Int y = static_cast<Int>(yoe) + era * 400;
        const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);                // [0, 365]
        const unsigned mp = (5*doy + 2)/153;                                   // [0, 11]
        const unsigned d = doy - (153*mp+2)/5 + 1;                             // [1, 31]
        const unsigned m = mp + (mp < 10 ? 3 : -9);                            // [1, 12]
        return std::tuple<Int, unsigned, unsigned>(y + (m <= 2), m, d);
    }
    
    int
    main()
    {
        using namespace std;
        using namespace std::chrono;
    
        auto nanosecondsSinceEpochTS = 1592130258959736008;
        using days = duration<int, ratio_multiply<hours::period, ratio<24> >>;
    
        nanoseconds ns(nanosecondsSinceEpochTS);
        auto D = floor<days>(ns);
        ns -= D;
        auto H = duration_cast<hours>(ns);
        ns -= H;
        auto M = duration_cast<minutes>(ns);
        ns -= M;
        auto S = duration_cast<seconds>(ns);
        ns -= S;
        auto [y, m, d] = civil_from_days(D.count());
        cout << "y = " << y << '\n';
        cout << "m = " << m << '\n';
        cout << "d = " << d << '\n';
        cout << "H = " << H.count() << '\n';
        cout << "M = " << M.count() << '\n';
        cout << "S = " << S.count() << '\n';
        cout << "NS = " << ns.count() << '\n';
    }
    

    Output:

    y = 2020
    m = 6
    d = 14
    H = 10
    M = 24
    S = 18
    NS = 959736008
    

    Update

    After discussions in the comments below, it was discovered that nanosecondsSinceEpochTS is UTC, not America/Chicago as I presumed. That means that the UTC offset, which is a function of both the timezone and the nanosecond count, must be added to the count as the first step. And then proceed as directed above to get each field.

    Finding the correct offset is a non-trivial procedure which I won't attempt to show code for. One technique is to precompute a table of {utc_timestamp, utc_offset} for all of the input years in question, and then use the input utc_timestamp to look up the correct offset.

    In C++20 one can simply:

    zoned_time zt{"America/Chicago", sys_time{nanoseconds{nanosecondsSinceEpochTS}}};
    cout << zt << '\n';
    

    And get the output:

    2020-06-14 05:24:18.959736008 CDT
    

    If one wants the integral fields:

    auto lt = zt.get_local_time();  // look up utc offset and add it to sys_time
    year_month_day ymd{floor<days>(lt)};  // run civil_from_days
    hh_mm_ss tod{lt - floor<days>(lt)};  // {H, M, S, NS} since local midnight
    
    // copy each underlying integral value
    auto y = int{ymd.year()};
    auto m = unsigned{ymd.month()};
    auto d = unsigned{ymd.day()};
    auto H = tod.hours().count();
    auto M = tod.minutes().count();
    auto S = tod.seconds().count();
    auto NS = tod.subseconds().count();
    

    Disclaimer: As I write this, no vendor is yet shipping this part of C++20.

    Update for POSIX time zones

    If you're willing to use this free, open-source, header-only library you can use POSIX time zones which avoid the IANA database install issues.

    It looks like:

    #include "date/ptz.h"
    #include <iostream>
    
    int
    main()
    {
        using namespace date;
        using namespace std;
        using namespace std::chrono;
    
        auto nanosecondsSinceEpochTS = 1592130258959736008;
        zoned_time zt{Posix::time_zone{"CST6CDT,M3.2.0,M11.1.0"},
                      sys_time<nanoseconds>{nanoseconds{nanosecondsSinceEpochTS}}};
        cout << zt << '\n';
    }
    

    which outputs:

    2020-06-14 05:24:18.959736008 CDT
    

    Note that this only models America/Chicago back to 2007. Prior to 2007 America/Chicago had different daylight saving rules.