summaryrefslogtreecommitdiff
path: root/autoopts/parse-duration.c
diff options
context:
space:
mode:
Diffstat (limited to 'autoopts/parse-duration.c')
-rw-r--r--autoopts/parse-duration.c601
1 files changed, 601 insertions, 0 deletions
diff --git a/autoopts/parse-duration.c b/autoopts/parse-duration.c
new file mode 100644
index 0000000..7f1fb49
--- /dev/null
+++ b/autoopts/parse-duration.c
@@ -0,0 +1,601 @@
+/* Parse a time duration and return a seconds count
+ Copyright (C) 2008-2012 Free Software Foundation, Inc.
+ Written by Bruce Korb <bkorb@gnu.org>, 2008.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+/* Specification. */
+#include "parse-duration.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef NUL
+#define NUL '\0'
+#endif
+
+#define cch_t char const
+
+typedef enum {
+ NOTHING_IS_DONE,
+ YEAR_IS_DONE,
+ MONTH_IS_DONE,
+ WEEK_IS_DONE,
+ DAY_IS_DONE,
+ HOUR_IS_DONE,
+ MINUTE_IS_DONE,
+ SECOND_IS_DONE
+} whats_done_t;
+
+#define SEC_PER_MIN 60
+#define SEC_PER_HR (SEC_PER_MIN * 60)
+#define SEC_PER_DAY (SEC_PER_HR * 24)
+#define SEC_PER_WEEK (SEC_PER_DAY * 7)
+#define SEC_PER_MONTH (SEC_PER_DAY * 30)
+#define SEC_PER_YEAR (SEC_PER_DAY * 365)
+
+#define TIME_MAX 0x7FFFFFFF
+
+/* Wrapper around strtoul that does not require a cast. */
+static unsigned long inline
+str_const_to_ul (cch_t * str, cch_t ** ppz, int base)
+{
+ return strtoul (str, (char **)ppz, base);
+}
+
+/* Wrapper around strtol that does not require a cast. */
+static long inline
+str_const_to_l (cch_t * str, cch_t ** ppz, int base)
+{
+ return strtol (str, (char **)ppz, base);
+}
+
+/* Returns BASE + VAL * SCALE, interpreting BASE = BAD_TIME
+ with errno set as an error situation, and returning BAD_TIME
+ with errno set in an error situation. */
+static time_t inline
+scale_n_add (time_t base, time_t val, int scale)
+{
+ if (base == BAD_TIME)
+ {
+ if (errno == 0)
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ if (val > TIME_MAX / scale)
+ {
+ errno = ERANGE;
+ return BAD_TIME;
+ }
+
+ val *= scale;
+ if (base > TIME_MAX - val)
+ {
+ errno = ERANGE;
+ return BAD_TIME;
+ }
+
+ return base + val;
+}
+
+/* After a number HH has been parsed, parse subsequent :MM or :MM:SS. */
+static time_t
+parse_hr_min_sec (time_t start, cch_t * pz)
+{
+ int lpct = 0;
+
+ errno = 0;
+
+ /* For as long as our scanner pointer points to a colon *AND*
+ we've not looped before, then keep looping. (two iterations max) */
+ while ((*pz == ':') && (lpct++ <= 1))
+ {
+ unsigned long v = str_const_to_ul (pz+1, &pz, 10);
+
+ if (errno != 0)
+ return BAD_TIME;
+
+ start = scale_n_add (v, start, 60);
+
+ if (errno != 0)
+ return BAD_TIME;
+ }
+
+ /* allow for trailing spaces */
+ while (isspace ((unsigned char)*pz))
+ pz++;
+ if (*pz != NUL)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ return start;
+}
+
+/* Parses a value and returns BASE + value * SCALE, interpreting
+ BASE = BAD_TIME with errno set as an error situation, and returning
+ BAD_TIME with errno set in an error situation. */
+static time_t
+parse_scaled_value (time_t base, cch_t ** ppz, cch_t * endp, int scale)
+{
+ cch_t * pz = *ppz;
+ time_t val;
+
+ if (base == BAD_TIME)
+ return base;
+
+ errno = 0;
+ val = str_const_to_ul (pz, &pz, 10);
+ if (errno != 0)
+ return BAD_TIME;
+ while (isspace ((unsigned char)*pz))
+ pz++;
+ if (pz != endp)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ *ppz = pz;
+ return scale_n_add (base, val, scale);
+}
+
+/* Parses the syntax YEAR-MONTH-DAY.
+ PS points into the string, after "YEAR", before "-MONTH-DAY". */
+static time_t
+parse_year_month_day (cch_t * pz, cch_t * ps)
+{
+ time_t res = 0;
+
+ res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
+
+ pz++; /* over the first '-' */
+ ps = strchr (pz, '-');
+ if (ps == NULL)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+ res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
+
+ pz++; /* over the second '-' */
+ ps = pz + strlen (pz);
+ return parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
+}
+
+/* Parses the syntax YYYYMMDD. */
+static time_t
+parse_yearmonthday (cch_t * in_pz)
+{
+ time_t res = 0;
+ char buf[8];
+ cch_t * pz;
+
+ if (strlen (in_pz) != 8)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ memcpy (buf, in_pz, 4);
+ buf[4] = NUL;
+ pz = buf;
+ res = parse_scaled_value (0, &pz, buf + 4, SEC_PER_YEAR);
+
+ memcpy (buf, in_pz + 4, 2);
+ buf[2] = NUL;
+ pz = buf;
+ res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MONTH);
+
+ memcpy (buf, in_pz + 6, 2);
+ buf[2] = NUL;
+ pz = buf;
+ return parse_scaled_value (res, &pz, buf + 2, SEC_PER_DAY);
+}
+
+/* Parses the syntax yy Y mm M ww W dd D. */
+static time_t
+parse_YMWD (cch_t * pz)
+{
+ time_t res = 0;
+ cch_t * ps = strchr (pz, 'Y');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
+ pz++;
+ }
+
+ ps = strchr (pz, 'M');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
+ pz++;
+ }
+
+ ps = strchr (pz, 'W');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK);
+ pz++;
+ }
+
+ ps = strchr (pz, 'D');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
+ pz++;
+ }
+
+ while (isspace ((unsigned char)*pz))
+ pz++;
+ if (*pz != NUL)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ return res;
+}
+
+/* Parses the syntax HH:MM:SS.
+ PS points into the string, after "HH", before ":MM:SS". */
+static time_t
+parse_hour_minute_second (cch_t * pz, cch_t * ps)
+{
+ time_t res = 0;
+
+ res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
+
+ pz++;
+ ps = strchr (pz, ':');
+ if (ps == NULL)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
+
+ pz++;
+ ps = pz + strlen (pz);
+ return parse_scaled_value (res, &pz, ps, 1);
+}
+
+/* Parses the syntax HHMMSS. */
+static time_t
+parse_hourminutesecond (cch_t * in_pz)
+{
+ time_t res = 0;
+ char buf[4];
+ cch_t * pz;
+
+ if (strlen (in_pz) != 6)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ memcpy (buf, in_pz, 2);
+ buf[2] = NUL;
+ pz = buf;
+ res = parse_scaled_value (0, &pz, buf + 2, SEC_PER_HR);
+
+ memcpy (buf, in_pz + 2, 2);
+ buf[2] = NUL;
+ pz = buf;
+ res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MIN);
+
+ memcpy (buf, in_pz + 4, 2);
+ buf[2] = NUL;
+ pz = buf;
+ return parse_scaled_value (res, &pz, buf + 2, 1);
+}
+
+/* Parses the syntax hh H mm M ss S. */
+static time_t
+parse_HMS (cch_t * pz)
+{
+ time_t res = 0;
+ cch_t * ps = strchr (pz, 'H');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
+ pz++;
+ }
+
+ ps = strchr (pz, 'M');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
+ pz++;
+ }
+
+ ps = strchr (pz, 'S');
+ if (ps != NULL)
+ {
+ res = parse_scaled_value (res, &pz, ps, 1);
+ pz++;
+ }
+
+ while (isspace ((unsigned char)*pz))
+ pz++;
+ if (*pz != NUL)
+ {
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+
+ return res;
+}
+
+/* Parses a time (hours, minutes, seconds) specification in either syntax. */
+static time_t
+parse_time (cch_t * pz)
+{
+ cch_t * ps;
+ time_t res = 0;
+
+ /*
+ * Scan for a hyphen
+ */
+ ps = strchr (pz, ':');
+ if (ps != NULL)
+ {
+ res = parse_hour_minute_second (pz, ps);
+ }
+
+ /*
+ * Try for a 'H', 'M' or 'S' suffix
+ */
+ else if (ps = strpbrk (pz, "HMS"),
+ ps == NULL)
+ {
+ /* Its a YYYYMMDD format: */
+ res = parse_hourminutesecond (pz);
+ }
+
+ else
+ res = parse_HMS (pz);
+
+ return res;
+}
+
+/* Returns a substring of the given string, with spaces at the beginning and at
+ the end destructively removed, per SNOBOL. */
+static char *
+trim (char * pz)
+{
+ /* trim leading white space */
+ while (isspace ((unsigned char)*pz))
+ pz++;
+
+ /* trim trailing white space */
+ {
+ char * pe = pz + strlen (pz);
+ while ((pe > pz) && isspace ((unsigned char)pe[-1]))
+ pe--;
+ *pe = NUL;
+ }
+
+ return pz;
+}
+
+/*
+ * Parse the year/months/days of a time period
+ */
+static time_t
+parse_period (cch_t * in_pz)
+{
+ char * pT;
+ char * ps;
+ char * pz = strdup (in_pz);
+ void * fptr = pz;
+ time_t res = 0;
+
+ if (pz == NULL)
+ {
+ errno = ENOMEM;
+ return BAD_TIME;
+ }
+
+ pT = strchr (pz, 'T');
+ if (pT != NULL)
+ {
+ *(pT++) = NUL;
+ pz = trim (pz);
+ pT = trim (pT);
+ }
+
+ /*
+ * Scan for a hyphen
+ */
+ ps = strchr (pz, '-');
+ if (ps != NULL)
+ {
+ res = parse_year_month_day (pz, ps);
+ }
+
+ /*
+ * Try for a 'Y', 'M' or 'D' suffix
+ */
+ else if (ps = strpbrk (pz, "YMWD"),
+ ps == NULL)
+ {
+ /* Its a YYYYMMDD format: */
+ res = parse_yearmonthday (pz);
+ }
+
+ else
+ res = parse_YMWD (pz);
+
+ if ((errno == 0) && (pT != NULL))
+ {
+ time_t val = parse_time (pT);
+ res = scale_n_add (res, val, 1);
+ }
+
+ free (fptr);
+ return res;
+}
+
+static time_t
+parse_non_iso8601 (cch_t * pz)
+{
+ whats_done_t whatd_we_do = NOTHING_IS_DONE;
+
+ time_t res = 0;
+
+ do {
+ time_t val;
+
+ errno = 0;
+ val = str_const_to_l (pz, &pz, 10);
+ if (errno != 0)
+ goto bad_time;
+
+ /* IF we find a colon, then we're going to have a seconds value.
+ We will not loop here any more. We cannot already have parsed
+ a minute value and if we've parsed an hour value, then the result
+ value has to be less than an hour. */
+ if (*pz == ':')
+ {
+ if (whatd_we_do >= MINUTE_IS_DONE)
+ break;
+
+ val = parse_hr_min_sec (val, pz);
+
+ if ((whatd_we_do == HOUR_IS_DONE) && (val >= SEC_PER_HR))
+ break;
+
+ return scale_n_add (res, val, 1);
+ }
+
+ {
+ unsigned int mult;
+
+ /* Skip over white space following the number we just parsed. */
+ while (isspace ((unsigned char)*pz))
+ pz++;
+
+ switch (*pz)
+ {
+ default: goto bad_time;
+ case NUL:
+ return scale_n_add (res, val, 1);
+
+ case 'y': case 'Y':
+ if (whatd_we_do >= YEAR_IS_DONE)
+ goto bad_time;
+ mult = SEC_PER_YEAR;
+ whatd_we_do = YEAR_IS_DONE;
+ break;
+
+ case 'M':
+ if (whatd_we_do >= MONTH_IS_DONE)
+ goto bad_time;
+ mult = SEC_PER_MONTH;
+ whatd_we_do = MONTH_IS_DONE;
+ break;
+
+ case 'W':
+ if (whatd_we_do >= WEEK_IS_DONE)
+ goto bad_time;
+ mult = SEC_PER_WEEK;
+ whatd_we_do = WEEK_IS_DONE;
+ break;
+
+ case 'd': case 'D':
+ if (whatd_we_do >= DAY_IS_DONE)
+ goto bad_time;
+ mult = SEC_PER_DAY;
+ whatd_we_do = DAY_IS_DONE;
+ break;
+
+ case 'h':
+ if (whatd_we_do >= HOUR_IS_DONE)
+ goto bad_time;
+ mult = SEC_PER_HR;
+ whatd_we_do = HOUR_IS_DONE;
+ break;
+
+ case 'm':
+ if (whatd_we_do >= MINUTE_IS_DONE)
+ goto bad_time;
+ mult = SEC_PER_MIN;
+ whatd_we_do = MINUTE_IS_DONE;
+ break;
+
+ case 's':
+ mult = 1;
+ whatd_we_do = SECOND_IS_DONE;
+ break;
+ }
+
+ res = scale_n_add (res, val, mult);
+
+ pz++;
+ while (isspace ((unsigned char)*pz))
+ pz++;
+ if (*pz == NUL)
+ return res;
+
+ if (! isdigit ((unsigned char)*pz))
+ break;
+ }
+
+ } while (whatd_we_do < SECOND_IS_DONE);
+
+ bad_time:
+ errno = EINVAL;
+ return BAD_TIME;
+}
+
+time_t
+parse_duration (char const * pz)
+{
+ while (isspace ((unsigned char)*pz))
+ pz++;
+
+ switch (*pz)
+ {
+ case 'P':
+ return parse_period (pz + 1);
+
+ case 'T':
+ return parse_time (pz + 1);
+
+ default:
+ if (isdigit ((unsigned char)*pz))
+ return parse_non_iso8601 (pz);
+
+ errno = EINVAL;
+ return BAD_TIME;
+ }
+}
+
+/*
+ * Local Variables:
+ * mode: C
+ * c-file-style: "gnu"
+ * indent-tabs-mode: nil
+ * End:
+ * end of parse-duration.c */