Loki Astari
Loki Astari

Reputation: 264411

C++ Converting a time string to seconds from the epoch

I have a string with the following format:

2010-11-04T23:23:01Z

The Z indicates that the time is UTC.
I would rather store this as a epoch time to make comparison easy.

What is the recomended method for doing this?

Currently (after a quck search) the simplist algorithm is:

1: <Convert string to struct_tm: by manually parsing string>
2: Use mktime() to convert struct_tm to epoch time.

// Problem here is that mktime uses local time not UTC time.

Upvotes: 16

Views: 40538

Answers (10)

Howard Hinnant
Howard Hinnant

Reputation: 218750

New answer to an old question. Rationale for new answer: In case you want to use <chrono> types to solve a problem like this.

In addition to C++11/C++14, you'll need this free, open source, header-only date/time library:

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

int
main()
{
    std::istringstream is("2010-11-04T23:23:01Z");
    is.exceptions(std::ios::failbit);
    date::sys_seconds tp;
    is >> date::parse("%FT%TZ", tp);
    std::cout << "seconds from epoch is " << tp.time_since_epoch().count() << "s\n";
}

This program outputs:

seconds from epoch is 1288912981s

If the parse fails in any way, an exception will be thrown. If you would rather not throw exceptions, don't is.exceptions(std::ios::failbit);, but instead check for is.fail().

In C++20, you won't need this library any more as it becomes part of <chrono>:

#include <chrono>
#include <iostream>
#include <sstream>

int
main()
{
    std::istringstream is("2010-11-04T23:23:01Z");
    is.exceptions(std::ios::failbit);
    std::chrono::sys_seconds tp;
    is >> std::chrono::parse("%FT%TZ", tp);
    std::cout << "seconds from epoch is " << tp.time_since_epoch().count() << "s\n";
}

The latest versions of MSVC have it. It will ship in gcc-14. LLVM is in the process of implementing it.

Demo.

Upvotes: 2

Loki Astari
Loki Astari

Reputation: 264411

Using C++11 functionality we can now use streams to parse times:

The iomanip std::get_time will convert a string based on a set of format parameters and convert them into a struct tz object.

You can then use std::mktime() to convert this into an epoch value.

#include <iostream>
#include <sstream>
#include <locale>
#include <iomanip>

