Как разобрать дату iso 8601 (с дополнительными миллисекундами) на struct tm в С++?

У меня есть строка, которая должна указывать дату и время в формате ISO 8601, который может иметь или не иметь миллисекунд в нем, и я я хочу получить от него struct tm, а также любое значение миллисекунды, которое может быть указано (которое можно считать равным нулю, если не указано в строке).

Что было бы связано с обнаружением того, соответствует ли строка правильному формату, а также преобразует указанную пользователем строку в значения struct tm и миллисекунды?

Если бы не проблема с миллисекундами, возможно, я мог бы просто использовать функцию C strptime(), но я не знаю, как должно быть определено определенное поведение этой функции, когда секунды содержат десятичную точку.

Как одно из последних предостережений, если это вообще возможно, я бы предпочел решение, которое не имеет никакой зависимости от функций, которые находятся только в Boost (но я рад принять С++ 11 в качестве предпосылки).

Вход будет выглядеть примерно так:

2014-11-12T19:12:14.505Z

или

2014-11-12T12:12:14.505-5:00

Z, в этом случае указывает UTC, но любой часовой пояс может использоваться и будет выражаться как смещение + или - часы/минуты от GMT. Десятичная часть поля секунд является необязательной, но факт, что она может быть вообще, заключается в том, почему я не могу просто использовать strptime() или std::get_time(), которые не описывают какого-либо определенного определенного поведения, если такой символ найден в секундной части строки.

Ответ 1

