5

I would like to know time as seconds since the epoch. Notably, I would not like it to matter where the machine doing the conversion is, the time zone string should be enough.

I have this test program, pt.cc:

#include <assert.h>
#include <errno.h>
#include <iostream>
#include <stdio.h>
#include <string>
#include <string.h>
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE
#endif
#include <time.h>

using namespace std;  // To be brief, don't do this in real life.

int main(int argc, char* argv[]) {
    (void)argc; (void)argv;   // Skip compile warning.

    // I expect both of these to transform to 1440671500.
    cout << "1440671500 expected" << endl;
    const char utc_example[] = "2015-08-27T11:31:40+0100";
    struct tm tm;
    memset(&tm, 0, sizeof(struct tm));
    char* end = strptime(utc_example, "%Y-%m-%dT%H:%M:%S%z", &tm);
    assert(end);
    assert(*end == '\0');
    time_t seconds_since_epoch = mktime(&tm);
    cout << "utc example: " << seconds_since_epoch << "  or maybe  "
         << seconds_since_epoch - tm.tm_gmtoff + (tm.tm_isdst ? 3600 : 0) << endl;

    const char tz_example[] = "2015-08-27T10:31:40Z";
    memset(&tm, 0, sizeof(struct tm));
    end = strptime(tz_example, "%Y-%m-%dT%H:%M:%S%nZ", &tm);
    assert(end);
    assert(*end == '\0');
    seconds_since_epoch = mktime(&tm);
    cout << " tz example: " << seconds_since_epoch << "  or maybe  "
         << seconds_since_epoch - tm.tm_gmtoff + (tm.tm_isdst ? 3600 : 0) << endl;

    return 0;
}

This is the output:

jeff@birdsong:tmp $ clang++ -ggdb3 -Wall -Wextra -std=c++14 pt.cc -o pt
jeff@birdsong:tmp $ ./pt
1440671500 expected
utc example: 1440671500  or maybe  1440667900
 tz example: 1440667900  or maybe  1440664300
jeff@birdsong:tmp $ TZ=America/New_York ./pt
1440671500 expected
utc example: 1440693100  or maybe  1440711100
 tz example: 1440689500  or maybe  1440707500
jeff@birdsong:tmp $ TZ=Europe/London ./pt
1440671500 expected
utc example: 1440675100  or maybe  1440675100
 tz example: 1440671500  or maybe  1440671500
jeff@birdsong:tmp $ 

Note how the return value of mktime() changes depending on the ambient time zone. The man page entry for mktime() suggests it interprets the broken down time as local time. So I tried subtracting the GMT offset and compensating for timezone in case it was ignoring those values (the "or maybe" value).

Any tips on how to do this correctly? (Should it matter, I only need this to work on linux.)

jma
  • 3,580
  • 6
  • 40
  • 60
  • 1
    You could try http://howardhinnant.github.io/date_v2.html with http://howardhinnant.github.io/tz.html - I believe it has all the functionality you need. – Rostislav Oct 29 '15 at 18:04

4 Answers4

12

This answer uses this date/time library:

http://howardhinnant.github.io/date/date.html

The approach taken here is to completely circumvent the C date/time API. Personally I find the C approach a bit confusing, cumbersome, and somewhat dangerous.

That being said, the parsing and formatting facilities in my date/time library are non-existent. I am foreseeing that such facilities may become a separate library, layered on top of my library in the future.

In the meantime, it is not difficult to roll your own parsing for this particular problem. Here is how:

#include "chrono_io.h"
#include "date.h"
#include <iostream>
#include <string>
#include <sstream>

using second_point = std::chrono::time_point<std::chrono::system_clock,
                                             std::chrono::seconds>;

std::chrono::minutes
parse_offset(std::istream& in)
{
    using namespace std::chrono;
    char c;
    in >> c;
    minutes result = 10*hours{c - '0'};
    in >> c;
    result += hours{c - '0'};
    in >> c;
    result += 10*minutes{c - '0'};
    in >> c;
    result += minutes{c - '0'};
    return result;
}

second_point
parse(const std::string& str)
{
    std::istringstream in(str);
    in.exceptions(std::ios::failbit | std::ios::badbit);
    int yi, mi, di;
    char dash;
    // check dash if you're picky
    in >> yi >> dash >> mi >> dash >> di;
    using namespace date;
    auto ymd = year{yi}/mi/di;
    // check ymd.ok() if you're picky
    char T;
    in >> T;
    // check T if you're picky
    int hi, si;
    char colon;
    in >> hi >> colon >> mi >> colon >> si;
    // check colon if you're picky
    using namespace std::chrono;
    auto h = hours{hi};
    auto m = minutes{mi};
    auto s = seconds{si};
    second_point result = sys_days{ymd} + h + m + s;
    char f;
    in >> f;
    if (f == '+')
        result -= parse_offset(in);
    else if (f == '-')
        result += parse_offset(in);
    else
        ;// check f == 'Z' if you're picky
    return result;
}

int
main()
{
    using namespace date;
    std::cout << parse("2015-08-27T11:31:40+0100").time_since_epoch() << '\n';
    std::cout << parse("2015-08-27T10:31:40Z").time_since_epoch() << '\n';
}

To be completely upfront, this solution is making major use of std::istringstream, std::chrono, and actually just a very small part of it is my date library.

