How to convert std::chrono::time_point to std::tm without using time_t?
Asked Answered
P

3

19

I would like to print or extract year/month/day values.

I don't want to use time_t because of the year 2038 problem, but all examples I found on the Internet use it to convert time_point to tm.

Is there a simple way to convert from time_point to tm (preferably without boost)?


An implementation like timesub from libc would be my last resort: http://www.opensource.apple.com/source/Libc/Libc-262/stdtime/localtime.c


Edit: After reading the suggested links and doing some more research, I came to the following conclusion.

  1. Using time_t where it is 64bit long is ok (for most purposes).
  2. Using Boost.Date_Time for portable code.

It is noteworthy that Boost.Date_Time can be a header-only library. Source: http://www.boost.org/doc/libs/1_53_0/more/getting_started/unix-variants.html#header-only-libraries

Pneumodynamics answered 27/5, 2013 at 12:31 Comment(3)
A related question What should we do to prepare for 2038?Dromedary
A second problem with the method of going via time_t is that all of the standard functions for converting time_t to tm are not thread-safe.Sara
time_t t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());Bawdy
F
31

Answer updated with better algorithms, link to detailed description of the algorithms, and complete conversion to std::tm.


I would like to print or extract year/month/day values. Is there a simple way to convert from time_point to tm (preferably without boost)?

The first thing to note is that std::chrono::time_point is templated not only on duration, but also on the clock. The clock implies an epoch. And different clocks can have different epochs.

For example, on my system, std::chrono::high_resolution_clock and std::chrono::steady_clock have an epoch of: whenever the computer booted up. If you don't know what time the computer booted up, there is no way to convert that time_point to any calendar system.

That being said, you were probably talking just about std::chrono::system_clock::time_point, as this time_point, and only this time_point, is required to have a deterministic relationship with the civil (gregorian) calendar.

As it turns out, every implementation of std::chrono::system_clock I'm aware of is using unix time. This has an epoch of New Years 1970 neglecting leap seconds.

This isn't guaranteed by the standard. However you can take advantage of this fact if you want to with the following formulas found at:

chrono-Compatible Low-Level Date Algorithms

First off, warning, I'm using the latest C++1y draft, which includes great new constexpr tools. If you need to back off some of the constexpr attributes for your compiler, just do so.

Given the algorithms found at the above link, you can can convert a std::chrono::time_point<std::chrono::system_clock, Duration> to a std::tm, without using time_t with the following function:

template <class Duration>
std::tm
make_utc_tm(std::chrono::time_point<std::chrono::system_clock, Duration> tp)
{
    using namespace std;
    using namespace std::chrono;
    typedef duration<int, ratio_multiply<hours::period, ratio<24>>> days;
    // t is time duration since 1970-01-01
    Duration t = tp.time_since_epoch();
    // d is days since 1970-01-01
    days d = round_down<days>(t);
    // t is now time duration since midnight of day d
    t -= d;
    // break d down into year/month/day
    int year;
    unsigned month;
    unsigned day;
    std::tie(year, month, day) = civil_from_days(d.count());
    // start filling in the tm with calendar info
    std::tm tm = {0};
    tm.tm_year = year - 1900;
    tm.tm_mon = month - 1;
    tm.tm_mday = day;
    tm.tm_wday = weekday_from_days(d.count());
    tm.tm_yday = d.count() - days_from_civil(year, 1, 1);
    // Fill in the time
    tm.tm_hour = duration_cast<hours>(t).count();
    t -= hours(tm.tm_hour);
    tm.tm_min = duration_cast<minutes>(t).count();
    t -= minutes(tm.tm_min);
    tm.tm_sec = duration_cast<seconds>(t).count();
    return tm;
}

Also note that the std::chrono::system_clock::time_point on all existing implementations is a duration in the UTC (neglecting leap seconds) time zone. If you want to convert the time_point using another timezone, you will need to add/subtract the duration offset of the timezone to the std::chrono::system_clock::time_point prior to converting it to a precision of days. And if you further want to take leap seconds into account, then adjust by the appropriate number of seconds prior to truncation to days using this table, and the knowledge that unix time is aligned with UTC now.

