xfreopen need not depend on freopen-safer
[gnulib.git] / lib / parse-duration.c
blobbb58b7c1eb91b2bcd39dec85e881ce02228d77d8
1 /* Parse a time duration and return a seconds count
2 Copyright (C) 2008-2019 Free Software Foundation, Inc.
3 Written by Bruce Korb <bkorb@gnu.org>, 2008.
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.
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.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>. */
18 #include <config.h>
20 /* Specification. */
21 #include "parse-duration.h"
23 #include <ctype.h>
24 #include <errno.h>
25 #include <limits.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
30 #include "intprops.h"
32 #ifndef NUL
33 #define NUL '\0'
34 #endif
36 #define cch_t char const
38 typedef 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;
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)
56 #undef MAX_DURATION
57 #define MAX_DURATION TYPE_MAXIMUM(time_t)
59 /* Wrapper around strtoul that does not require a cast. */
60 static unsigned long
61 str_const_to_ul (cch_t * str, cch_t ** ppz, int base)
63 return strtoul (str, (char **)ppz, base);
66 /* Wrapper around strtol that does not require a cast. */
67 static long
68 str_const_to_l (cch_t * str, cch_t ** ppz, int base)
70 return strtol (str, (char **)ppz, base);
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. */
76 static time_t
77 scale_n_add (time_t base, time_t val, int scale)
79 if (base == BAD_TIME)
81 if (errno == 0)
82 errno = EINVAL;
83 return BAD_TIME;
86 if (val > MAX_DURATION / scale)
88 errno = ERANGE;
89 return BAD_TIME;
92 val *= scale;
93 if (base > MAX_DURATION - val)
95 errno = ERANGE;
96 return BAD_TIME;
99 return base + val;
102 /* After a number HH has been parsed, parse subsequent :MM or :MM:SS. */
103 static time_t
104 parse_hr_min_sec (time_t start, cch_t * pz)
106 int lpct = 0;
108 errno = 0;
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))
114 unsigned long v = str_const_to_ul (pz+1, &pz, 10);
116 if (errno != 0)
117 return BAD_TIME;
119 start = scale_n_add (v, start, 60);
121 if (errno != 0)
122 return BAD_TIME;
125 /* allow for trailing spaces */
126 while (isspace ((unsigned char)*pz))
127 pz++;
128 if (*pz != NUL)
130 errno = EINVAL;
131 return BAD_TIME;
134 return start;
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. */
140 static time_t
141 parse_scaled_value (time_t base, cch_t ** ppz, cch_t * endp, int scale)
143 cch_t * pz = *ppz;
144 time_t val;
146 if (base == BAD_TIME)
147 return base;
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)
157 errno = EINVAL;
158 return BAD_TIME;
161 *ppz = pz;
162 return scale_n_add (base, val, scale);
165 /* Parses the syntax YEAR-MONTH-DAY.
166 PS points into the string, after "YEAR", before "-MONTH-DAY". */
167 static time_t
168 parse_year_month_day (cch_t * pz, cch_t * ps)
170 time_t res = 0;
172 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
174 pz++; /* over the first '-' */
175 ps = strchr (pz, '-');
176 if (ps == NULL)
178 errno = EINVAL;
179 return BAD_TIME;
181 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
183 pz++; /* over the second '-' */
184 ps = pz + strlen (pz);
185 return parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
188 /* Parses the syntax YYYYMMDD. */
189 static time_t
190 parse_yearmonthday (cch_t * in_pz)
192 time_t res = 0;
193 char buf[8];
194 cch_t * pz;
196 if (strlen (in_pz) != 8)
198 errno = EINVAL;
199 return BAD_TIME;
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);
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);
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);
218 /* Parses the syntax yy Y mm M ww W dd D. */
219 static time_t
220 parse_YMWD (cch_t * pz)
222 time_t res = 0;
223 cch_t * ps = strchr (pz, 'Y');
224 if (ps != NULL)
226 res = parse_scaled_value (0, &pz, ps, SEC_PER_YEAR);
227 pz++;
230 ps = strchr (pz, 'M');
231 if (ps != NULL)
233 res = parse_scaled_value (res, &pz, ps, SEC_PER_MONTH);
234 pz++;
237 ps = strchr (pz, 'W');
238 if (ps != NULL)
240 res = parse_scaled_value (res, &pz, ps, SEC_PER_WEEK);
241 pz++;
244 ps = strchr (pz, 'D');
245 if (ps != NULL)
247 res = parse_scaled_value (res, &pz, ps, SEC_PER_DAY);
248 pz++;
251 while (isspace ((unsigned char)*pz))
252 pz++;
253 if (*pz != NUL)
255 errno = EINVAL;
256 return BAD_TIME;
259 return res;
262 /* Parses the syntax HH:MM:SS.
263 PS points into the string, after "HH", before ":MM:SS". */
264 static time_t
265 parse_hour_minute_second (cch_t * pz, cch_t * ps)
267 time_t res = 0;
269 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
271 pz++;
272 ps = strchr (pz, ':');
273 if (ps == NULL)
275 errno = EINVAL;
276 return BAD_TIME;
279 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
281 pz++;
282 ps = pz + strlen (pz);
283 return parse_scaled_value (res, &pz, ps, 1);
286 /* Parses the syntax HHMMSS. */
287 static time_t
288 parse_hourminutesecond (cch_t * in_pz)
290 time_t res = 0;
291 char buf[4];
292 cch_t * pz;
294 if (strlen (in_pz) != 6)
296 errno = EINVAL;
297 return BAD_TIME;
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);
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);
310 memcpy (buf, in_pz + 4, 2);
311 buf[2] = NUL;
312 pz = buf;
313 return parse_scaled_value (res, &pz, buf + 2, 1);
316 /* Parses the syntax hh H mm M ss S. */
317 static time_t
318 parse_HMS (cch_t * pz)
320 time_t res = 0;
321 cch_t * ps = strchr (pz, 'H');
322 if (ps != NULL)
324 res = parse_scaled_value (0, &pz, ps, SEC_PER_HR);
325 pz++;
328 ps = strchr (pz, 'M');
329 if (ps != NULL)
331 res = parse_scaled_value (res, &pz, ps, SEC_PER_MIN);
332 pz++;
335 ps = strchr (pz, 'S');
336 if (ps != NULL)
338 res = parse_scaled_value (res, &pz, ps, 1);
339 pz++;
342 while (isspace ((unsigned char)*pz))
343 pz++;
344 if (*pz != NUL)
346 errno = EINVAL;
347 return BAD_TIME;
350 return res;
353 /* Parses a time (hours, minutes, seconds) specification in either syntax. */
354 static time_t
355 parse_time (cch_t * pz)
357 cch_t * ps;
358 time_t res = 0;
361 * Scan for a hyphen
363 ps = strchr (pz, ':');
364 if (ps != NULL)
366 res = parse_hour_minute_second (pz, ps);
370 * Try for a 'H', 'M' or 'S' suffix
372 else if (ps = strpbrk (pz, "HMS"),
373 ps == NULL)
375 /* Its a YYYYMMDD format: */
376 res = parse_hourminutesecond (pz);
379 else
380 res = parse_HMS (pz);
382 return res;
385 /* Returns a substring of the given string, with spaces at the beginning and at
386 the end destructively removed, per SNOBOL. */
387 static char *
388 trim (char * pz)
390 /* trim leading white space */
391 while (isspace ((unsigned char)*pz))
392 pz++;
394 /* trim trailing white space */
396 char * pe = pz + strlen (pz);
397 while ((pe > pz) && isspace ((unsigned char)pe[-1]))
398 pe--;
399 *pe = NUL;
402 return pz;
406 * Parse the year/months/days of a time period
408 static time_t
409 parse_period (cch_t * in_pz)
411 char * pT;
412 char * ps;
413 char * pz = strdup (in_pz);
414 void * fptr = pz;
415 time_t res = 0;
417 if (pz == NULL)
419 errno = ENOMEM;
420 return BAD_TIME;
423 pT = strchr (pz, 'T');
424 if (pT != NULL)
426 *(pT++) = NUL;
427 pz = trim (pz);
428 pT = trim (pT);
432 * Scan for a hyphen
434 ps = strchr (pz, '-');
435 if (ps != NULL)
437 res = parse_year_month_day (pz, ps);
441 * Try for a 'Y', 'M' or 'D' suffix
443 else if (ps = strpbrk (pz, "YMWD"),
444 ps == NULL)
446 /* Its a YYYYMMDD format: */
447 res = parse_yearmonthday (pz);
450 else
451 res = parse_YMWD (pz);
453 if ((errno == 0) && (pT != NULL))
455 time_t val = parse_time (pT);
456 res = scale_n_add (res, val, 1);
459 free (fptr);
460 return res;
463 static time_t
464 parse_non_iso8601 (cch_t * pz)
466 whats_done_t whatd_we_do = NOTHING_IS_DONE;
468 time_t res = 0;
470 do {
471 time_t val;
473 errno = 0;
474 val = str_const_to_l (pz, &pz, 10);
475 if (errno != 0)
476 goto bad_time;
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 == ':')
484 if (whatd_we_do >= MINUTE_IS_DONE)
485 break;
487 val = parse_hr_min_sec (val, pz);
489 if ((whatd_we_do == HOUR_IS_DONE) && (val >= SEC_PER_HR))
490 break;
492 return scale_n_add (res, val, 1);
496 unsigned int mult;
498 /* Skip over white space following the number we just parsed. */
499 while (isspace ((unsigned char)*pz))
500 pz++;
502 switch (*pz)
504 default: goto bad_time;
505 case NUL:
506 return scale_n_add (res, val, 1);
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;
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;
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;
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;
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;
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;
550 case 's':
551 mult = 1;
552 whatd_we_do = SECOND_IS_DONE;
553 break;
556 res = scale_n_add (res, val, mult);
558 pz++;
559 while (isspace ((unsigned char)*pz))
560 pz++;
561 if (*pz == NUL)
562 return res;
564 if (! isdigit ((unsigned char)*pz))
565 break;
568 } while (whatd_we_do < SECOND_IS_DONE);
570 bad_time:
571 errno = EINVAL;
572 return BAD_TIME;
575 time_t
576 parse_duration (char const * pz)
578 while (isspace ((unsigned char)*pz))
579 pz++;
581 switch (*pz)
583 case 'P':
584 return parse_period (pz + 1);
586 case 'T':
587 return parse_time (pz + 1);
589 default:
590 if (isdigit ((unsigned char)*pz))
591 return parse_non_iso8601 (pz);
593 errno = EINVAL;
594 return BAD_TIME;
599 * Local Variables:
600 * mode: C
601 * c-file-style: "gnu"
602 * indent-tabs-mode: nil
603 * End:
604 * end of parse-duration.c */