Home > OS >  Convert timestamp string into local time
Convert timestamp string into local time

Time:10-20

How to convert timestamp string, e.g. "1997-07-16T19:20:30.45 01:00" into UTC time. The result of conversion should be timespec structure as in utimensat input arguments.

timespec get_local_time(const char* ts);

P.S. I need solution using either standard Linux/C/C facilities (whatever that means) or Boost C library.

CodePudding user response:

Assumption: You want the " 01:00" to be subtracted from the "1997-07-16T19:20:30.45" to get a UTC timestamp and then convert that into a timespec.

Here is a C 20 solution that will automatically handle the centisecond precision and the [ /-]hh:mm UTC offset for you:

#include <chrono>
#include <ctime>
#include <sstream>

std::timespec
get_local_time(const char* ts)
{
    using namespace std;
    using namespace chrono;

    istringstream in{ts};
    in.exceptions(ios::failbit);
    sys_time<nanoseconds> tp;
    in >> parse("%FT%T%Ez", tp);
    auto tps = floor<seconds>(tp);
    return {.tv_sec = tps.time_since_epoch().count(),
            .tv_nsec = (tp - tps).count()};
}

When used like this:

auto r = get_local_time("1997-07-16T19:20:30.45 01:00");
std::cout << '{' << r.tv_sec << ", " << r.tv_nsec << "}\n";

The result is:

{869077230, 450000000}
  • std::chrono::parse will subtract the /-hh:mm UTC offset from the parsed local value to obtain a UTC timestamp (to up to nanosecond precision).

  • If the input has precision seconds, this code will handle it. If the precision is as fine as nanoseconds, this code will handle it.

  • If the input does not conform to this syntax, an exception will be thrown. If this is not desired, remove in.exceptions(ios::failbit);, and then you must check in.fail() to see if the parse failed.

  • This code will also handle dates prior to the UTC epoch of 1970-01-01 by putting a negative value into .tv_sec, and a positive value ([0, 999'999'999]) into .tv_nsec. Note that handling pre-epoch dates is normally outside of the timespec specification, and so most C utilities will not handle such a timespec value.

  • If you can not use C 20, or if your vendor has yet to implement this part of C 20, there exists a header-only library which implements this part of C 20, and works with C 11/14/17. I have not linked to it here as it is not in the set: "standard Linux/C/C facilities (whatever that means) or Boost C library". I'm happy to add a link if requested.

CodePudding user response:

For comparison, here's how you could do this in mostly-standard C. It's somewhat cumbersome, because C's date/time support is still rather fragmented, unlike the much more complete support which C has, as illustrated in Howard Hinnant's answer. (Also, two of the functions I'm going to use are not specified by the C Standard, although they're present on many/most systems.)

If you have the semistandard strptime function, and if you didn't care about subseconds and explicit time zones, it would be relatively straightforward. strptime is a (partial) inverse of strftime, parsing a time string under control of a format specifier, and constructing a struct tm. Then you can call mktime to turn that struct tm into a time_t. Then you can use the time_t to populate a struct timespec.

char *inpstr = "1997-07-16T19:20:30.45 01:00";

struct tm tm;
memset(&tm, 0, sizeof(tm));
char *p = strptime(inpstr, "%Y-%m-%dT%H:%M:%S", &tm);
if(p == NULL) {
    printf("strptime failed\n");
    exit(1);
}

tm.tm_isdst = -1;
time_t t = mktime(&tm);
if(t == -1) {
    printf("mktime failed\n");
    exit(1);
}

struct timespec ts;
ts.tv_sec = t;
ts.tv_nsec = 0;

printf("%ld %ld\n", ts.tv_sec, ts.tv_nsec);
printf("%s", ctime(&ts.tv_sec));
printf("rest = %s\n", p);

In my time zone, currently UTC 4, this prints

869095230 0
Wed Jul 16 19:20:30 1997
rest = .45 01:00

But you did have subsecond information, and you did have an explicit time zone, and there's no built-in support for those in any of the basic C time-conversion functions, so you have to do things "by hand". Here's one way to do it. I'm going to use sscanf to separate out the year, month, day, hour, minute, second, and other components. I'm going to use those components to populate a struct tm, then use the semistandard timegm function to convert them straight to a UTC time. (That is, I temporarily assume that the HH:MM:SS part was UTC.) Then I'm going to manually correct for the time zone. Finally, I'm going to populate the tv_nsec field of the struct timesec with the subsecond information I extracted back in the beginning.

int y, m, d;
int H, M, S;
int ss;     /* subsec */
char zs;    /* zone sign */
int zh, zm; /* zone hours, minutes */
int r = sscanf(inpstr, "%d-%d-%dT%d:%d:%d.-%c%d:%d",
            &y, &m, &d, &H, &M, &S, &ss, &zs, &zh, &zm);
if(r != 10 || (zs != ' ' && zs != '-')) {
    printf("parse failed\n");
    exit(1);
}

struct tm tm;
memset(&tm, 0, sizeof(tm));
tm.tm_year = y - 1900;
tm.tm_mon = m - 1;
tm.tm_mday = d;
tm.tm_hour = H;
tm.tm_min = M;
tm.tm_sec = S;
time_t t = timegm(&tm);

if(t == -1) {
    printf("timegm failed\n");
    exit(1);
}

long int z = ((zh * 60L)   zm) * 60;

if(zs == ' ')    /* East of Greenwich */
     t -= z;
else t  = z;

struct timespec ts;
ts.tv_sec = t;
ts.tv_nsec = ss * (1000000000 / 100);

printf("%ld %ld\n", ts.tv_sec, ts.tv_nsec);
printf("%s", ctime(&ts.tv_sec));
printf(".ld\n", ts.tv_nsec / (1000000000 / 100));

For me this prints

869077230 450000000
Wed Jul 16 14:20:30 1997
.45

The time zone and subsecond information have been honored.

This code makes no special provision for dates prior to 1970. I think it will work if mktime/timegm work.

As mentioned, two of these functions — strptime and timegm — are not specified by the ANSI/ISO C Standard and are therefore not guaranteed to be available everywhere.

  • Related