如何在不使用time_t的情况下将std :: chrono :: time_point转换为std :: tm?

19
我想打印或提取年/月/日值。
我不想使用 time_t,因为会有2038年问题,但我在互联网上找到的所有示例都使用它将 time_point 转换为 tm
有没有一种简单的方法可以从 time_point 转换为 tm(最好不使用 boost)?
如果必要,像 libctimesub 实现将是我的最后选择。 http://www.opensource.apple.com/source/Libc/Libc-262/stdtime/localtime.c
编辑:阅读了建议的链接并进行了更多的研究后,我得出了以下结论。
  1. 在64位长的地方使用 time_t 是可以的(对于大多数目的而言)。
  2. 使用 Boost.Date_Time 进行可移植代码。
值得注意的是,Boost.Date_Time 可以是一个仅包含头文件的库。来源: http://www.boost.org/doc/libs/1_53_0/more/getting_started/unix-variants.html#header-only-libraries

一个相关的问题我们应该如何为2038年做准备? - Ali
使用“time_t”方法的第二个问题是,将“time_t”转换为“tm”的所有标准函数都不是线程安全的。 - M.M
time_t t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - bobobobo
3个回答

31

更新了更好的算法,链接到算法的详细描述,并完全转换为 std::tm


我想要打印或提取年/月/日值。是否有一种简单的方法将时间点转换为 tm(最好不使用 boost)?

首先要注意的是,std::chrono::time_point 不仅以 duration 为模板参数,还以时钟为模板参数。时钟意味着一个纪元。不同的时钟可以有不同的纪元。

例如,在我的系统上,std::chrono::high_resolution_clockstd::chrono::steady_clock 的纪元是:计算机启动时的时间。如果您不知道计算机启动的时间,就无法将该 time_point 转换为任何日历系统。

话虽如此,您可能只是在谈论 std::chrono::system_clock::time_point,因为只有这个 time_point,而且只有这个 time_point,才需要与民用(公历)日历具有确定的关系。

事实证明,我所知道的每个 std::chrono::system_clock 实现都使用 Unix 时间。忽略闰秒,这有一个 1970 年元旦的纪元。

这并不是标准保证的。但是,如果您想要利用这一点,则可以使用以下公式在:

Chrono-Compatible Low-Level Date Algorithms

首先,警告,我使用最新的 C++1y 草案,其中包括很棒的新 constexpr 工具。如果您需要为您的编译器取消一些 constexpr 属性,请这样做。

通过上面链接中找到的算法,您可以将 std::chrono::time_point<std::chrono::system_clock, Duration> 转换为 std::tm,而无需使用 time_t,使用下面的函数即可:

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;
}

请注意,所有现有实现中的std::chrono::system_clock::time_point都是UTC(忽略闰秒)时区中的持续时间。如果您想使用另一个时区转换time_point,则需要在将其转换为days的精度之前向std::chrono::system_clock::time_point添加/减去时区的持续时间偏移量。如果您还想考虑闰秒,则在截断到days之前根据此表格调整适当数量的秒,并且要知道unix时间与UTC当前对齐。
可以通过以下方式验证此函数:
#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()));
}

目前对我来说,输出如下:

2013年9月15日星期日UTC 18:16:50。这距离1月1日已经过去257天

如果chrono-Compatible Low-Level Date Algorithms 离线或者被移动了,这里是make_utc_tm中使用的算法。在上述链接中有关于这些算法的深入解释。它们经过了充分的测试,并且拥有非常广阔的有效范围。

// 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;
}

更新

最近,我将上述算法封装为一个免费可用的日期/时间库,并在这里进行了文档记录和发布。该库使得从std::system_clock::time_point中提取年/月/日,甚至是时:分:秒:毫秒等信息变得非常容易,而且无需经过time_t

下面是一个简单的程序,使用上述头文件库将当前日期和时间以UTC时区的形式打印出来,精确到system_clock::time_point提供的精度(在本例中为微秒):

#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";
}

它只是为我输出:

2015-05-19 15:03:47.754002 UTC

这个库有效地将std::chrono::system_clock::time_point转换为易于使用的日期时间类型。


你应该使用 std::chrono::system_clock::to_time_t。在 MSVC++ 中,time_t 被定义为 long long,因此我们从现在开始很长一段时间内都不会遇到 Y2038 类型错误的问题。不过关于纪元/steady_clock 的观点也很好。 - bobobobo
1
今天在MSVC++上的答案是使用C++20中的chrono库,这个库完全废除了time_t和其他C时间API。十年前的答案显然是不同的。 - Howard Hinnant

4

标准库中除了基于time_t的C库函数外,没有支持日历日期的功能。

以下是按我偏好顺序的选项:

  • Boost.Date_Time,但你说你想避免使用它
  • 使用其他第三方日期/时间库(我没有推荐的,因为我会使用Boost)
  • 修改一个开源的gmtime()实现
  • 使用这个算法之前要检查一下它是否正确。

1

我使用Howard Hinnant的日期库编写了一个将time_point转换为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;
}

这有效地绕过了32位系统上潜在的Y2038问题,不再使用time_t。该函数已被贡献到GitHub wiki中,我希望其他人能够贡献其他有用的示例和方法。


时区在哪里?如果没有明确的时区或地区设置,我就不能认真对待任何事情了。 - Lothar
@Lothar,我只是假设时间是本地时间。 - Emile Cormier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接