This function can be verified with:

#include <iostream>
#include <iomanip>

void
print_tm(const std::tm& tm)
{
    using namespace std;
    cout << tm.tm_year+1900;
    char fill = cout.fill();
    cout << setfill('0');
    cout << '-' << setw(2) << tm.tm_mon+1;
    cout << '-' << setw(2) << tm.tm_mday;
    cout << ' ';
    switch (tm.tm_wday)
    {
    case 0:
        cout << "Sun";
        break;
    case 1:
        cout << "Mon";
        break;
    case 2:
        cout << "Tue";
        break;
    case 3:
        cout << "Wed";
        break;
    case 4:
        cout << "Thu";
        break;
    case 5:
        cout << "Fri";
        break;
    case 6:
        cout << "Sat";
        break;
    }
    cout << ' ';
    cout << ' ' << setw(2) << tm.tm_hour;
    cout << ':' << setw(2) << tm.tm_min;
    cout << ':' << setw(2) << tm.tm_sec << " UTC.";
    cout << setfill(fill);
    cout << "  This is " << tm.tm_yday << " days since Jan 1\n";
}

int
main()
{
    print_tm(make_utc_tm(std::chrono::system_clock::now()));
}

Which for me currently prints out:

2013-09-15 Sun 18:16:50 UTC. This is 257 days since Jan 1

In case chrono-Compatible Low-Level Date Algorithms goes offline, or gets moved, here are the algorithms used in make_utc_tm. There are in-depth explanations of these algorithms at the above link. They are well-tested, and have an extraordinarily large range of validity.

// Returns number of days since civil 1970-01-01.  Negative values indicate
//    days prior to 1970-01-01.
// Preconditions:  y-m-d represents a date in the civil (Gregorian) calendar
//                 m is in [1, 12]
//                 d is in [1, last_day_of_month(y, m)]
//                 y is "approximately" in
//                   [numeric_limits<Int>::min()/366, numeric_limits<Int>::max()/366]
//                 Exact range of validity is:
//                 [civil_from_days(numeric_limits<Int>::min()),
//                  civil_from_days(numeric_limits<Int>::max()-719468)]
template <class Int>
constexpr
Int
days_from_civil(Int y, unsigned m, unsigned d) noexcept
{
    static_assert(std::numeric_limits<unsigned>::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits<Int>::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    y -= m <= 2;
    const Int era = (y >= 0 ? y : y-399) / 400;
    const unsigned yoe = static_cast<unsigned>(y - era * 400);      // [0, 399]
    const unsigned doy = (153*(m + (m > 2 ? -3 : 9)) + 2)/5 + d-1;  // [0, 365]
    const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;         // [0, 146096]
    return era * 146097 + static_cast<Int>(doe) - 719468;
}

// Returns year/month/day triple in civil calendar
// Preconditions:  z is number of days since 1970-01-01 and is in the range:
//                   [numeric_limits<Int>::min(), numeric_limits<Int>::max()-719468].
template <class Int>
constexpr
std::tuple<Int, unsigned, unsigned>
civil_from_days(Int z) noexcept
{
    static_assert(std::numeric_limits<unsigned>::digits >= 18,
             "This algorithm has not been ported to a 16 bit unsigned integer");
    static_assert(std::numeric_limits<Int>::digits >= 20,
             "This algorithm has not been ported to a 16 bit signed integer");
    z += 719468;
    const Int era = (z >= 0 ? z : z - 146096) / 146097;
    const unsigned doe = static_cast<unsigned>(z - era * 146097);          // [0, 146096]
    const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;  // [0, 399]
    const Int y = static_cast<Int>(yoe) + era * 400;
    const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);                // [0, 365]
    const unsigned mp = (5*doy + 2)/153;                                   // [0, 11]
    const unsigned d = doy - (153*mp+2)/5 + 1;                             // [1, 31]
    const unsigned m = mp + (mp < 10 ? 3 : -9);                            // [1, 12]
    return std::tuple<Int, unsigned, unsigned>(y + (m <= 2), m, d);
}