There are several design choices I've made, which you may choose not to (there are so many options when parsing). For example, I chose to throw an exception if there are any parsing errors, and I chose to not be picky about checking delimiters such as - and : (mainly for brevity reasons).

The code is relatively self-explanatory. And as you state in your question, local timezones are not (and should not be) part of the solution. The chrono library is used to manage arithmetic among hours, minutes and seconds. And my date library is used to handle the conversion of a year/month/day into a chrono::time_point that has a precision of days.

With all this arithmetic handled for you, you can concentrate just on the parsing of integers and characters. It is straight forward to add more checking to this example, and do whatever you want to for errors.

This example outputs:

1440671500s
1440671500s

Update

I have since added parsing abilities to "date.h" and the above parse function can now be more simply written:

date::sys_seconds
parse(const std::string& str)
{
    std::istringstream in(str);
    date::sys_seconds tp;
    in >> date::parse("%FT%TZ", tp);
    if (in.fail())
    {
        in.clear();
        in.str(str);
        in >> date::parse("%FT%T%z", tp);
    }
    return tp;
}

int
main()
{
    using namespace date;
    std::cout << parse("2015-08-27T11:31:40+0100").time_since_epoch() << '\n';
    std::cout << parse("2015-08-27T10:31:40Z").time_since_epoch() << '\n';
}

And returns the identical results.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • This is a good answer. I accepted Greg's only because using cctz provides a simpler solution for me, but I think they would both have worked just fine for me in the long run. Many thanks! – jma Nov 12 '15 at 17:33
  • Is there a summary what kind of format strings `date::parse` accepts? – bluenote10 Nov 14 '18 at 14:20
  • 1
    @bluenote10: Yes, [here is the parse format string documentation](https://howardhinnant.github.io/date/date.html#from_stream_formatting). – Howard Hinnant Nov 14 '18 at 16:21
3

Here's an answer that does what you want using Google's https://github.com/google/cctz

#include <chrono>
#include <iostream>
#include <string>

#include "src/cctz.h"

using namespace std;

int main(int argc, char* argv[]) {
  const char kFmt[] = "%Y-%m-%dT%H:%M:%S%Ez";

  // I expect both of these to transform to 1440671500.
  const char utc_example[] = "2015-08-27T11:31:40+0100";
  const char tz_example[] = "2015-08-27T10:31:40Z";
  cout << "1440671500 expected" << endl;

  // Required by cctz::Parse(). Only used if the formatted
  // time does not include offset info.
  const auto utc = cctz::UTCTimeZone();

  std::chrono::system_clock::time_point tp;
  if (!Parse(kFmt, utc_example, utc, &tp)) return -1;
  cout << "utc example: " << std::chrono::system_clock::to_time_t(tp) << "\n";

  if (!Parse(kFmt, tz_example, utc, &tp)) return -1;
  cout << " tz example: " << std::chrono::system_clock::to_time_t(tp) << "\n";

  return 0;
}

The output is:

1440671500 expected
utc example: 1440671500
 tz example: 1440671500

Note that other answers that involved adding/subtracting offsets from, say, a time_t are using a technique called "epoch shifting" and it doesn't actually work. I explain why at 12:30 in this talk from CppCon: https://youtu.be/2rnIHsqABfM?t=12m30s

  • This is great. It works, and it's simple. Thanks. The video link is good, the red box at 10:30 summarizes precisely the problem I was working around. – jma Nov 12 '15 at 17:28
  • For future readers of this thread, cctz depends on bazel to build, which caused me a few headaches (conversation ongoing with the bazel team). It was easy to work around with a makefile. If anyone wants to use this before the bazel issues get sorted, feel free to clone from here: https://github.com/Jellybooks/cctz . But still see the official repository in case I haven't updated something when you read this. – jma Nov 12 '15 at 17:31
0

Here is a simple function that will return the number-of-seconds-since-the-start-of-1970, in UTC:

#include <time.h>
#include <sys/time.h>

time_t GetSecondsSinceNewYearsDay1970UTC()
{
    struct timeval tv;
    if (gettimeofday(&tv, NULL) != 0) perror("doh!");
    return tv.tv_sec;
}

Should you prefer a value reflecting the computer's local time zone rather than the global UTC value, here's a second function that returns that.

time_t GetSecondsSinceNewYearsDay1970LocalTimeZone()
{
   const time_t utcTime = GetSecondsSinceNewYearsDay1970UTC();
   time_t now = time(NULL);
   time_t localTime = utcTime;  // will be adjusted below
   struct tm gmtm;
   struct tm * tm = gmtime_r(&now, &gmtm);
   if (tm)
   {
      localTime += (now-mktime(tm));
      if (tm->tm_isdst>0) localTime += (60*60);  // add an hour for DST
   }
   return localTime;
}
Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Jeremy, you're starting with broken down time instead of with ISO 8601 time. The problem is about parsing ISO 8601 and making sure timezone is properly taken into account. – jma Nov 09 '15 at 07:15
0

Before calling mktime() on an object returned by strptime(), set tm->tm_isdst to a negative number to make the library determine whether DST was in effect at that time. Note that timezone offsets to strptime() are a non-standard extension. (Also note that using a struct name as a variable name is bad style in C++, as it creates ambiguity about what tm is.)

Davislor
  • 14,674
  • 2
  • 34
  • 49
  • Thanks. I don't see how this works in my case. Did you try it on the above code? Cf. also the section on epoch shifting in Greg's video. – jma Nov 12 '15 at 17:32