parse-duration.c revision 290001
1/* Parse a time duration and return a seconds count
2   Copyright (C) 2008-2015 Free Software Foundation, Inc.
3   Written by Bruce Korb <bkorb@gnu.org>, 2008.
4
5   This program is free software: you can redistribute it and/or modify
6   it under the terms of the GNU Lesser General Public License as published by
7   the Free Software Foundation; either version 2.1 of the License, or
8   (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU Lesser General Public License for more details.
14
15   You should have received a copy of the GNU Lesser General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
17
18#include <config.h>
19
20/* Specification.  */
21#include "parse-duration.h"
22
23#include <ctype.h>
24#include <errno.h>
25#include <limits.h>
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29
30#include "intprops.h"
31
32#ifndef NUL
33#define NUL '\0'
34#endif
35
36#define cch_t char const
37
38typedef enum {
39  NOTHING_IS_DONE,
40  YEAR_IS_DONE,
41  MONTH_IS_DONE,
42  WEEK_IS_DONE,
43  DAY_IS_DONE,
44  HOUR_IS_DONE,
45  MINUTE_IS_DONE,
46  SECOND_IS_DONE
47} whats_done_t;
48
49#define SEC_PER_MIN     60
50#define SEC_PER_HR      (SEC_PER_MIN * 60)
51#define SEC_PER_DAY     (SEC_PER_HR  * 24)
52#define SEC_PER_WEEK    (SEC_PER_DAY * 7)
53#define SEC_PER_MONTH   (SEC_PER_DAY * 30)
54#define SEC_PER_YEAR    (SEC_PER_DAY * 365)
55
56#undef  MAX_DURATION
57#define MAX_DURATION    TYPE_MAXIMUM(time_t)
58
59/* Wrapper around strtoul that does not require a cast.  */
60static unsigned long
61str_const_to_ul (cch_t * str, cch_t ** ppz, int base)
62{
63  return strtoul (str, (char **)ppz, base);
64}
65
66/* Wrapper around strtol that does not require a cast.  */
67static long
68str_const_to_l (cch_t * str, cch_t ** ppz, int base)
69{
70  return strtol (str, (char **)ppz, base);
71}
72
73/* Returns BASE + VAL * SCALE, interpreting BASE = BAD_TIME
74   with errno set as an error situation, and returning BAD_TIME
75   with errno set in an error situation.  */
76static time_t
77scale_n_add (time_t base, time_t val, int scale)
78{
79  if (base == BAD_TIME)
80    {
81      if (errno == 0)
82        errno = EINVAL;
83      return BAD_TIME;
84    }
85
86  if (val > MAX_DURATION / scale)
87    {
88      errno = ERANGE;
89      return BAD_TIME;
90    }
91
92  val *= scale;
93  if (base > MAX_DURATION - val)
94    {
95      errno = ERANGE;
96      return BAD_TIME;
97    }
98
99  return base + val;
100}
101
102/* After a number HH has been parsed, parse subsequent :MM or :MM:SS.  */
103static time_t
104parse_hr_min_sec (time_t start, cch_t * pz)
105{
106  int lpct = 0;
107
108  errno = 0;
109
110  /* For as long as our scanner pointer points to a colon *AND*
111     we've not looped before, then keep looping.  (two iterations max) */
112  while ((*pz == ':') && (lpct++ <= 1))
113    {
114      unsigned long v = str_const_to_ul (pz+1, &pz, 10);
115
116      if (errno != 0)
117        return BAD_TIME;
118
119      start = scale_n_add (v, start, 60);
120
121      if (errno != 0)
122        return BAD_TIME;
123    }
124
125  /* allow for trailing spaces */
126  while (isspace ((unsigned char)*pz))
127    pz++;
128  if (*pz != NUL)
129    {
130      errno = EINVAL;
131      return BAD_TIME;
132    }
133
134  return start;
135}
136
137/* Parses a value and returns BASE + value * SCALE, interpreting
138   BASE = BAD_TIME with errno set as an error situation, and returning
139   BAD_TIME with errno set in an error situation.  */
140static time_t
141parse_scaled_value (time_t base, cch_t ** ppz, cch_t * endp, int scale)
142{
143  cch_t * pz = *ppz;
144  time_t val;
145
146  if (base == BAD_TIME)
147    return base;
148
149  errno = 0;
150  val = str_const_to_ul (pz, &pz, 10);
151  if (errno != 0)
152    return BAD_TIME;
153  while (isspace ((unsigned char)*pz))
154    pz++;
155  if (pz != endp)
156    {
157      errno = EINVAL;
158      return BAD_TIME;
159    }
160
161  *ppz = pz;
162  return scale_n_add (base, val, scale);
163}
164
165/* Parses the syntax YEAR-MONTH-DAY.
166   PS points into the string, after "YEAR", before "-MONTH-DAY".  */
167static time_t
168parse_year_month_day (cch_t * pz, cch_t * ps)
169{
170  time_t res = 0;
171
172  res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
173
174  pz++; /* over the first '-' */
175  ps = strchr (pz, '-');
176  if (ps == NULL)
177    {
178      errno = EINVAL;
179      return BAD_TIME;
180    }
181  res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
182
183  pz++; /* over the second '-' */
184  ps = pz + strlen (pz);
185  return parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
186}
187
188/* Parses the syntax YYYYMMDD.  */
189static time_t
190parse_yearmonthday (cch_t * in_pz)
191{
192  time_t res = 0;
193  char   buf[8];
194  cch_t * pz;
195
196  if (strlen (in_pz) != 8)
197    {
198      errno = EINVAL;
199      return BAD_TIME;
200    }
201
202  memcpy (buf, in_pz, 4);
203  buf[4] = NUL;
204  pz = buf;
205  res = parse_scaled_value (0, &pz, buf + 4, SEC_PER_YEAR);
206
207  memcpy (buf, in_pz + 4, 2);
208  buf[2] = NUL;
209  pz =   buf;
210  res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MONTH);
211
212  memcpy (buf, in_pz + 6, 2);
213  buf[2] = NUL;
214  pz =   buf;
215  return parse_scaled_value (res, &pz, buf + 2, SEC_PER_DAY);
216}
217
218/* Parses the syntax yy Y mm M ww W dd D.  */
219static time_t
220parse_YMWD (cch_t * pz)
221{
222  time_t res = 0;
223  cch_t * ps = strchr (pz, 'Y');
224  if (ps != NULL)
225    {
226      res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
227      pz++;
228    }
229
230  ps = strchr (pz, 'M');
231  if (ps != NULL)
232    {
233      res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
234      pz++;
235    }
236
237  ps = strchr (pz, 'W');
238  if (ps != NULL)
239    {
240      res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK);
241      pz++;
242    }
243
244  ps = strchr (pz, 'D');
245  if (ps != NULL)
246    {
247      res = parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
248      pz++;
249    }
250
251  while (isspace ((unsigned char)*pz))
252    pz++;
253  if (*pz != NUL)
254    {
255      errno = EINVAL;
256      return BAD_TIME;
257    }
258
259  return res;
260}
261
262/* Parses the syntax HH:MM:SS.
263   PS points into the string, after "HH", before ":MM:SS".  */
264static time_t
265parse_hour_minute_second (cch_t * pz, cch_t * ps)
266{
267  time_t res = 0;
268
269  res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
270
271  pz++;
272  ps = strchr (pz, ':');
273  if (ps == NULL)
274    {
275      errno = EINVAL;
276      return BAD_TIME;
277    }
278
279  res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
280
281  pz++;
282  ps = pz + strlen (pz);
283  return parse_scaled_value (res, &pz, ps, 1);
284}
285
286/* Parses the syntax HHMMSS.  */
287static time_t
288parse_hourminutesecond (cch_t * in_pz)
289{
290  time_t res = 0;
291  char   buf[4];
292  cch_t * pz;
293
294  if (strlen (in_pz) != 6)
295    {
296      errno = EINVAL;
297      return BAD_TIME;
298    }
299
300  memcpy (buf, in_pz, 2);
301  buf[2] = NUL;
302  pz = buf;
303  res = parse_scaled_value (0, &pz, buf + 2, SEC_PER_HR);
304
305  memcpy (buf, in_pz + 2, 2);
306  buf[2] = NUL;
307  pz =   buf;
308  res = parse_scaled_value (res, &pz, buf + 2, SEC_PER_MIN);
309
310  memcpy (buf, in_pz + 4, 2);
311  buf[2] = NUL;
312  pz =   buf;
313  return parse_scaled_value (res, &pz, buf + 2, 1);
314}
315
316/* Parses the syntax hh H mm M ss S.  */
317static time_t
318parse_HMS (cch_t * pz)
319{
320  time_t res = 0;
321  cch_t * ps = strchr (pz, 'H');
322  if (ps != NULL)
323    {
324      res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
325      pz++;
326    }
327
328  ps = strchr (pz, 'M');
329  if (ps != NULL)
330    {
331      res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
332      pz++;
333    }
334
335  ps = strchr (pz, 'S');
336  if (ps != NULL)
337    {
338      res = parse_scaled_value (res, &pz, ps, 1);
339      pz++;
340    }
341
342  while (isspace ((unsigned char)*pz))
343    pz++;
344  if (*pz != NUL)
345    {
346      errno = EINVAL;
347      return BAD_TIME;
348    }
349
350  return res;
351}
352
353/* Parses a time (hours, minutes, seconds) specification in either syntax.  */
354static time_t
355parse_time (cch_t * pz)
356{
357  cch_t * ps;
358  time_t  res = 0;
359
360  /*
361   *  Scan for a hyphen
362   */
363  ps = strchr (pz, ':');
364  if (ps != NULL)
365    {
366      res = parse_hour_minute_second (pz, ps);
367    }
368
369  /*
370   *  Try for a 'H', 'M' or 'S' suffix
371   */
372  else if (ps = strpbrk (pz, "HMS"),
373           ps == NULL)
374    {
375      /* Its a YYYYMMDD format: */
376      res = parse_hourminutesecond (pz);
377    }
378
379  else
380    res = parse_HMS (pz);
381
382  return res;
383}
384
385/* Returns a substring of the given string, with spaces at the beginning and at
386   the end destructively removed, per SNOBOL.  */
387static char *
388trim (char * pz)
389{
390  /* trim leading white space */
391  while (isspace ((unsigned char)*pz))
392    pz++;
393
394  /* trim trailing white space */
395  {
396    char * pe = pz + strlen (pz);
397    while ((pe > pz) && isspace ((unsigned char)pe[-1]))
398      pe--;
399    *pe = NUL;
400  }
401
402  return pz;
403}
404
405/*
406 *  Parse the year/months/days of a time period
407 */
408static time_t
409parse_period (cch_t * in_pz)
410{
411  char * pT;
412  char * ps;
413  char * pz   = strdup (in_pz);
414  void * fptr = pz;
415  time_t res  = 0;
416
417  if (pz == NULL)
418    {
419      errno = ENOMEM;
420      return BAD_TIME;
421    }
422
423  pT = strchr (pz, 'T');
424  if (pT != NULL)
425    {
426      *(pT++) = NUL;
427      pz = trim (pz);
428      pT = trim (pT);
429    }
430
431  /*
432   *  Scan for a hyphen
433   */
434  ps = strchr (pz, '-');
435  if (ps != NULL)
436    {
437      res = parse_year_month_day (pz, ps);
438    }
439
440  /*
441   *  Try for a 'Y', 'M' or 'D' suffix
442   */
443  else if (ps = strpbrk (pz, "YMWD"),
444           ps == NULL)
445    {
446      /* Its a YYYYMMDD format: */
447      res = parse_yearmonthday (pz);
448    }
449
450  else
451    res = parse_YMWD (pz);
452
453  if ((errno == 0) && (pT != NULL))
454    {
455      time_t val = parse_time (pT);
456      res = scale_n_add (res, val, 1);
457    }
458
459  free (fptr);
460  return res;
461}
462
463static time_t
464parse_non_iso8601 (cch_t * pz)
465{
466  whats_done_t whatd_we_do = NOTHING_IS_DONE;
467
468  time_t res = 0;
469
470  do  {
471    time_t val;
472
473    errno = 0;
474    val = str_const_to_l (pz, &pz, 10);
475    if (errno != 0)
476      goto bad_time;
477
478    /*  IF we find a colon, then we're going to have a seconds value.
479        We will not loop here any more.  We cannot already have parsed
480        a minute value and if we've parsed an hour value, then the result
481        value has to be less than an hour. */
482    if (*pz == ':')
483      {
484        if (whatd_we_do >= MINUTE_IS_DONE)
485          break;
486
487        val = parse_hr_min_sec (val, pz);
488
489        if ((whatd_we_do == HOUR_IS_DONE) && (val >= SEC_PER_HR))
490          break;
491
492        return scale_n_add (res, val, 1);
493      }
494
495    {
496      unsigned int mult;
497
498      /*  Skip over white space following the number we just parsed. */
499      while (isspace ((unsigned char)*pz))
500        pz++;
501
502      switch (*pz)
503        {
504        default:  goto bad_time;
505        case NUL:
506          return scale_n_add (res, val, 1);
507
508        case 'y': case 'Y':
509          if (whatd_we_do >= YEAR_IS_DONE)
510            goto bad_time;
511          mult = SEC_PER_YEAR;
512          whatd_we_do = YEAR_IS_DONE;
513          break;
514
515        case 'M':
516          if (whatd_we_do >= MONTH_IS_DONE)
517            goto bad_time;
518          mult = SEC_PER_MONTH;
519          whatd_we_do = MONTH_IS_DONE;
520          break;
521
522        case 'W':
523          if (whatd_we_do >= WEEK_IS_DONE)
524            goto bad_time;
525          mult = SEC_PER_WEEK;
526          whatd_we_do = WEEK_IS_DONE;
527          break;
528
529        case 'd': case 'D':
530          if (whatd_we_do >= DAY_IS_DONE)
531            goto bad_time;
532          mult = SEC_PER_DAY;
533          whatd_we_do = DAY_IS_DONE;
534          break;
535
536        case 'h':
537          if (whatd_we_do >= HOUR_IS_DONE)
538            goto bad_time;
539          mult = SEC_PER_HR;
540          whatd_we_do = HOUR_IS_DONE;
541          break;
542
543        case 'm':
544          if (whatd_we_do >= MINUTE_IS_DONE)
545            goto bad_time;
546          mult = SEC_PER_MIN;
547          whatd_we_do = MINUTE_IS_DONE;
548          break;
549
550        case 's':
551          mult = 1;
552          whatd_we_do = SECOND_IS_DONE;
553          break;
554        }
555
556      res = scale_n_add (res, val, mult);
557
558      pz++;
559      while (isspace ((unsigned char)*pz))
560        pz++;
561      if (*pz == NUL)
562        return res;
563
564      if (! isdigit ((unsigned char)*pz))
565        break;
566    }
567
568  } while (whatd_we_do < SECOND_IS_DONE);
569
570 bad_time:
571  errno = EINVAL;
572  return BAD_TIME;
573}
574
575time_t
576parse_duration (char const * pz)
577{
578  while (isspace ((unsigned char)*pz))
579    pz++;
580
581  switch (*pz)
582    {
583    case 'P':
584      return parse_period (pz + 1);
585
586    case 'T':
587      return parse_time (pz + 1);
588
589    default:
590      if (isdigit ((unsigned char)*pz))
591        return parse_non_iso8601 (pz);
592
593      errno = EINVAL;
594      return BAD_TIME;
595    }
596}
597
598/*
599 * Local Variables:
600 * mode: C
601 * c-file-style: "gnu"
602 * indent-tabs-mode: nil
603 * End:
604 * end of parse-duration.c */
605