Вы можете использовать C sscanf (http://www.cplusplus.com/reference/cstdio/sscanf/), чтобы проанализировать его:

const char *dateStr = "2014-11-12T19:12:14.505Z";
int y,M,d,h,m;
float s;
sscanf(dateStr, "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s);

Если у вас есть std::string, его можно вызвать так (http://www.cplusplus.com/reference/string/string/c_str/):

std::string dateStr = "2014-11-12T19:12:14.505Z";
sscanf(dateStr.c_str(), "%d-%d-%dT%d:%d:%fZ", &y, &M, &d, &h, &m, &s);

Если он должен обрабатывать разные временные интервалы, вам нужно использовать sscanf return value - количество проанализированных аргументов:

int tzh = 0, tzm = 0;
if (6 < sscanf(dateStr.c_str(), "%d-%d-%dT%d:%d:%f%d:%dZ", &y, &M, &d, &h, &m, &s, &tzh, &tzm)) {
    if (tzh < 0) {
       tzm = -tzm;    // Fix the sign on minutes.
    }
}

И затем вы можете заполнить tm (http://www.cplusplus.com/reference/ctime/tm/) struct:

tm time;
time.tm_year = y - 1900; // Year since 1900
time.tm_mon = M - 1;     // 0-11
time.tm_mday = d;        // 1-31
time.tm_hour = h;        // 0-23
time.tm_min = m;         // 0-59
time.tm_sec = (int)s;    // 0-61 (0-60 in C++11)

Это также можно сделать с помощью std::get_time (http://en.cppreference.com/w/cpp/io/manip/get_time), поскольку C++11 как @Barry, упомянутый в комментарии как разобрать дату iso 8601 (с дополнительными миллисекундами) на struct tm в С++?

Ответ 2

Новый ответ на старый вопрос. Обоснование: обновленные инструменты.

Используя эту бесплатную библиотеку с открытым исходным кодом, можно std::chrono::time_point<system_clock, milliseconds> синтаксический анализ в std::chrono::time_point<system_clock, milliseconds>, который имеет преимущество перед tm в способности сохранять точность в миллисекундах. И если вам действительно нужно, вы можете перейти к C API через system_clock::to_time_t (теряя миллисекунды по пути).

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

date::sys_time<std::chrono::milliseconds>
parse8601(std::istream&& is)
{
    std::string save;
    is >> save;
    std::istringstream in{save};
    date::sys_time<std::chrono::milliseconds> tp;
    in >> date::parse("%FT%TZ", tp);
    if (in.fail())
    {
        in.clear();
        in.exceptions(std::ios::failbit);
        in.str(save);
        in >> date::parse("%FT%T%Ez", tp);
    }
    return tp;
}

int
main()
{
    using namespace date;
    using namespace std;
    cout << parse8601(istringstream{"2014-11-12T19:12:14.505Z"}) << '\n';
    cout << parse8601(istringstream{"2014-11-12T12:12:14.505-5:00"}) << '\n';
}

Это выводит:

2014-11-12 19:12:14.505
2014-11-12 17:12:14.505

Обратите внимание, что оба выхода UTC. parse конвертирован местное время UTC с помощью -5:00 смещением. Если вам действительно нужно местное время, существует также способ разбора на тип с именем date::local_time<milliseconds> который затем анализирует, но игнорирует смещение. При желании можно даже проанализировать смещение в chrono::minutes (используя перегрузку parse занимает minutes&).

Точность синтаксического анализа контролируется точностью chrono::time_point, а не флагами в строке формата. И смещение может иметь стиль + / -hhmm с %z или + / -[h]h:mm с %Ez.

Ответ 3

Современная C++ версия функции разбора ISO 8601

#include <cstdlib>
#include <ctime>
#include <string>

#ifdef _WIN32
#define timegm _mkgmtime
#endif

inline int ParseInt(const char* value)
{
    return std::strtol(value, nullptr, 10);
}

std::time_t ParseISO8601(const std::string& input)
{
    constexpr const size_t expectedLength = sizeof("1234-12-12T12:12:12Z") - 1;
    static_assert(expectedLength == 20, "Unexpected ISO 8601 date/time length");

    if (input.length() < expectedLength)
    {
        return 0;
    }

    std::tm time = { 0 };
    time.tm_year = ParseInt(&input[0]) - 1900;
    time.tm_mon = ParseInt(&input[5]) - 1;
    time.tm_mday = ParseInt(&input[8]);
    time.tm_hour = ParseInt(&input[11]);
    time.tm_min = ParseInt(&input[14]);
    time.tm_sec = ParseInt(&input[17]);
    time.tm_isdst = 0;
    const int millis = input.length() > 20 ? ParseInt(&input[20]) : 0;
    return timegm(&time) * 1000 + millis;
}

Ответ 4

Старый вопрос, и у меня есть старый код, чтобы внести свой вклад;). Я использовал библиотеку дат, упомянутую здесь. Хотя он отлично работает, он идет с затратами на производительность. В большинстве случаев это не очень актуально. Однако, если у вас есть, например, служба анализа данных, как у меня, это действительно имеет значение.

Я профилировал свое серверное приложение для оптимизации производительности и обнаружил, что анализ временной метки ISO с использованием библиотеки дат был в 3 раза медленнее, чем анализ всего документа (примерно 500 байт) json. В общем разборе временная метка составляла около 4,8% от общего процессорного времени.

В моем стремлении оптимизировать эту часть я не нашел много с C++, который я бы рассмотрел для живого продукта. И код, который я рассмотрел далее, в основном имел некоторые зависимости (например, синтаксический анализатор ISO в CEPH выглядит нормально и хорошо протестирован).

В конце концов, я обратился к старому доброму C и удалил некоторый код из SQLite date.c, чтобы он работал автономно. Разница:

дата: 872 мс

SQLite date.c: 54 мс

(Профилированный вес функции приложения для реальной жизни)

Вот оно (все кредиты на SQLite):

Заголовочный файл date_util.h

#include <stdint.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

    // Calculates time since epoch including milliseconds
    uint64_t ParseTimeToEpochMillis(const char *str, bool *error);

    // Creates an ISO timestamp with milliseconds from epoch with millis.
    // The buffer size (resultLen) for result must be at least 100 bytes.
    void TimeFromEpochMillis(uint64_t epochMillis, char *result, int resultLen, bool *error);

#ifdef __cplusplus
}
#endif

Это файл C date_util.c:

#include "_date.h"
#include <ctype.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdarg.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>


/*
 ** A structure for holding a single date and time.
 */
typedef struct DateTime DateTime;
struct DateTime {
    int64_t iJD;        /* The julian day number times 86400000 */
    int Y, M, D;        /* Year, month, and day */
    int h, m;           /* Hour and minutes */
    int tz;             /* Timezone offset in minutes */
    double s;           /* Seconds */
    char validJD;       /* True (1) if iJD is valid */
    char rawS;          /* Raw numeric value stored in s */
    char validYMD;      /* True (1) if Y,M,D are valid */
    char validHMS;      /* True (1) if h,m,s are valid */
    char validTZ;       /* True (1) if tz is valid */
    char tzSet;         /* Timezone was set explicitly */
    char isError;       /* An overflow has occurred */
};

/*
 ** Convert zDate into one or more integers according to the conversion
 ** specifier zFormat.
 **
 ** zFormat[] contains 4 characters for each integer converted, except for
 ** the last integer which is specified by three characters.  The meaning
 ** of a four-character format specifiers ABCD is:
 **
 **    A:   number of digits to convert.  Always "2" or "4".
 **    B:   minimum value.  Always "0" or "1".
 **    C:   maximum value, decoded as:
 **           a:  12
 **           b:  14
 **           c:  24
 **           d:  31
 **           e:  59
 **           f:  9999
 **    D:   the separator character, or \000 to indicate this is the
 **         last number to convert.
 **
 ** Example:  To translate an ISO-8601 date YYYY-MM-DD, the format would
 ** be "40f-21a-20c".  The "40f-" indicates the 4-digit year followed by "-".
 ** The "21a-" indicates the 2-digit month followed by "-".  The "20c" indicates
 ** the 2-digit day which is the last integer in the set.
 **
 ** The function returns the number of successful conversions.
 */
static int GetDigits(const char *zDate, const char *zFormat, ...){
    /* The aMx[] array translates the 3rd character of each format
     ** spec into a max size:    a   b   c   d   e     f */
    static const uint16_t aMx[] = { 12, 14, 24, 31, 59, 9999 };
    va_list ap;
    int cnt = 0;
    char nextC;
    va_start(ap, zFormat);
    do{
        char N = zFormat[0] - '0';
        char min = zFormat[1] - '0';
        int val = 0;
        uint16_t max;

        assert( zFormat[2]>='a' && zFormat[2]<='f' );
        max = aMx[zFormat[2] - 'a'];
        nextC = zFormat[3];
        val = 0;
        while( N-- ){
            if( !isdigit(*zDate) ){
                goto end_getDigits;
            }
            val = val*10 + *zDate - '0';
            zDate++;
        }
        if( val<(int)min || val>(int)max || (nextC!=0 && nextC!=*zDate) ){
            goto end_getDigits;
        }
        *va_arg(ap,int*) = val;
        zDate++;
        cnt++;
        zFormat += 4;
    }while( nextC );
end_getDigits:
    va_end(ap);
    return cnt;
}

/*
 ** Parse a timezone extension on the end of a date-time.
 ** The extension is of the form:
 **
 **        (+/-)HH:MM
 **
 ** Or the "zulu" notation:
 **
 **        Z
 **
 ** If the parse is successful, write the number of minutes
 ** of change in p->tz and return 0.  If a parser error occurs,
 ** return non-zero.
 **
 ** A missing specifier is not considered an error.
 */
static int ParseTimezone(const char *zDate, DateTime *p){
    int sgn = 0;
    int nHr, nMn;
    int c;
    while( isspace(*zDate) ){ zDate++; }
    p->tz = 0;
    c = *zDate;
    if( c=='-' ){
        sgn = -1;
    }else if( c=='+' ){
        sgn = +1;
    }else if( c=='Z' || c=='z' ){
        zDate++;
        goto zulu_time;
    }else{
        return c!=0;
    }
    zDate++;
    if( GetDigits(zDate, "20b:20e", &nHr, &nMn)!=2 ){
        return 1;
    }
    zDate += 5;
    p->tz = sgn*(nMn + nHr*60);
zulu_time:
    while( isspace(*zDate) ){ zDate++; }
    p->tzSet = 1;
    return *zDate!=0;
}

/*
 ** Parse times of the form HH:MM or HH:MM:SS or HH:MM:SS.FFFF.
 ** The HH, MM, and SS must each be exactly 2 digits.  The
 ** fractional seconds FFFF can be one or more digits.
 **
 ** Return 1 if there is a parsing error and 0 on success.
 */
static int ParseHhMmSs(const char *zDate, DateTime *p){
    int h, m, s;
    double ms = 0.0;
    if( GetDigits(zDate, "20c:20e", &h, &m)!=2 ){
        return 1;
    }
    zDate += 5;
    if( *zDate==':' ){
        zDate++;
        if( GetDigits(zDate, "20e", &s)!=1 ){
            return 1;
        }
        zDate += 2;
        if( *zDate=='.' && isdigit(zDate[1]) ){
            double rScale = 1.0;
            zDate++;
            while( isdigit(*zDate) ){
                ms = ms*10.0 + *zDate - '0';
                rScale *= 10.0;
                zDate++;
            }
            ms /= rScale;
        }
    }else{
        s = 0;
    }
    p->validJD = 0;
    p->rawS = 0;
    p->validHMS = 1;
    p->h = h;
    p->m = m;
    p->s = s + ms;
    if( ParseTimezone(zDate, p) ) return 1;
    p->validTZ = (p->tz!=0)?1:0;
    return 0;
}

/*
 ** Put the DateTime object into its error state.
 */
static void DatetimeError(DateTime *p){
    memset(p, 0, sizeof(*p));
    p->isError = 1;
}

/*
 ** Convert from YYYY-MM-DD HH:MM:SS to julian day.  We always assume
 ** that the YYYY-MM-DD is according to the Gregorian calendar.
 **
 ** Reference:  Meeus page 61
 */
static void ComputeJD(DateTime *p){
    int Y, M, D, A, B, X1, X2;

    if( p->validJD ) return;
    if( p->validYMD ){
        Y = p->Y;
        M = p->M;
        D = p->D;
    }else{
        Y = 2000;  /* If no YMD specified, assume 2000-Jan-01 */
        M = 1;
        D = 1;
    }
    if( Y<-4713 || Y>9999 || p->rawS ){
        DatetimeError(p);
        return;
    }
    if( M<=2 ){
        Y--;
        M += 12;
    }
    A = Y/100;
    B = 2 - A + (A/4);
    X1 = 36525*(Y+4716)/100;
    X2 = 306001*(M+1)/10000;
    p->iJD = (int64_t)((X1 + X2 + D + B - 1524.5 ) * 86400000);
    p->validJD = 1;
    if( p->validHMS ){
        p->iJD += p->h*3600000 + p->m*60000 + (int64_t)(p->s*1000);
        if( p->validTZ ){
            p->iJD -= p->tz*60000;
            p->validYMD = 0;
            p->validHMS = 0;
            p->validTZ = 0;
        }
    }
}

/*
 ** Parse dates of the form
 **
 **     YYYY-MM-DD HH:MM:SS.FFF
 **     YYYY-MM-DD HH:MM:SS
 **     YYYY-MM-DD HH:MM
 **     YYYY-MM-DD
 **
 ** Write the result into the DateTime structure and return 0
 ** on success and 1 if the input string is not a well-formed
 ** date.
 */
static int ParseYyyyMmDd(const char *zDate, DateTime *p){
    int Y, M, D, neg;

    if( zDate[0]=='-' ){
        zDate++;
        neg = 1;
    }else{
        neg = 0;
    }
    if( GetDigits(zDate, "40f-21a-21d", &Y, &M, &D)!=3 ){
        return 1;
    }
    zDate += 10;
    while( isspace(*zDate) || 'T'==*(uint8_t*)zDate ){ zDate++; }
    if( ParseHhMmSs(zDate, p)==0 ){
        /* We got the time */
    }else if( *zDate==0 ){
        p->validHMS = 0;
    }else{
        return 1;
    }
    p->validJD = 0;
    p->validYMD = 1;
    p->Y = neg ? -Y : Y;
    p->M = M;
    p->D = D;
    if( p->validTZ ){
        ComputeJD(p);
    }
    return 0;
}

/* The julian day number for 9999-12-31 23:59:59.999 is 5373484.4999999.
 ** Multiplying this by 86400000 gives 464269060799999 as the maximum value
 ** for DateTime.iJD.
 **
 ** But some older compilers (ex: gcc 4.2.1 on older Macs) cannot deal with
 ** such a large integer literal, so we have to encode it.
 */
#define INT_464269060799999  ((((int64_t)0x1a640)<<32)|0x1072fdff)

/*
 ** Return TRUE if the given julian day number is within range.
 **
 ** The input is the JulianDay times 86400000.
 */
static int ValidJulianDay(int64_t iJD){
    return iJD>=0 && iJD<=INT_464269060799999;
}

/*
 ** Compute the Year, Month, and Day from the julian day number.
 */
static void ComputeYMD(DateTime *p){
    int Z, A, B, C, D, E, X1;
    if( p->validYMD ) return;
    if( !p->validJD ){
        p->Y = 2000;
        p->M = 1;
        p->D = 1;
    }else if( !ValidJulianDay(p->iJD) ){
        DatetimeError(p);
        return;
    }else{
        Z = (int)((p->iJD + 43200000)/86400000);
        A = (int)((Z - 1867216.25)/36524.25);
        A = Z + 1 + A - (A/4);
        B = A + 1524;
        C = (int)((B - 122.1)/365.25);
        D = (36525*(C&32767))/100;
        E = (int)((B-D)/30.6001);
        X1 = (int)(30.6001*E);
        p->D = B - D - X1;
        p->M = E<14 ? E-1 : E-13;
        p->Y = p->M>2 ? C - 4716 : C - 4715;
    }
    p->validYMD = 1;
}

/*
 ** Compute the Hour, Minute, and Seconds from the julian day number.
 */
static void ComputeHMS(DateTime *p){
    int s;
    if( p->validHMS ) return;
    ComputeJD(p);
    s = (int)((p->iJD + 43200000) % 86400000);
    p->s = s/1000.0;
    s = (int)p->s;
    p->s -= s;
    p->h = s/3600;
    s -= p->h*3600;
    p->m = s/60;
    p->s += s - p->m*60;
    p->rawS = 0;
    p->validHMS = 1;
}

/*
 ** Compute both YMD and HMS
 */
static void ComputeYMD_HMS(DateTime *p){
    ComputeYMD(p);
    ComputeHMS(p);
}

/*
 ** Input "r" is a numeric quantity which might be a julian day number,
 ** or the number of seconds since 1970.  If the value if r is within
 ** range of a julian day number, install it as such and set validJD.
 ** If the value is a valid unix timestamp, put it in p->s and set p->rawS.
 */
static void SetRawDateNumber(DateTime *p, double r){
    p->s = r;
    p->rawS = 1;
    if( r>=0.0 && r<5373484.5 ){
        p->iJD = (int64_t)(r*86400000.0 + 0.5);
        p->validJD = 1;
    }
}

/*
 ** Clear the YMD and HMS and the TZ
 */
static void ClearYMD_HMS_TZ(DateTime *p){
    p->validYMD = 0;
    p->validHMS = 0;
    p->validTZ = 0;
}

// modified methods to only calculate for and back between epoch and iso timestamp with millis

uint64_t ParseTimeToEpochMillis(const char *str, bool *error) {
    assert(str);
    assert(error);
    *error = false;
    DateTime dateTime;

    int res = ParseYyyyMmDd(str, &dateTime);
    if (res) {
        *error = true;
        return 0;
    }

    ComputeJD(&dateTime);
    ComputeYMD_HMS(&dateTime);

    // get fraction (millis of a full second): 24.355 => 355
    int millis = (dateTime.s - (int)(dateTime.s)) * 1000;
    uint64_t epoch = (int64_t)(dateTime.iJD/1000 - 21086676*(int64_t)10000) * 1000 + millis;

    return epoch;
}

void TimeFromEpochMillis(uint64_t epochMillis, char *result, int resultLen, bool *error) {
    assert(resultLen >= 100);
    assert(result);
    assert(error);

    int64_t seconds = epochMillis / 1000;
    int millis = epochMillis - seconds * 1000;
    DateTime x;

    *error = false;
    memset(&x, 0, sizeof(x));
    SetRawDateNumber(&x, seconds);

    /*
     **    unixepoch
     **
     ** Treat the current value of p->s as the number of
     ** seconds since 1970.  Convert to a real julian day number.
     */
    {
        double r = x.s*1000.0 + 210866760000000.0;
        if( r>=0.0 && r<464269060800000.0 ){
            ClearYMD_HMS_TZ(&x);
            x.iJD = (int64_t)r;
            x.validJD = 1;
            x.rawS = 0;
        }

        ComputeJD(&x);
        if( x.isError || !ValidJulianDay(x.iJD) ) {
            *error = true;
        }
    }

    ComputeYMD_HMS(&x);
    snprintf(result, resultLen, "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ",
             x.Y, x.M, x.D, x.h, x.m, (int)(x.s), millis);
}

Эти два вспомогательных метода просто преобразуются в и из отметки времени в миллисекундах. Установка структуры tm из DateTime должна быть очевидной.

Пример использования:

// Calculate milliseconds since epoch
std::string timeStamp = "2019-09-02T22:02:24.355Z";
bool error;
uint64_t time = ParseTimeToEpochMillis(timeStamp.c_str(), &error);

// Get ISO timestamp with milliseconds component from epoch in milliseconds.
// Multiple by 1000 in case you have a standard epoch in seconds)
uint64_t epochMillis = 1567461744355; // == "2019-09-02T22:02:24.355Z"
char result[100] = {0};
TimeFromEpochMillis(epochMillis, result, sizeof(result), &error);
std::string resultStr(result); // == "2019-09-02T22:02:24.355Z"

Ответ 5

Хотя я сначала пошел по пути sscanf(), после переключения IDE на CLion он предложил использовать функцию std::strtol() для замены sscanf().

Помните, что это всего лишь пример достижения того же результата, что и версия sscanf(). Это не означает, что оно должно быть короче, универсально и правильно во всех отношениях, но должно указывать всем в направлении "чистого решения C++". Он основан на строках меток времени, которые я получаю от API, и пока не универсален (в моем случае требуется обработка в YYYY-MM-DDTHH:mm:ss.sssZ), его можно легко изменить для обработки различных.

Перед публикацией кода нужно сделать одну вещь перед использованием std::strtol(): очистить саму строку, удалив все нецифровые маркеры ("-", ":", "T", "Z ",". "), потому что без него std::strtol() будет анализировать числа неправильным образом (без него вы можете получить отрицательные значения месяца или дня).

Этот небольшой фрагмент принимает строку ISO-8601 (формат, который мне нужен, как упомянуто выше) и преобразует ее в результат std::time_t, представляющий время эпохи в миллисекундах. Отсюда довольно легко перейти к объектам std::chrono-type.

std::time_t parseISO8601(const std::string &input)
{
    // prepare the data output placeholders
    struct std::tm time = {0};
    int millis;

    // string cleaning for strtol() - this could be made cleaner, but for the sake of the example itself...
    std::string cleanInput = input
        .replace(4, 1, 1, ' ')
        .replace(7, 1, 1, ' ')
        .replace(10, 1, 1, ' ')
        .replace(13, 1, 1, ' ')
        .replace(16, 1, 1, ' ')
        .replace(19, 1, 1, ' ');

    // pointers for std::strtol()
    const char* timestamp = cleanInput.c_str();
    // last parsing end position - it where strtol finished parsing the last number found
    char* endPointer;
    // the casts aren't necessary, but I just wanted CLion to be quiet ;)
    // first parse - start with the timestamp string, give endPointer the position after the found number
    time.tm_year = (int) std::strtol(timestamp, &endPointer, 10) - 1900;
    // next parses - use endPointer instead of timestamp (skip the part, that already parsed)
    time.tm_mon = (int) std::strtol(endPointer, &endPointer, 10) - 1;
    time.tm_mday = (int) std::strtol(endPointer, &endPointer, 10);
    time.tm_hour = (int) std::strtol(endPointer, &endPointer, 10);
    time.tm_min = (int) std::strtol(endPointer, &endPointer, 10);
    time.tm_sec = (int) std::strtol(endPointer, &endPointer, 10);
    millis = (int) std::strtol(endPointer, &endPointer, 10);

    // convert the tm struct into time_t and then from seconds to milliseconds
    return std::mktime(&time) * 1000 + millis;
}

Не самый чистый и универсальный, но выполняет свою работу, не прибегая к функциям в стиле C, таким как sscanf().