document timefunc api
[libi2ncommon] / src / timefunc.cpp
index cab9469..0883055 100644 (file)
@@ -24,6 +24,8 @@ on this file might be covered by the GNU General Public License.
  *
  */
 
+#include <cstdio>
+#include <errno.h>
 #include <string>
 #include <sstream>
 #include <iostream>
@@ -771,15 +773,267 @@ bool realtime_clock_gettime(long int& seconds, long int& nano_seconds)
 } // eo realtime_clock_gettime(long int&,long int&)
 
 
+/*
+ * 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.
+ */
+
+namespace iso8601 {
+
+    /*
+     * 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 bufsize = 33;
+
+    enum kind {
+        d    = 0,
+        t    = 1,
+        tz   = 2,
+        dt   = 3,
+        dtz  = 4,
+        ISO8601_SIZE = 5,
+    };
+
+    /*
+     * Unfortunately the glibc strptime(3) on the Intranator trips over
+     * the length specifier in field descriptors so we can’t reuse the
+     * formatters here. This problem is fixed in newer glibc. For the time
+     * being we keep two tables of formatters and choose the appropriate
+     * at runtime.
+     */
+
+    static const char *const formatter [ISO8601_SIZE] =
+    { /* [iso8601::d  ] = */ "%4Y-%m-%d",
+      /* [iso8601::t  ] = */ "%T",
+      /* [iso8601::tz ] = */ "%TZ%z",
+      /* [iso8601::dt ] = */ "%4Y-%m-%dT%T",
+      /* [iso8601::dtz] = */ "%4Y-%m-%dT%TZ%z",
+    };
+
+    static const char *const scanner [ISO8601_SIZE] =
+    { /* [iso8601::d  ] = */ "%Y-%m-%d",
+      /* [iso8601::t  ] = */ "%T",
+      /* [iso8601::tz ] = */ "%TZ%z",
+      /* [iso8601::dt ] = */ "%Y-%m-%dT%T",
+      /* [iso8601::dtz] = */ "%Y-%m-%dT%TZ%z",
+    };
+
+    static inline const char *
+    pick_fmt (const bool date, const bool time, const bool tz, const bool scan=false)
+    {
+        const char *const  *table  = scan ? iso8601::scanner : iso8601::formatter;
+        enum iso8601::kind  format = iso8601::dtz;
+
+        if (date) {
+            if (time) {
+                if (tz) {
+                    format = iso8601::dtz;
+                } else {
+                    format = iso8601::dt;
+                }
+            } else {
+                format = iso8601::d;
+            }
+        } else if (time && tz) {
+            format = iso8601::tz;
+        } else {
+            format = iso8601::t; /* default to %T */
+        }
+
+        return table [format];
+    }
+
+} /* [iso8601] */
+
+
+namespace {
+
+    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 needed 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 = iso8601::pick_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 here
+     * is always one character shorter.
+     */
+    if (strftime (start, iso8601::bufsize-1, format, &tmp) == 0)
+    {
+        return std::string ();
+    }
+
+    buf [iso8601::bufsize-1] = '\0'; /* Just in case. */
+
+    return std::string (buf);
+}
+
+typedef struct tm * (*time_breakdown_fn) (const time_t *, struct tm *);
+
+/**
+ * @brief         Format a UNIX timestamp according to ISO-8601. Converts
+ *                to broken down time first.
+ *
+ * @param t       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 (time_t t, const bool utc, const bool date,
+                            const bool time, const bool tz)
+{
+    time_breakdown_fn breakdown = utc ? gmtime_r : localtime_r;
+    struct tm tm;
+
+    errno = 0;
+    if (breakdown (&t, &tm) == NULL) {
+        return std::string ("error analyzing timestamp: ") + strerror (errno);
+    }
+
+    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 = iso8601::pick_fmt (date, time, tz, true);
+    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;
+}
+
+/**
+ * @brief         Format a \c struct timespec in the schema established by
+ *                time(1): “3m14.159s”.
+ *
+ * @param ts      The time spec to format.
+ *
+ * @return        \c boost:none in case of error during formatting, an optional
+ *                \c std::string otherwise.
+ */
+boost::optional<std::string>
+format_min_sec_msec (const struct timespec &ts)
+{
+    char ms [4] = { '\0', '\0', '\0', '\0' };
+
+    if (snprintf (ms, 4, "%.3ld", ts.tv_nsec / 1000000) < 0) {
+        return boost::none;
+    }
+
+    const time_t min = ts.tv_sec / 60;
+    const time_t sec = ts.tv_sec - min * 60;
+
+    return I2n::to_string (min) + "m"
+         + I2n::to_string (sec) + "."
+         + ms + "s"
+         ;
+}
+
 namespace I2n {
 
 namespace clock {
 
     namespace {
 
+        /**
+         * @brief         <b>For internal use only</b>. Translates clock
+         *                specification flags to kernel clock types.
+         *
+         * @param id      Master clock id: \c mono, \c real, \c boot, or \c
+         *                cpu.
+         * @param var     Variant of clock if appropriate: \c raw, \c exact, \c
+         *                process, or \c thread. Use \c dflt for the base
+         *                variant.
+         *
+         * @return        The clock id for using with kernel APIs.
+         */
         static inline clockid_t
         clockid_of_flags (const enum type::id      id,
-                          const enum type::variant var)
+                          const enum type::variant var) NOEXCEPT
         {
             clockid_t cid = CLOCK_MONOTONIC_COARSE;
 
@@ -813,9 +1067,7 @@ namespace clock {
                 }
 
                 case type::boot: {
-                    if (var & type::exact) {
-                        cid = CLOCK_BOOTTIME;
-                    }
+                    cid = CLOCK_BOOTTIME;
                     break;
                 }
 
@@ -834,31 +1086,88 @@ namespace clock {
 
         static const struct timespec zero_time = { 0, 0 };
 
-#       define NANO (1000L * 1000 * 1000)
-
     } /* [namespace] */
 
-    Time::Time (const enum type::id id, const enum type::variant var)
+    Time::Time (const enum type::id id,
+                const enum type::variant var) NOEXCEPT
         : value   (zero_time)
         , id      (id)
         , variant (var)
         , err     (0)
     { }
 
+    /*
+     * Ctor from *struct tm*. On 32 bit systems the conversion to *time_t* will
+     * fail with years outside the range from epoch to 2038.
+     */
+    Time::Time (const struct tm          &tm,
+                const enum type::id       id,
+                const enum type::variant  var)
+    {
+        struct tm tmp_tm; /* dummy for mktime(3) */
+        Time   tmp_time;
+
+        memcpy (&tmp_tm, &tm, sizeof (tmp_tm));
+
+        errno = 0;
+        const time_t t = mktime (&tmp_tm);
+        if (t == - 1) { /* Glibc does not set errno on out-of-range here! */
+            const char *datestr = asctime (&tm);
+            throw conversion_error (errno,
+                                    std::string ("mktime: from struct tm {")
+                                    + std::string (datestr, 0, strlen(datestr)-1)
+                                    + "}");
+        }
+
+        tmp_time = Time (t, 0l, id, var);
+
+        this->swap (tmp_time);
+    }
+
     int64_t
-    Time::as_nanosec (void)
-    { return int64_t (this->value.tv_sec) * NANO + this->value.tv_nsec; }
+    Time::as_nanosec (void) const NOEXCEPT
+    {
+        return int64_t (this->value.tv_sec) * TIME_CONST_FACTOR_NANO
+             + this->value.tv_nsec;
+    }
 
     long
-    Time::as_nanosec_L (void) /* likely to overflow */
+    Time::as_nanosec_L (void) const NOEXCEPT /* likely to overflow */
     { return static_cast<long>(this->as_nanosec ()); }
 
+    Time &
+    Time::operator= (Time t2) NOEXCEPT
+    {
+        this->swap (t2);
+
+        return *this;
+    }
+
+    /*
+     * @note    This operator is an up-assignment from a type containing less
+     *          information than the structure assigned from. Since the
+     *          operator can only be two valued we must normalize the remaining
+     *          fields to the default clock. When assigning from non-default
+     *          clocks, use the appropriate constructor and pass it the desired
+     *          id and variant so as to assign the result.
+     */
+    Time &
+    Time::operator= (struct timespec ts) NOEXCEPT
+    {
+        std::swap (this->value, ts);
+        this->id      = clock::type::mono;
+        this->variant = clock::type::dflt;
+        this->err     = 0;
+
+        return *this;
+    }
+
     void
-    Time::unset (void)
+    Time::unset (void) NOEXCEPT
     { this->value = zero_time; }
 
     bool
-    Time::set (void)
+    Time::set (void) NOEXCEPT
     {
         struct timespec now;
 
@@ -877,10 +1186,100 @@ namespace clock {
         return true;
     }
 
+    Time &
+    Time::add (const time_t sec, const long nsec) NOEXCEPT
+    {
+        this->value.tv_sec  += sec;
+        this->value.tv_nsec += nsec;
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    Time &
+    Time::subtract (const time_t sec, const long nsec) NOEXCEPT
+    {
+        this->value.tv_sec  -= sec;
+        this->value.tv_nsec -= nsec;
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    Time &
+    Time::scale (const int64_t factor) NOEXCEPT
+    {
+        this->value.tv_sec  *= factor;
+        this->value.tv_nsec *= factor;
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    /*
+     * Below division code purposely does not attempt to handle divide-
+     * by-zero just as any other C++ division function does. It is up to
+     * the caller to ensure that the divisor is not zero.
+     */
+    Time &
+    Time::divide (const int64_t divisor) NOEXCEPT
+    {
+        const long   sec  = static_cast<long>    (this->value.tv_sec );
+        int64_t      nsec = static_cast<int64_t> (this->value.tv_nsec);
+        const ldiv_t div  = ldiv (sec, divisor);
+
+        if (div.rem != 0) {
+            nsec += div.rem * TIME_CONST_FACTOR_NANO;
+        }
+
+        nsec /= divisor;
+
+        this->value.tv_sec  = static_cast<time_t> (div.quot);
+        this->value.tv_nsec = static_cast<long>   (nsec);
+
+        this->carry_nsec ();
+
+        return *this;
+    }
+
+    boost::optional<std::string>
+    Time::format_iso8601 (const bool utc,
+                          const bool date,
+                          const bool time,
+                          const bool tz) const
+    {
+        time_breakdown_fn breakdown = utc ? gmtime_r : localtime_r;
+        struct tm tm;
+
+        if (breakdown (&this->value.tv_sec, &tm) == NULL) {
+            return boost::none;
+        }
+
+        return ::format_iso8601 (tm, date, time, tz);
+    }
+
+    std::string
+    Time::make_nice_time (void) const
+    {
+        /* XXX the cast below results in loss of precision with 64 bit time_t! */
+        return ::make_nice_time (static_cast<int> (this->value.tv_sec));
+    }
+
+    std::string
+    Time::format_full_time (void) const
+    { return ::format_full_time (this->value.tv_sec); }
+
+    std::string
+    Time::format_date (void) const
+    { return ::format_date (this->value.tv_sec); }
+
     boost::optional<Time>
-    now (const enum type::id id, const enum type::variant var)
+    now (const enum type::id id, const enum type::variant var) NOEXCEPT
     {
-        Time ret (id);
+        Time ret (id, var);
 
         if (!ret.set ()) {
             return boost::none;
@@ -889,7 +1288,54 @@ namespace clock {
         return ret;
     }
 
+    Time
+    zero (const enum type::id id, const enum type::variant var) NOEXCEPT
+    { return Time (id, var); }
+
+    int
+    compare (const Time &t1, const Time &t2) NOEXCEPT
+    {
+        if (t1.value.tv_sec < t2.value.tv_sec) {
+            return -1;
+        }
+
+        if (t1.value.tv_sec > t2.value.tv_sec) {
+            return 1;
+        }
+
+        if (t1.value.tv_nsec < t2.value.tv_nsec) {
+            return -1;
+        }
+
+        if (t1.value.tv_nsec > t2.value.tv_nsec) {
+            return 1;
+        }
+
+        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;
+        }
+
+        try {
+            return Time (*tm, id, var);
+        }
+        catch (conversion_error &_unused) { }
+
+        return boost::none;
+    }
+
 } /* [namespace clock] */
 
 } /* [namespace I2n] */
-