1

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';
user997112
  • 29,025
  • 43
  • 182
  • 361
  • There are too many unknowns here for me to give an answer. However if you include an example input/output pair, then I might be able to help. For example 1596185732497162000 -> 2020-07-31 08:55:32.497162000 EDT. – Howard Hinnant Jul 31 '20 at 13:01
  • @HowardHinnant Sure. I need to convert 1592130258959736008 in to 2020-06-14 05:24:18.959736008. This is Chicago time. We spoke about this previously but i need to stick to standard libraries (I can move to cpp20 in a few months). Explicitly I need to extract "2020", "06", "14", "05", "24", "18" and "959736008" as integers – user997112 Jul 31 '20 at 14:39

1 Answers1

2

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.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • Thanks Howard, really really appreciate the help. My only wonder is, has nobody really added that helper function to the standard library since your cpp20 chrono improvements?! – user997112 Jul 31 '20 at 14:42
  • 1
    In C++20 `civil_from_days` is spelled `year_month_day ymd{floor(tp)};`. Or if you already have a count of days `D` in integral form: `year_month_day ymd{sys_days{days{D}}};` – Howard Hinnant Jul 31 '20 at 14:48
  • Howard, I sincerely apologise but I edited my original comment I think whilst you were posting this answer. I got the 10am part wrong, that's UTC, it's supposed to be 5am for CDT. Understandably the code returns 10:24:18 instead of 05:24:18 – user997112 Jul 31 '20 at 15:01
  • 1
    That means your input count is nanoseconds since 1970-01-01 00:00:00 UTC and your output fields are in America/Chicago. That procedure is significantly harder. Step 1: Find the UTC offset associated with both "America/Chicago", and the UTC time of your input. This currently varies between -6h and -5h depending on the time of year (https://en.wikipedia.org/wiki/Time_in_the_United_States). Add the UTC offset to your count of nanoseconds to get "local nanoseconds". Then proceed as in the answer with the "local nanoseconds". – Howard Hinnant Jul 31 '20 at 15:06
  • 1
    If your inputs are in the range of a few years, you could precompute a table of UTC times when the offset changes, and store that along with the offset at each change. Then you could search the table with your input to discover the correct offset. – Howard Hinnant Jul 31 '20 at 15:10
  • "That means your input count is nanoseconds since 1970-01-01 00:00:00 UTC" Yes this is correct. Sorry, I should have mentioned the CDT along with Epoch. – user997112 Jul 31 '20 at 15:13
  • So my Epoch is not 1970-01-01 00:00:00 but rather 1969-12-31 19:00:00. Would a crude fix be to subtract 5 hours from the source nanos? There was no leap year/daylight hours in the last 5 hours of the year 1969, so I just re-base my timestamp by subtracting? – user997112 Jul 31 '20 at 15:16
  • 1
    If the desired UTC offset at 1592130258959736008ns after 1970-01-01 00:00:00 UTC is -5h hours, yes, just add -5h to 1592130258959736008ns. The tricky part is if you add 6 months to the input, then you'll need to subtract 6h instead. – Howard Hinnant Jul 31 '20 at 15:19
  • Didn't follow the 6 month part? These timestamps are only being used for logging and subtracting from now(CDT) (to check events occur within seconds). – user997112 Jul 31 '20 at 15:21
  • Btw, will this task be easy using C++20 or still difficult? Be good if I can simplify the code later. – user997112 Jul 31 '20 at 15:25
  • 1
    The American/Chicago UTC offset is -6h in the Winter and -5h in the Summer. It changes on the second Sunday of March and ends on the first Sunday of November at 2am. In C++20 this is a trivial amount of code. It will apply the UTC offset for you for any timezone of your choosing. – Howard Hinnant Jul 31 '20 at 15:27
  • Oooooooooooooooooo – user997112 Jul 31 '20 at 15:34
  • 1
    Howard, may I burden you for the cpp20 answer too? Upvoting every comment you make to thank you! – user997112 Jul 31 '20 at 15:40
  • Out of curiousity, does Boost cover this at all? – user997112 Jul 31 '20 at 16:08
  • 1
    Yes, boost::date_time is a complete date/time library. I believe boost lacks the full historical data contained in the IANA timezone database. This will cause the UTC offsets for some older time points to be incorrect. I don't know exactly for which timezones and how old to see these effects. – Howard Hinnant Jul 31 '20 at 16:11
  • @user997112 Updated with POSIX time zone example. – Howard Hinnant Jul 31 '20 at 19:17