How to parse a date string into a c++11 std::chrono time_point or similar?
Asked Answered
R

4

65

Consider a historic date string of format:

Thu Jan 9 12:35:34 2014

I want to parse such a string into some kind of C++ date representation, then calculate the amount of time that has passed since then.

From the resulting duration I need access to the numbers of seconds, minutes, hours and days.

Can this be done with the new C++11 std::chrono namespace? If not, how should I go about this today?

I'm using g++-4.8.1 though presumably an answer should just target the C++11 spec.

Rainbow answered 9/1, 2014 at 13:18 Comment(5)
POSIX systems (like Linux or OSX) have a strptime that parses a string into a tm structure. Unfortunately it doesn't exist for Windows, but there are alternatives.Fishy
@JoachimPileborg Does it support the +0000 at the end, though?Speedwriting
@remyabel, actually I was mistaken. That suffix does not exist. I've updated the question.Rainbow
That's good, because the timezone suffix doesn't seem to be supported. :)Fishy
Note that chrono was not designed with calendar functionality in mind, so associating a time_point with an actual date is not at the core of its functionality. Boost tried to address this in its Date Time library which predates Chrono. Unfortunately, those two libraries don't go together as smoothly as one might wish.Rorqual
A
98
std::tm tm = {};
std::stringstream ss("Jan 9 2014 12:35:34");
ss >> std::get_time(&tm, "%b %d %Y %H:%M:%S");
auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm));

GCC prior to version 5 doesn't implement std::get_time. You should also be able to write:

std::tm tm = {};
strptime("Thu Jan 9 2014 12:35:34", "%a %b %d %Y %H:%M:%S", &tm);
auto tp = std::chrono::system_clock::from_time_t(std::mktime(&tm));
Amalgamation answered 9/1, 2014 at 13:41 Comment(9)
But get_time() is not implemented yet on gcc?Creole
@Creole it might not be, but it's in the C++ spec.Amalgamation
According to the bug 54354 at gcc this is solved in gcc 5. Little bit late for a C++11 feature.Quadroon
Just in case someone run into the same problem I just had. I needed to bzero(&tm, sizeof(std::tm)), otherwise unitialized were used, causing trouble. I'm kinda suprise I had to do this, however.Mooneye
@Mooneye std::tm t{}; // should doOtha
Is it still the case that GCC doesn't implement std::get_time()?Nucleus
@einpoklum, should work since gcc 5.0. I've tested on gcc 5.4 (that is in current stable Ubuntu 16.04).Schwann
Perfect answer, just to remind there is a bug in Visual Studio that doesn't set fail bit for some invalid inputs to std::get_time. See this question.Synergism
This might be able to do entirely in chrono starting with C++20: en.cppreference.com/w/cpp/chrono/parseFelon
L
51

New answer for old question. Rationale for the new answer: The question was edited from its original form because tools at the time would not handle exactly what was being asked. And the resulting accepted answer gives a subtly different behavior than what the original question asked for.

I'm not trying to put down the accepted answer. It's a good answer. It's just that the C API is so confusing that it is inevitable that mistakes like this will happen.

