allow creating Time objects from formatted timestamps
authorPhilipp Gesang <philipp.gesang@intra2net.com>
Tue, 30 Jan 2018 14:52:25 +0000 (15:52 +0100)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Wed, 27 Mar 2019 09:31:38 +0000 (10:31 +0100)
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.

src/timefunc.cpp
src/timefunc.hxx
test/test_timefunc.cpp

index 0abdfcb..b84acb8 100644 (file)
@@ -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<struct tm>
+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<Time>
+    time_of_iso8601 (const std::string        &s,
+                     const bool                date,
+                     const bool                time,
+                     const bool                tz,
+                     const enum type::id       id,
+                     const enum type::variant  var) NOEXCEPT
+    {
+        boost::optional<struct tm> tm = scan_iso8601 (s, date, time, tz);
+
+        if (!tm) {
+            return boost::none;
+        }
+
+        return Time (*tm, id, var);
+    }
+
 } /* [namespace clock] */
 
 } /* [namespace I2n] */
index 1b9df92..43c5f20 100644 (file)
@@ -64,6 +64,16 @@ inline std::string format_iso8601(const struct timespec ts, const bool utc=true,
                                   const bool tz=true)
 { return format_iso8601 (ts.tv_sec, utc, date, time, tz); }
 
+boost::optional<struct tm> scan_iso8601 (const char *s,
+                                         const bool date=true, const bool time=true,
+                                         const bool tz=true) NOEXCEPT;
+inline boost::optional<struct tm> scan_iso8601 (const std::string &s,
+                                                const bool date=true,
+                                                const bool time=true,
+                                                const bool tz=true) NOEXCEPT
+{ return scan_iso8601 (s.c_str (), date, time, tz); }
+
+
 void seconds_to_hour_minute(int seconds, int *hour, int *minute);
 void split_daysec(int daysec, int *outhours=NULL, int *outminutes=NULL, int *outseconds=NULL);
 std::string output_hour_minute(int hour, int minute, bool h_for_00=true, int seconds=0);
@@ -355,6 +365,10 @@ namespace clock {
                 , err     (err)
             { this->carry_nsec (); }
 
+            Time (const struct tm          &tm,
+                  const enum type::id       id  = type::mono,
+                  const enum type::variant  var = type::dflt) NOEXCEPT;
+
         /* value read access *************************************************/
         public:
 
@@ -569,6 +583,14 @@ namespace clock {
     zero (const enum type::id      id  = type::mono,
           const enum type::variant var = type::dflt) NOEXCEPT;
 
+    boost::optional<Time>
+    time_of_iso8601 (const std::string        &s,
+                     const bool                date = true,
+                     const bool                time = true,
+                     const bool                tz   = true,
+                     const enum type::id       id   = type::real,
+                     const enum type::variant  var  = type::dflt) NOEXCEPT;
+
 } /* [namespace clock] */
 
 } /* [namespace I2n] */
index e5b4adf..08ac8ef 100644 (file)
@@ -1051,6 +1051,62 @@ BOOST_AUTO_TEST_SUITE(Clock)
         BOOST_CHECK_EQUAL("11.11.2018", *s);
     }
 
+    BOOST_AUTO_TEST_CASE(FromString_iso8601_full)
+    {
+        const std::string in1 ("0001-01-01T00:00:00Z+0000");
+        const std::string in2 ("2018-11-11T11:11:11Z+0000");
+
+        boost::optional<I2n::clock::Time> t1 = I2n::clock::time_of_iso8601 (in1);
+        boost::optional<I2n::clock::Time> t2 = I2n::clock::time_of_iso8601 (in2);
+
+        BOOST_CHECK(t1);
+        BOOST_CHECK(t2);
+
+        BOOST_CHECK_EQUAL(*t1->format_iso8601 (), in1);
+        BOOST_CHECK_EQUAL(*t2->format_iso8601 (), in2);
+    }
+
+    BOOST_AUTO_TEST_CASE(FromString_iso8601_full_negyear)
+    {
+        const std::string in1 ("-0001-01-01T00:00:00Z+0000");
+        const std::string in2 ("-2018-11-11T11:11:11Z+0000");
+
+        boost::optional<I2n::clock::Time> t1 = I2n::clock::time_of_iso8601 (in1);
+        boost::optional<I2n::clock::Time> t2 = I2n::clock::time_of_iso8601 (in2);
+
+        BOOST_CHECK(t1);
+        BOOST_CHECK(t2);
+
+        BOOST_CHECK_EQUAL(*t1->format_iso8601 (), in1);
+        BOOST_CHECK_EQUAL(*t2->format_iso8601 (), in2);
+    }
+
+    BOOST_AUTO_TEST_CASE(FromString_iso8601_partial)
+    {
+        const std::string in1 ("2018-11-11T11:11:11");
+        const std::string in2 ("2018-11-11");
+        const std::string in3 ("11:11:11");
+
+        boost::optional<I2n::clock::Time> t1 =
+            I2n::clock::time_of_iso8601 (in1, true, true, false);
+        boost::optional<I2n::clock::Time> t2 =
+            I2n::clock::time_of_iso8601 (in2, true, false, false);
+        boost::optional<I2n::clock::Time> t3 =
+            I2n::clock::time_of_iso8601 (in3, false, true, false);
+
+        BOOST_CHECK(t1);
+        BOOST_CHECK(t2);
+        BOOST_CHECK(t3);
+        /*
+         * We test for the difference here which is zero if the number is
+         * correct but causes the difference from the expected value to be
+         * printed in case the test fails.
+         */
+        BOOST_CHECK_EQUAL(*t1->format_iso8601 (true,  true,  true, false), in1);
+        BOOST_CHECK_EQUAL(*t2->format_iso8601 (true,  true, false, false), in2);
+        BOOST_CHECK_EQUAL(*t3->format_iso8601 (true, false,  true, false), in3);
+    }
+
 BOOST_AUTO_TEST_SUITE_END() /* [Clock] */
 
 BOOST_AUTO_TEST_SUITE_END()