Как преобразовать std:: chrono:: time_point в std:: tm без использования time_t?

Я хотел бы печатать или извлекать значения года/месяца/дня.

Я не хочу использовать time_t из-за проблемы 2038 года, но все примеры, которые я нашел в Интернете, используют для преобразования time_point в tm.

Есть ли простой способ конвертировать из time_point в tm (желательно без boost)?


Реализация, такая как timesub из libc, будет моей последней целью: http://www.opensource.apple.com/source/Libc/Libc-262/stdtime/localtime.c


Изменить:. Прочитав предлагаемые ссылки и сделав еще несколько исследований, я пришел к следующему выводу.

  • Использование time_t, где оно имеет длину 64 бит, подходит (для большинства целей).
  • Использование Boost.Date_Time для переносимого кода.

Следует отметить, что Boost.Date_Time может быть только библиотекой заголовка. Источник: http://www.boost.org/doc/libs/1_53_0/more/getting_started/unix-variants.html#header-only-libraries

Ответ 1

Ответ обновляется с помощью лучших алгоритмов, ссылки на подробное описание алгоритмов и полное преобразование в std::tm.


Я хотел бы распечатать или извлечь данные за год/месяц/день. Есть ли простой способ конвертировать из time_point в tm (желательно без повышения)?

Первое, что нужно отметить, это то, что std::chrono::time_point шаблонизируется не только по duration, но и по часам. Часы означают эпоху. И разные часы могут иметь разные эпохи.

Например, в моей системе std::chrono::high_resolution_clock и std::chrono::steady_clock имеют эпоху: всякий раз, когда компьютер загружается. Если вы не знаете, в какое время компьютер загрузился, нет способа конвертировать эту time_point в любую систему календаря.

При этом вы, вероятно, говорили только о std::chrono::system_clock::time_point, так как эта time_point и только эта time_point должна иметь детерминированные отношения с гражданским (gregorian) календарем.

Как оказалось, каждая реализация std::chrono::system_clock я знаю, использует время unix. У этого есть эпоха Нового года 1970 пренебрегая прыжковыми секундами.

Это не гарантируется стандартом. Однако вы можете воспользоваться этим фактом, если хотите со следующими формулами, найденными по адресу:

хронологически совместимые алгоритмы даты низкого уровня

Во-первых, предупреждение, я использую последний проект С++ 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 с использованием другого часового пояса, вам нужно будет добавить/вычесть смещение по длительности часового пояса в std::chrono::system_clock::time_point до преобразования его в точность days. И если вы захотите снова взять секунды прыжка, настройте соответствующее количество секунд до усечения до 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-09-15 Вс 18:16:50 UTC. Это 257 дней с 1 января

В случае, если хроно-совместимые алгоритмы даты низкого уровня выходят в автономный режим или перемещаются, вот алгоритмы, используемые в 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 предлагает любая 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 в простой в использовании тип даты и времени.

Ответ 2

Там нет поддержки дат календаря в стандартной библиотеке, кроме функций библиотеки C, основанных на time_t.

Параметры в порядке моих предпочтений:

  • Boost.Date_Time, о котором вы говорите, что предпочитаете избегать
  • Некоторая другая сторонняя библиотека даты и времени (у меня нет рекомендаций, так как я буду использовать Boost)
  • Измените реализацию с открытым исходным кодом gmtime()
  • Используйте этот алгоритм, после проверки правильности.

Ответ 3

Я использовал библиотеку дат 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;
}

Это эффективно обходит time_t с его скрытой проблемой Y2038 на 32-битных системах. Эта функция была внесена в эту GitHub wiki, где я надеюсь, что другие будут способствовать другим полезным примерам и рецептам.