The original question was to parse "Thu, 9 Jan 2014 12:35:34 +0000". So clearly the intent was to parse a timestamp representing a UTC time. But strptime (which isn't standard C or C++, but is POSIX) does not parse the trailing UTC offset indicating this is a UTC timestamp (it will format it with %z, but not parse it).

The question was then edited to ask about "Thu Jan 9 12:35:34 2014". But the question was not edited to clarify if this was a UTC timestamp, or a timestamp in the computer's current local timezone. The accepted answer implicitly assumes the timestamp represents the computer's current local timezone because of the use of std::mktime.

std::mktime not only transforms the field type tm to the serial type time_t, it also performs an offset adjustment from the computer's local time zone to UTC.

But what if we want to parse a UTC timestamp as the original (unedited) question asked?

That can be done today using this newer, free open-source library.

#include "date/date.h"
#include <iostream>
#include <sstream>

int
main()
{
    using namespace std;
    using namespace date;
    istringstream in{"Thu, 9 Jan 2014 12:35:34 +0000"};
    sys_seconds tp;
    in >> parse("%a, %d %b %Y %T %z", tp);
}

This library can parse %z. And date::sys_seconds is just a typedef for:

std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>

The question also asks:

From the resulting duration I need access to the numbers of seconds, minutes, hours and days.

That part has remained unanswered. Here's how you do it with this library.

#include "date/date.h"
#include <chrono>
#include <iostream>
#include <sstream>

int
main()
{
    using namespace std;
    using namespace date;
    istringstream in{"Thu, 9 Jan 2014 12:35:34 +0000"};
    sys_seconds tp;
    in >> parse("%a, %d %b %Y %T %z", tp);
    auto tp_days = floor<days>(tp);
    auto hms = hh_mm_ss<seconds>{tp - tp_days};
    std::cout << "Number of days    = " << tp_days.time_since_epoch() << '\n';
    std::cout << "Number of hours   = " << hms.hours() << '\n';
    std::cout << "Number of minutes = " << hms.minutes() << '\n';
    std::cout << "Number of seconds = " << hms.seconds() << '\n';
}

floor<days> truncates the seconds-precision time_point to a days-precision time_point. If you subtract the days-precision time_point from tp, you're left with a duration that represents the time since midnight (UTC).

The type hh_mm_ss<seconds> takes any duration convertible to seconds (in this case time since midnight) and creates a {hours, minutes, seconds} field type with getters for each field. If the duration has precision finer than seconds this field type will also have a getter for the subseconds. Prior to C++17, one has to specify that finer duration as the template parameter. In C++17 and later it can be deduced:

auto hms = hh_mm_ss{tp - tp_days};

Finally, one can just print out all of these durations. This example outputs:

Number of days    = 16079d
Number of hours   = 12h
Number of minutes = 35min
Number of seconds = 34s

So 2014-01-09 is 16079 days after 1970-01-01.

Here is the full example but at milliseconds precision:

#include "date/date.h"
#include <chrono>
#include <iostream>
#include <sstream>

int
main()
{
    using namespace std;
    using namespace std::chrono;
    using namespace date;

    istringstream in{"Thu, 9 Jan 2014 12:35:34.123 +0000"};
    sys_time<milliseconds> tp;
    in >> parse("%a, %d %b %Y %T %z", tp);
    auto tp_days = floor<days>(tp);
    hh_mm_ss hms{tp - tp_days};
    std::cout << tp << '\n';
    std::cout << "Number of days         = " << tp_days.time_since_epoch() << '\n';
    std::cout << "Number of hours        = " << hms.hours() << '\n';
    std::cout << "Number of minutes      = " << hms.minutes() << '\n';
    std::cout << "Number of seconds      = " << hms.seconds() << '\n';
    std::cout << "Number of milliseconds = " << hms.subseconds() << '\n';
}

Output:

2014-01-09 12:35:34.123
Number of days         = 16079d
Number of hours        = 12h
Number of minutes      = 35min
Number of seconds      = 34s
Number of milliseconds = 123ms

This library is now part of C++20, but is in namespace std::chrono and found in the header <chrono>.

Lingenfelter answered 12/1, 2017 at 12:56 Comment(6)
The example at milliseconds precision is exactly what I was looking for. Unfortunately, neither gcc 10.2 nor clang 11.0.0 find std::chrono::parse when I precise the --std=c++20 option. Any idea when this would be available?Wink
No idea. But in the meantime, date/date.h is free, open-source and header-only.Lingenfelter
@HowardHinnant I like that you point out a nasty limitation with strptime. But it turns out this entire response is a shameless cloaked promotion of this new magical open source library which when you click through surprise happens to be authored by you. Of course user-contributed code is the heart of stack overflow, but cloaking your authorship here put me off. I'd rather have a disclosure, in response to these nasty bugs I wrote some open source that you can find "here".Lettielettish
Sorry @BillGale. I did not mean to cloak my authorship. Indeed unlike many others (not yourself) I use my full name both for my Stack Overflow ID, and my github ID. If I were trying to cloak my authorship, one would think I would be a little more clever than that. Additionally, one could interpret adding "And I wrote it!" as bragging. I wish to neither brag nor cloak. And I haven't earned a single buck off of this software, nor am I seeking to. My main goal was seeking field experience for a C++ standards proposal. Mission accomplished. Library standardized in C++20.Lingenfelter
This now compiles with gcc trunk (remove date namespace and header)Overuse
@BillGale seriously? Just say thank you to the man that standardized one of the most hair-tearing aspects of C++ coding with the great std::chrono library, and then wrote us another helper library. Anyway @HowardHinnant here's a big THANK YOU from me!Bibbye
S
1

This is rather C-ish and not as elegant of a solution as Simple's answer, but I think it might work. This answer is probably wrong but I'll leave it up so someone can post corrections.

#include <iostream>
#include <ctime>

int main ()
{
  struct tm timeinfo;
  std::string buffer = "Thu, 9 Jan 2014 12:35:00";

  if (!strptime(buffer.c_str(), "%a, %d %b %Y %T", &timeinfo))
    std::cout << "Error.";

  time_t now;
  struct tm timeinfo2;
  time(&now);
  timeinfo2 = *gmtime(&now);

  time_t seconds = difftime(mktime(&timeinfo2), mktime(&timeinfo));
  time(&seconds);
  struct tm result;
  result = *gmtime ( &seconds );
  std::cout << result.tm_sec << " " << result.tm_min << " "
            << result.tm_hour << " " << result.tm_mday;
  return 0;
}
Speedwriting answered 9/1, 2014 at 13:18 Comment(0)
M
1

Cases covered (code is below):

  • since a give date until now

    long int min0 = getMinutesSince( "2005-02-19 12:35:00" );

  • since the epoch until now

    long int min1 = getMinutesSince1970( );

  • between two date+hours (since the epoch until a given date)

    long int min0 = getMinutesSince1970Until( "2019-01-18 14:23:00" );

    long int min1 = getMinutesSince1970Until( "2019-01-18 14:27:00" );

    cout << min1 - min0 << endl;

Complete code:

#include <iostream>
#include <chrono>
#include <sstream>
#include <string>
#include <iomanip>

using namespace std;

// ------------------------------------------------
// ------------------------------------------------
long int getMinutesSince1970Until( string dateAndHour ) {

  tm tm = {};
  stringstream ss( dateAndHour );
  ss >> get_time(&tm, "%Y-%m-%d  %H:%M:%S");

  chrono::system_clock::time_point tp = chrono::system_clock::from_time_t(mktime(&tm));


  return
    chrono::duration_cast<chrono::minutes>(
                                           tp.time_since_epoch()).count();

} // ()
// ------------------------------------------------
// ------------------------------------------------
long int getMinutesSince1970() {
  chrono::system_clock::time_point now = chrono::system_clock::now();

  return
    chrono::duration_cast<chrono::minutes>( now.time_since_epoch() ).count();
} // ()

// ------------------------------------------------
// ------------------------------------------------
long int getMinutesSince( string dateAndHour ) {

  tm tm = {};
  stringstream ss( dateAndHour );
  ss >> get_time(&tm, "%Y-%m-%d  %H:%M:%S");

  chrono::system_clock::time_point then =
    chrono::system_clock::from_time_t(mktime(&tm));

  chrono::system_clock::time_point now = chrono::system_clock::now();

  return
    chrono::duration_cast<chrono::minutes>(
                                           now.time_since_epoch()-
                                           then.time_since_epoch()
                                           ).count();
} // ()


// ------------------------------------------------
// ------------------------------------------------
int main () {

  long int min = getMinutesSince1970Until( "1970-01-01 01:01:00" );

  cout << min << endl;


  long int min0 = getMinutesSince1970Until( "2019-01-18 14:23:00" );
  long int min1 = getMinutesSince1970Until( "2019-01-18 14:27:00" );

  if ( (min1 - min0) != 4 ) {
    cout << " something is wrong " << endl;
  } else {
    cout << " it appears to work !" << endl;
  }

  min0 = getMinutesSince( "1970-01-01 01:00:00" );
  min1 = getMinutesSince1970( );

  if ( (min1 - min0) != 0 ) {
    cout << " something is wrong " << endl;
  } else {
    cout << " it appears to work !" << endl;
  }

} // ()
Mesentery answered 18/1, 2019 at 13:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.