Search code examples
c++utc

How can I convert a UTC timestamp to local time, seconds past the hour?


I have a large data set with timestamps that are in UTC time in milliseconds. I'm synchronizing this data set with another's who has timestamps of microseconds past the hour, so I need to convert the first to local time, in seconds past the hour.

Most of the similar questions I've read on this subject get UTC time from the time() function which gets the current time.

I've tried implementing the following which was pulled from C++ Reference.

The timestamp I'm trying to convert is a double, but I'm not sure how to actually use this value.

An example ts from my data set: 1512695257869

int main ()
{
  double my_utc_ts; //value acquired from the data set

  time_t rawtime;
  struct tm * ptm;

  time ( &rawtime ); //getting current time
  //rawtime = my_utc_ts;  //this is what I tried and is wrong, and results in a nullptr

  ptm = gmtime ( &rawtime );

  puts ("Current time around the World:");
  printf ("Phoenix, AZ (U.S.) :  %2d:%02d\n", (ptm->tm_hour+MST)%24, ptm->tm_min);

  return 0;
}

After I'm able to convert it to a usable gmtime object or whatever, I need to get seconds past the hour... I think I'll be able to figure this part out if I can get the UTC timestamps to successfully convert, but I haven't thought this far ahead.

Guidance would be much appreciated. Thanks in advance.


Solution

  • After I'm able to convert it to a usable gmtime object or whatever, I need to get seconds past the hour...

    Here is how you can convert a double representing milliseconds since 1970-01-01 00:00:00 UTC to seconds past the local hour using Howard Hinnant's, free, open-source, C++11/14/17 timezone library which is based on <chrono>:

    #include "date/tz.h"
    #include <iostream>
    
    int
    main()
    {
        using namespace std::chrono;
        using namespace date;
        double my_utc_ts = 1512695257869;
        using ms = duration<double, std::milli>;
        sys_time<milliseconds> utc_ms{round<milliseconds>(ms{my_utc_ts})};
        auto loc_s = make_zoned(current_zone(), floor<seconds>(utc_ms)).get_local_time();
        auto sec_past_hour = loc_s - floor<hours>(loc_s);
        std::cout << utc_ms << " UTC\n";
        std::cout << sec_past_hour << " past the local hour\n";
    }
    

    This outputs for me:

    2017-12-08 01:07:37.869 UTC
    457s past the local hour
    

    If your local time zone is not an integral number of hours offset from UTC, the second line of output will be different for you.

    Explanation of code:

    We start with your input my_utc_ts.

    The next line creates a custom std::chrono::duration that has double as the representation and milliseconds as the precision. This type-alias is named ms.

    The next line constructs utc_ms which is a std::chrono::time_point<system_clock, milliseconds> holding 1512695257869, and represents the time point 2017-12-08 01:07:37.869 UTC. So far, no actual computation has been performed. Simply the double 1512695257869 has been cast into a type which represents an integral number of milliseconds since 1970-01-01 00:00:00 UTC.

    This line starts the computation:

    auto loc_s = make_zoned(current_zone(), floor<seconds>(utc_ms)).get_local_time();
    

    This creates a {time_zone, system_time} pair capable of mapping between UTC and a local time, using time_zone as that map. It uses current_zone() to find the computer's current time zone, and truncates the time point utc_ms from a precision of milliseconds to a precision of seconds. Finally the trailing .get_local_time() extracts the local time from this mapping, with a precision of seconds, and mapped into the current time zone. That is, loc_s is a count of seconds since 1970-01-01 00:00:00 UTC, offset by your-local-time-zone's UTC offset that was in effect at 2017-12-08 01:07:37 UTC.

    Now if you truncate loc_s to a precision of hours, and subtract that truncated time point from loc_s, you'll get the seconds past the local hour:

    auto sec_past_hour = loc_s - floor<hours>(loc_s);
    

    The entire computation is just the two lines of code above. The next two lines simply stream out utc_ms and sec_past_hour.

    Assuming that your local time zone was offset from UTC by an integral number of hours at 2017-12-08 01:07:37 UTC, you can double-check that:

    457 == 7*60 + 37
    

    Indeed, if you can assume that your local time zone is always offset from UTC by an integral number of hours, the above program can be simplified by not mapping into local time at all:

    sys_time<milliseconds> utc_ms{round<milliseconds>(ms{my_utc_ts})};
    auto utc_s = floor<seconds>(utc_ms);
    auto sec_past_hour = utc_s - floor<hours>(utc_s);
    

    The results will be identical.

    (Warning: Not all time zones are offset from UTC by an integral number of hours)

    And if your database is known to be generated with a time zone that is not the computer's current local time zone, that can be taken into account by replacing current_zone() with the IANA time zone identifier that your database was generated with, for example:

    auto loc_s = make_zoned("America/New_York", floor<seconds>(utc_ms)).get_local_time();
    

    Update

    This entire library is based on the std <chrono> library introduced with C++11. The types above utc_ms and loc_s are instantiations of std::chrono::time_point, and sec_past_hour has type std::chrono::seconds (which is itself an instantiation of std::chrono::duration).

    durations can be converted to their "representation" type using the .count() member function. For seconds, this representation type will be a signed 64 bit integer.

    For a more detailed video tutorial on <chrono>, please see this Cppcon 2016 presentation. This presentation will encourage you to avoid using the .count() member function as much as humanly possible.

    For example instead of converting sec_past_hour to a long so that you can compare it to other values of your dataset, convert other values of your dataset to std::chrono::durations so that you can compare them to sec_past_hour.

    For example:

    long other_data = 123456789;  // past the hour in microseconds
    if (microseconds{other_data} < sec_past_hour)
        // ...
    

    This snippet shows how <chrono> will take care of units conversions for you. This means you won't make mistakes like dividing by 1,000,000 when you should have multiplied, or spelling "million" with the wrong number of zeroes.