c++ parsing posix datetime-format

c++ - struct tm



¿Cómo puedo analizar una fecha iso 8601(con milisegundos opcionales) a una estructura tm en C++? (4)

Versión moderna C ++ de la función parse 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; }

Tengo una cadena que debe especificar una fecha y hora en formato ISO 8601 , que puede tener o no milisegundos, y deseo obtener una struct tm así como cualquier valor de milisegundos que se haya especificado (que se puede suponer que es cero si no está presente en la cadena).

¿Qué implicaría la detección de la cadena en el formato correcto, así como la conversión de una cadena especificada por el usuario en la struct tm y los valores de milisegundos?

Si no fuera por el problema de los miliscondios, probablemente podría usar la función C strptime() , pero no sé cuál es el comportamiento definido de esa función cuando los segundos contienen un punto decimal.

Como una advertencia final, si es posible, preferiría mucho una solución que no dependa de las funciones que solo se encuentran en Boost (pero estoy feliz de aceptar C ++ 11 como requisito previo).

La entrada se verá algo así como:

2014-11-12T19:12:14.505Z

o

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

Z , en este caso, indica UTC, pero se puede usar cualquier zona horaria, y se expresará como una diferencia de horas o minutos con respecto a GMT. La parte decimal del campo de segundos es opcional, pero el hecho de que pueda estar allí es por lo que no puedo usar simplemente strptime() o std::get_time() , que no describen ningún comportamiento definido en particular si tal carácter es Se encuentra en la parte de segundos de la cadena.


Mientras seguí la ruta sscanf() al principio, después de cambiar mi IDE a CLion, sugirió el uso de la función std::strtol() para reemplazar sscanf() .

Recuerde que este es solo un ejemplo de cómo lograr el mismo resultado que la versión sscanf() . No pretende ser más corto, universal y correcto en todos los sentidos, sino apuntar a todos en la dirección de "solución C ++ pura". Se basa en las cadenas de fecha y hora que recibo de una API y aún no es universal (mi caso necesita manejar el YYYY-MM-DDTHH:mm:ss.sssZ ), se podría modificar fácilmente para manejar diferentes.

Antes de publicar el código, hay una cosa que debe hacerse antes de usar std::strtol() : limpiar la cadena en sí misma, así que elimine cualquier marcador que no sea un dígito ("-", ":", "T", "Z ",". "), porque sin ella std::strtol() analizará los números de manera incorrecta (podría terminar con valores de mes o día negativos sin él).

Este pequeño fragmento toma una cadena ISO-8601 (el formato que necesitaba, como se mencionó anteriormente) y lo convierte en un resultado std::time_t , que representa el tiempo de la época en milisegundos. Desde aquí es bastante fácil entrar en objetos de 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; }

No es el más limpio y universal, pero hace el trabajo sin tener que recurrir a funciones de estilo C como sscanf() .


Nueva respuesta para la vieja pregunta. Fundamento: herramientas actualizadas.

Al usar esta biblioteca de código abierto y gratuita, se puede analizar en std::chrono::time_point<system_clock, milliseconds> , que tiene la ventaja sobre la tm de poder mantener una precisión de milisegundos. Y si realmente lo necesita, puede continuar con la API de C a través de system_clock::to_time_t (perdiendo los milisegundos en el camino).

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

Esto produce:

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

Tenga en cuenta que ambas salidas son UTC. El parse convirtió la hora local a UTC utilizando el desplazamiento -5:00 . Si realmente desea la hora local , también hay una forma de analizar en un tipo llamado date::local_time<milliseconds> que luego se analizaría pero ignoraría el desplazamiento. Incluso se puede analizar el desplazamiento en un chrono::minutes si se desea (usando una sobrecarga de parse tomando minutes& ).

La precisión del análisis se controla mediante la precisión del chrono::time_point que se pasa, en lugar de mediante indicadores en la cadena de formato. Y el desplazamiento puede ser del estilo +/-hhmm con %z , o +/-[h]h:mm con %Ez .


Puede usar sscanf C ( http://www.cplusplus.com/reference/cstdio/sscanf/ ) para analizarlo:

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 tiene std::string , puede llamarse así ( 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);

Si debe manejar diferentes zonas horarias, debe usar el valor de retorno de sscanf - número de argumentos analizados:

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

Y luego puede llenar la estructura tm ( http://www.cplusplus.com/reference/ctime/tm/ ):

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)

También se puede hacer con std::get_time ( std::get_time ) desde C++11 como @Barry mencionó en el comentario, ¿cómo puedo analizar una fecha ISO 8601? (con milisegundos opcionales) a una estructura tm en C ++?