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

拈花ヽ惹草 提交于 2019-12-03 05:20:49

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.

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.
Emile Cormier

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!