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