From: Philipp Gesang Date: Tue, 30 Jan 2018 14:52:25 +0000 (+0100) Subject: allow creating Time objects from formatted timestamps X-Git-Tag: v2.11~1^2~13 X-Git-Url: http://developer.intra2net.com/git/?p=libi2ncommon;a=commitdiff_plain;h=2795e39cc40483f3508a7d932f08ecac36e9d729 allow creating Time objects from formatted timestamps Read ISO-8601 formatted strings to construct timestamps from. The format strings already defined for formatting timestamps are reused. Since years can be negative but the sign handling in glibc is flawed with the %Y specifier, we need to handle negative year numbers manually. --- diff --git a/src/timefunc.cpp b/src/timefunc.cpp index 0abdfcb..b84acb8 100644 --- a/src/timefunc.cpp +++ b/src/timefunc.cpp @@ -772,30 +772,24 @@ bool realtime_clock_gettime(long int& seconds, long int& nano_seconds) } // eo realtime_clock_gettime(long int&,long int&) -static const char *const iso8601_fmt_d = "%F"; +/* + * There is a discrepancy of one input character + * due to the lack of sign handling in strptime(3): + * + * - strftime(3) needs the year specified as %5Y to account for the + * leading dash; + * - strptime(3) will not parse the leading dash with that format + * but apart from that it works well. + */ +static const char *const iso8601_fmt_d = "%4Y-%m-%d"; static const char *const iso8601_fmt_t = "%T"; static const char *const iso8601_fmt_tz = "%TZ%z"; -static const char *const iso8601_fmt_dt = "%FT%T"; -static const char *const iso8601_fmt_dtz = "%FT%TZ%z"; +static const char *const iso8601_fmt_dt = "%4Y-%m-%dT%T"; +static const char *const iso8601_fmt_dtz = "%4Y-%m-%dT%TZ%z"; -/** - * @brief Format a time structure according to ISO-8601, e. g. - * “2018-01-09T10:40:00Z+0100”; see \c strftime(3) for - * the details. - * - * @param tm Time to format as broken-down \c struct tm. - * @param date Include the day part ([-]YYYY-MM-DD). - * @param time Include the time part (hh:mm:ss). - * @param tz Include the timezone ([±]ZZZZ); only heeded if - * \c time is requested as well. - * - * @return The formatted timestamp. - */ -std::string format_iso8601 (const struct tm &tm, const bool date, - const bool time, const bool tz) +static inline const char * +pick_iso8601_fmt (const bool date, const bool time, const bool tz) { -# define ISO8601_BUFSIZE 27 /* max: -YYYY-MM-DDThh:mm:ssZ+zzzz ⇒ 26 */ - char buf [ISO8601_BUFSIZE] = { 0 }; const char *format = NULL; if (date) { @@ -814,14 +808,63 @@ std::string format_iso8601 (const struct tm &tm, const bool date, format = iso8601_fmt_t; /* default to %T */ } - const size_t n = strftime (buf, ISO8601_BUFSIZE, format, &tm); + return format; +} + +namespace { + + /* + * max: -YYYYYYYYYY-MM-DDThh:mm:ssZ+zzzz ⇒ 32 + * That is assuming the year in broken down time is an int we + * need to reserve ten decimal places. + */ +// static_assert (sizeof (((struct tm *)NULL)->tm_year) == 4); + static const size_t iso8601_bufsize = 33; + + static inline int flip_tm_year (const int y) + { return (y + 1900) * -1 - 1900; } +} /* [namespace] */ + +/** + * @brief Format a time structure according to ISO-8601, e. g. + * “2018-01-09T10:40:00Z+0100”; see \c strftime(3) for + * the details. + * + * @param tm Time to format as broken-down \c struct tm. + * @param date Include the day part ([-]YYYY-MM-DD). + * @param time Include the time part (hh:mm:ss). + * @param tz Include the timezone ([±]ZZZZ); only heeded if + * \c time is requested as well. + * + * @return The formatted timestamp. + */ +std::string format_iso8601 (const struct tm &tm, const bool date, + const bool time, const bool tz) +{ + struct tm tmp; + char buf [iso8601_bufsize] = { 0 }; + char *start = &buf [0]; + const char *format = pick_iso8601_fmt (date, time, tz); + + memcpy (&tmp, &tm, sizeof (tmp)); + + if (tmp.tm_year < -1900) { /* negative year */ + *start = '-'; + start++; + tmp.tm_year = flip_tm_year (tmp.tm_year); + } + + /* + * The sign is *always* handled above so the formatted string her + * is always one character shorter. + * */ + const size_t n = strftime (start, iso8601_bufsize-1, format, &tmp); - buf [n] = '\0'; + buf [n+1] = '\0'; return std::string (buf); } - typedef struct tm * (*time_breakdown_fn) (const time_t *, struct tm *); /** @@ -850,6 +893,61 @@ std::string format_iso8601 (time_t t, const bool utc, const bool date, return format_iso8601 (tm, date, time, tz); } +/** + * @brief Read a ISO-8601 formatted date stamp into broken down time. + * + * @param s String containing the timestamp. + * + * @return \c boost::none if the input string was \c NULL or malformed, + * an optional \c struct tm with the extracted values otherwise. + */ +boost::optional +scan_iso8601 (const char *s, + const bool date, const bool time, const bool tz) NOEXCEPT +{ + struct tm tm; + const char *format = pick_iso8601_fmt (date, time, tz); + const char *start = s; + bool negyear = false; + + if (s == NULL) { + return boost::none; + } + + switch (s [0]) { + case '\0': { + return boost::none; + break; + } + /* + * Contrary to what the man page indicates, strptime(3) is *not* + * the inverse operation of strftime(3)! The later correctly formats + * negative year numbers with the %F modifier wheres the former trips + * over the sign character. + */ + case '-': { + negyear = true; + start++; + break; + } + default: { + break; + } + } + + memset (&tm, 0, sizeof (tm)); + + if (strptime (start, format, &tm) == NULL) { + return boost::none; + } + + if (negyear) { + tm.tm_year = flip_tm_year (tm.tm_year); + } + + return tm; +} + namespace I2n { @@ -924,6 +1022,19 @@ namespace clock { , err (0) { } + Time::Time (const struct tm &tm, + const enum type::id id, + const enum type::variant var) NOEXCEPT + { + struct tm tmp_tm; /* dummy for mktime(3) */ + Time tmp_time; + + memcpy (&tmp_tm, &tm, sizeof (tmp_tm)); + tmp_time = Time (mktime (&tmp_tm), 0l, id, var); + + this->swap (tmp_time); + } + int64_t Time::as_nanosec (void) const NOEXCEPT { @@ -1080,6 +1191,23 @@ namespace clock { return 0; } + boost::optional