web-dev-qa-db-fra.com

comment analyser une date ISO 8601 (avec des millisecondes optionnelles) dans une struct tm en C++?

J'ai une chaîne qui doit spécifier une date et une heure dans ISO 8601 format, qui peut contenir ou non des millisecondes, et je souhaite obtenir un struct tm ainsi que toute valeur en millisecondes pouvant avoir été spécifié (qui peut être supposé être zéro s'il n'est pas présent dans la chaîne).

Qu'impliquerait-on pour détecter si la chaîne est au format correct, ainsi que pour convertir une chaîne spécifiée par l'utilisateur en valeurs struct tm et millisecondes?

Sans la question des millisecondes, je pourrais probablement simplement utiliser la fonction C strptime(), mais je ne sais pas quel est le comportement défini de cette fonction lorsque les secondes contiennent un point décimal.

Enfin, si cela est possible, je préférerais grandement une solution ne dépendant pas de fonctions qui ne se trouvent que dans Boost (mais je suis heureux d’accepter le C++ 11 comme condition préalable).

L'entrée va ressembler à quelque chose comme:

2014-11-12T19:12:14.505Z

ou

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

Z, dans ce cas, indique l’heure UTC, mais n’importe quel fuseau horaire peut être utilisé et sera exprimé en décalage horaire de + ou - heures/minutes par rapport à l’heure GMT. La partie décimale du champ des secondes est facultative, mais le fait qu’elle soit présente est la raison pour laquelle je ne peux pas simplement utiliser strptime() ou std::get_time(), qui ne décrivent aucun comportement défini particulier si un tel caractère est trouvé dans la partie secondes du mot chaîne.

17
markt1964

Vous pouvez utiliser C 's sscanf ( http://www.cplusplus.com/reference/cstdio/sscanf/ ) pour l’analyser:

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

Si vous avez std::string, il peut être appelé comme ceci ( 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);

S'il doit gérer des fuseaux horaires différents, vous devez utiliser sscanf return value - nombre d'arguments analysés:

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

Et ensuite, vous pouvez remplir 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)

Cela peut également être fait avec std::get_time ( http://en.cppreference.com/w/cpp/io/manip/get_time ) puisque C++11 en tant que @Barry est mentionné dans le commentaire comment puis-je analyser une date ISO 8601 (avec facultatif millisecondes) à une struct tm en C++?

13
k06a

Nouvelle réponse à l'ancienne question. Justification: outils mis à jour.

En utilisant cette bibliothèque gratuite à code source libre , vous pouvez analyser un std::chrono::time_point<system_clock, milliseconds>, qui présente l’avantage sur une tm de pouvoir conserver une précision à la milliseconde. Et si vous en avez vraiment besoin, vous pouvez passer à l'API C via system_clock::to_time_t (perdre les millisecondes en cours de route).

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

Cela génère:

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

Notez que les deux sorties sont en UTC. La parse a converti l'heure locale en heure UTC à l'aide du décalage -5:00. Si vous voulez réellement heure locale , il existe également un moyen d’analyser dans un type appelé date::local_time<milliseconds> qui ensuite analyserait mais ignorerait le décalage. On peut même analyser le décalage dans un chrono::minutes si désiré (en utilisant une surcharge parse en prenant minutes&).

La précision de l'analyse est contrôlée par la précision du chrono::time_point que vous transmettez, plutôt que par les indicateurs de la chaîne de format. Et le décalage peut être soit du style +/-hhmm avec %z, soit du +/-[h]h:mm avec %Ez.

17
Howard Hinnant

Version C++ moderne de la fonction d'analyse 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;
}
0
Sergey

Bien que je sois d'abord entré dans le chemin sscanf(), après avoir basculé mon IDE sur CLion, il a suggéré d'utiliser la fonction std::strtol() pour remplacer sscanf()

N'oubliez pas qu'il s'agit simplement d'un exemple permettant d'obtenir le même résultat que la version sscanf(). Ce n'est pas censé être plus court, universel et correct à tous égards, mais orienter tout le monde dans la direction de "solution C++ pure". Il est basé sur les chaînes d'horodatage que je reçois d'une API et n'est pas encore universel (mon cas nécessite de gérer le format YYYY-MM-DDTHH:mm:ss.sssZ), il pourrait être facilement modifié pour en gérer plusieurs.

Avant de poster le code, il y a une chose à faire avant d'utiliser std::strtol(): nettoyer la chaîne elle-même, en supprimant ainsi les marqueurs autres que des chiffres ("-", ":", "T", "Z", "." ), car sans std::strtol() analysera les chiffres dans le mauvais sens (vous pourriez vous retrouver avec des valeurs de mois ou de jour négatives sans cela).

Ce petit extrait prend une chaîne ISO-8601 (le format dont j'avais besoin, comme mentionné ci-dessus) et la convertit en un résultat std::time_t, représentant le temps Epoch en millisecondes. À partir de là, il est assez facile d’accéder aux objets 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's 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's 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;
}

Pas le plus propre et le plus universel, mais fait le travail sans avoir recours aux fonctions de style C comme sscanf().

0
TeHMoroS