template <class Int>
constexpr
unsigned
weekday_from_days(Int z) noexcept
{
    return static_cast<unsigned>(z >= -4 ? (z+4) % 7 : (z+5) % 7 + 6);
}

template <class To, class Rep, class Period>
To
round_down(const std::chrono::duration<Rep, Period>& d)
{
    To t = std::chrono::duration_cast<To>(d);
    if (t > d)
        --t;
    return t;
}

Update

More recently I have wrapped the above algorithms up into a freely available date/time library documented and available here. This library makes it very easy to extract a year/month/day from std::system_clock::time_point, and even hours:minutes:seconds:fractional-seconds. And all without going through time_t.

Here is a simple program using the above header-only library to print out the current date and time in the UTC timezone, to the precision of whatever system_clock::time_point offers (in this case microseconds):

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


int
main()
{
    using namespace date;
    using namespace std;
    using namespace std::chrono;
    auto const now = system_clock::now();
    auto const dp = time_point_cast<days>(now);
    auto const date = year_month_day(dp);
    auto const time = make_time(now-dp);
    cout << date << ' ' << time << " UTC\n";
}

Which just output for me:

2015-05-19 15:03:47.754002 UTC

This library effectively turns std::chrono::system_clock::time_point into an easy-to-use date-time type.

Ferrante answered 28/5, 2013 at 5:8 Comment(2)
You should use std::chrono::system_clock::to_time_t. In MSVC++ time_t is defined as long long, so we should be good on Y2038 type bugs for a long long time from now. Good point about the epochs/steady_clock though.Bawdy
Today's answer on MSVC++ is to use the C++20 chrono facilities, which completely obsolete time_t and the rest of the C timing API. A decade ago the answer was obviously different.Ferrante
I
4

There's nothing to support calendar dates in the Standard Library apart from the C library functions based on time_t.

Options are, in order of my preference:

  • Boost.Date_Time, which you say you'd prefer to avoid
  • Some other third-party date/time library (I don't have an recommendations since I'd use Boost)
  • Modify an open-source implementation of gmtime()
  • Use this algorithm, after checking that it's correct.
Internationalism answered 27/5, 2013 at 12:50 Comment(0)
I
1

I have used Howard Hinnant's date library to write a function that converts from time_point to struct tm:

template <typename Clock, typename Duration>
std::tm to_calendar_time(std::chrono::time_point<Clock, Duration> tp)
{
    using namespace date;
    auto date = floor<days>(tp);
    auto ymd = year_month_day(date);
    auto weekday = year_month_weekday(date).weekday_indexed().weekday();
    auto tod = make_time(tp - date);
    days daysSinceJan1 = date - sys_days(ymd.year()/1/1);

    std::tm result;
    std::memset(&result, 0, sizeof(result));
    result.tm_sec   = tod.seconds().count();
    result.tm_min   = tod.minutes().count();
    result.tm_hour  = tod.hours().count();
    result.tm_mday  = unsigned(ymd.day());
    result.tm_mon   = unsigned(ymd.month()) - 1u; // Zero-based!
    result.tm_year  = int(ymd.year()) - 1900;
    result.tm_wday  = unsigned(weekday);
    result.tm_yday  = daysSinceJan1.count();
    result.tm_isdst = -1; // Information not available
    return result;
}

This effectively bypasses time_t with its lurking Y2038 problem on 32-bit systems. This function has been contributed to this GitHub wiki, where I hope others will contribute other useful examples and recipes.

Interpol answered 31/7, 2015 at 22:8 Comment(2)
Where is the timezone? I can't take anything without an expclicit timezone or locale serious anymore.Chassepot
@Lothar, I had simply assumed that the time was in local time.Interpol

© 2022 - 2024 — McMap. All rights reserved.