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