int main()
{
    std::tm t = {};
    std::istringstream ss("2010-11-04T23:23:01Z");

    if (ss >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S"))
    {
        std::cout << std::put_time(&t, "%c") << "\n"
                  << std::mktime(&t) << "\n";
    }
    else
    {
        std::cout << "Parse failed\n";
    }
    return 0;
}

Upvotes: 14

Mark Lakata
Mark Lakata

Reputation: 20838

Problem here is that mktime uses local time not UTC time.

Linux provides timegm which is what you want (i.e. mktime for UTC time).

Here is my solution, which I forced to only accept "Zulu" (Z timezone). Note that strptime doesn't actually seem to parse the time zone correctly, even though glib seems to have some support for that. That is why I just throw an exception if the string doesn't end in 'Z'.

static double EpochTime(const std::string& iso8601Time)
{
    struct tm t;
    if (iso8601Time.back() != 'Z') throw PBException("Non Zulu 8601 timezone not supported");
    char* ptr = strptime(iso8601Time.c_str(), "%FT%T", &t);
    if( ptr == nullptr)
    {
        throw PBException("strptime failed, can't parse " + iso8601Time);
    }
    double t2 = timegm(&t); // UTC
    if (*ptr)
    {
        double fraction = atof(ptr);
        t2 += fraction;
    }
    return t2;
}

Upvotes: 3

caf
caf

Reputation: 239041

X/Open provides a global timezone variable which indicates the number of seconds that local time is behind UTC. You can use this to adjust the output of mktime():

#define _XOPEN_SOURCE
#include <stdio.h>
#include <time.h>

/* 2010-11-04T23:23:01Z */
time_t zulu_time(const char *time_str)
{
    struct tm tm = { 0 };

    if (!strptime(time_str, "%Y-%m-%dT%H:%M:%SZ", &tm))
        return (time_t)-1;

    return mktime(&tm) - timezone;
}

Upvotes: 1

Steve Townsend
Steve Townsend

Reputation: 54148

It's not an exact dup but you will find @Cubbi's answer from here useful, I wager. This specifically assumes UTC input.

Boost also support direct conversion from ISO 8601 via boost::posix_time::from_iso_string which calls boost::date_time::parse_iso_time, here again you would just strip the trailing 'Z' and treat the TZ as implicit UTC.

#include <iostream>
#include <boost/date_time.hpp>

namespace bt = boost::posix_time;

const std::locale formats[] = {
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d %H:%M:%S")),
std::locale(std::locale::classic(),new bt::time_input_facet("%Y/%m/%d %H:%M:%S")),
std::locale(std::locale::classic(),new bt::time_input_facet("%d.%m.%Y %H:%M:%S")),
std::locale(std::locale::classic(),new bt::time_input_facet("%Y-%m-%d"))};
const size_t formats_n = sizeof(formats)/sizeof(formats[0]);

std::time_t pt_to_time_t(const bt::ptime& pt)
{
    bt::ptime timet_start(boost::gregorian::date(1970,1,1));
    bt::time_duration diff = pt - timet_start;
    return diff.ticks()/bt::time_duration::rep_type::ticks_per_second;

}
void seconds_from_epoch(const std::string& s)
{
    bt::ptime pt;
    for(size_t i=0; i<formats_n; ++i)
    {
        std::istringstream is(s);
        is.imbue(formats[i]);
        is >> pt;
        if(pt != bt::ptime()) break;
    }
    std::cout << " ptime is " << pt << '\n';
    std::cout << " seconds from epoch are " << pt_to_time_t(pt) << '\n';
}
int main()
{
    seconds_from_epoch("2004-03-21 12:45:33");
    seconds_from_epoch("2004/03/21 12:45:33");
    seconds_from_epoch("23.09.2004 04:12:21");
    seconds_from_epoch("2003-02-11");
}

Upvotes: 3

Dirk is no longer here
Dirk is no longer here

Reputation: 368241

What's wrong with strptime() ?

And on Linux, you even get the 'seconds east of UTC' field relieving you from any need to parse:

#define _XOPEN_SOURCE
#include <iostream>
#include <time.h>

int main(void) {

    const char *timestr = "2010-11-04T23:23:01Z";

    struct tm t;
    strptime(timestr, "%Y-%m-%dT%H:%M:%SZ", &t);

    char buf[128];
    strftime(buf, sizeof(buf), "%d %b %Y %H:%M:%S", &t);

    std::cout << timestr << " -> " << buf << std::endl;

    std::cout << "Seconds east of UTC " << t.tm_gmtoff << std::endl;
}   

which for me yields

/tmp$ g++ -o my my.cpp 
/tmp$ ./my
2010-11-04T23:23:01Z -> 04 Nov 2010 23:23:01
Seconds east of UTC 140085769590024

Upvotes: 1

casablanca
casablanca

Reputation: 70701

Problem here is that mktime uses local time not UTC time.

How about just computing the time difference between UTC and local time, then adding it to the value returned by mktime?

time_t local = time(NULL),
       utc   = mktime(gmtime(&local));
int    diff  = utc - local;

Upvotes: 1

Kirill V. Lyadvinsky
Kirill V. Lyadvinsky

Reputation: 99585

This is ISO8601 format. You can use strptime function to parse it with %FT%T%z argument. It is not a part of the C++ Standard though you can use open source implementation of it (this, for instance).

Upvotes: 6

Justin Ethier
Justin Ethier

Reputation: 134157

You can use a function such as strptime to convert a string to a struct tm, instead of parsing it manually.

Upvotes: 5

Yippie-Ki-Yay
Yippie-Ki-Yay

Reputation: 22814

You could utilize the boost::date_time and write a small manual parser (probably regexp-based) for your strings.

Upvotes: 1

Related Questions