2 * rrd_parsetime.c - parse time for at(1)
3 * Copyright (C) 1993, 1994 Thomas Koenig
5 * modifications for English-language times
6 * Copyright (C) 1993 David Parsons
8 * A lot of modifications and extensions
9 * (including the new syntax being useful for RRDB)
10 * Copyright (C) 1999 Oleg Cherevko (aka Olwi Deer)
12 * severe structural damage inflicted by Tobi Oetiker in 1999
14 * Redistribution and use in source and binary forms, with or without
15 * modification, are permitted provided that the following conditions
17 * 1. Redistributions of source code must retain the above copyright
18 * notice, this list of conditions and the following disclaimer.
19 * 2. The name of the author(s) may not be used to endorse or promote
20 * products derived from this software without specific prior written
23 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
24 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
25 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
26 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
27 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
28 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30 * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
32 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 /* NOTE: nothing in here is thread-safe!!!! Not even the localtime
39 * The BNF-like specification of the time syntax parsed is below:
41 * As usual, [ X ] means that X is optional, { X } means that X may
42 * be either omitted or specified as many times as needed,
43 * alternatives are separated by |, brackets are used for grouping.
44 * (# marks the beginning of comment that extends to the end of line)
46 * TIME-SPECIFICATION ::= TIME-REFERENCE [ OFFSET-SPEC ] |
48 * ( START | END ) OFFSET-SPEC
50 * TIME-REFERENCE ::= NOW | TIME-OF-DAY-SPEC [ DAY-SPEC-1 ] |
51 * [ TIME-OF-DAY-SPEC ] DAY-SPEC-2
53 * TIME-OF-DAY-SPEC ::= NUMBER (':') NUMBER [am|pm] | # HH:MM
54 * 'noon' | 'midnight' | 'teatime'
56 * DAY-SPEC-1 ::= NUMBER '/' NUMBER '/' NUMBER | # MM/DD/[YY]YY
57 * NUMBER '.' NUMBER '.' NUMBER | # DD.MM.[YY]YY
58 * NUMBER # Seconds since 1970
61 * DAY-SPEC-2 ::= MONTH-NAME NUMBER [NUMBER] | # Month DD [YY]YY
62 * 'yesterday' | 'today' | 'tomorrow' |
66 * OFFSET-SPEC ::= '+'|'-' NUMBER TIME-UNIT { ['+'|'-'] NUMBER TIME-UNIT }
68 * TIME-UNIT ::= SECONDS | MINUTES | HOURS |
69 * DAYS | WEEKS | MONTHS | YEARS
73 * START ::= 'start' | 's'
76 * SECONDS ::= 'seconds' | 'second' | 'sec' | 's'
77 * MINUTES ::= 'minutes' | 'minute' | 'min' | 'm'
78 * HOURS ::= 'hours' | 'hour' | 'hr' | 'h'
79 * DAYS ::= 'days' | 'day' | 'd'
80 * WEEKS ::= 'weeks' | 'week' | 'wk' | 'w'
81 * MONTHS ::= 'months' | 'month' | 'mon' | 'm'
82 * YEARS ::= 'years' | 'year' | 'yr' | 'y'
84 * MONTH-NAME ::= 'jan' | 'january' | 'feb' | 'february' | 'mar' | 'march' |
85 * 'apr' | 'april' | 'may' | 'jun' | 'june' | 'jul' | 'july' |
86 * 'aug' | 'august' | 'sep' | 'september' | 'oct' | 'october' |
87 * 'nov' | 'november' | 'dec' | 'december'
89 * DAY-OF-WEEK ::= 'sunday' | 'sun' | 'monday' | 'mon' | 'tuesday' | 'tue' |
90 * 'wednesday' | 'wed' | 'thursday' | 'thu' | 'friday' | 'fri' |
94 * As you may note, there is an ambiguity with respect to
95 * the 'm' time unit (which can mean either minutes or months).
96 * To cope with this, code tries to read users mind :) by applying
97 * certain heuristics. There are two of them:
99 * 1. If 'm' is used in context of (i.e. right after the) years,
100 * months, weeks, or days it is assumed to mean months, while
101 * in the context of hours, minutes, and seconds it means minutes.
102 * (e.g., in -1y6m or +3w1m 'm' means 'months', while in
103 * -3h20m or +5s2m 'm' means 'minutes')
105 * 2. Out of context (i.e. right after the '+' or '-' sign) the
106 * meaning of 'm' is guessed from the number it directly follows.
107 * Currently, if the number absolute value is below 25 it is assumed
108 * that 'm' means months, otherwise it is treated as minutes.
109 * (e.g., -25m == -25 minutes, while +24m == +24 months)
121 #include "rrd_tool.h"
123 /* Structures and unions */
126 MIDNIGHT, NOON, TEATIME,
127 PM, AM, YESTERDAY, TODAY, TOMORROW, NOW, START, END, EPOCH,
128 SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS,
130 NUMBER, PLUS, MINUS, DOT, COLON, SLASH, ID, JUNK,
131 JAN, FEB, MAR, APR, MAY, JUN,
132 JUL, AUG, SEP, OCT, NOV, DEC,
133 SUN, MON, TUE, WED, THU, FRI, SAT
136 /* the below is for plus_minus() */
137 #define PREVIOUS_OP (-1)
139 /* parse translation table - table driven parsers can be your FRIEND!
141 struct SpecialToken {
142 char *name; /* token name */
143 int value; /* token id */
145 static const struct SpecialToken VariousWords[] = {
146 {"midnight", MIDNIGHT}, /* 00:00:00 of today or tomorrow */
147 {"noon", NOON}, /* 12:00:00 of today or tomorrow */
148 {"teatime", TEATIME}, /* 16:00:00 of today or tomorrow */
149 {"am", AM}, /* morning times for 0-12 clock */
150 {"pm", PM}, /* evening times for 0-12 clock */
151 {"tomorrow", TOMORROW},
152 {"yesterday", YESTERDAY},
200 {NULL, 0} /*** SENTINEL ***/
203 static const struct SpecialToken TimeMultipliers[] = {
204 {"second", SECONDS}, /* seconds multiplier */
205 {"seconds", SECONDS}, /* (pluralized) */
206 {"sec", SECONDS}, /* (generic) */
207 {"s", SECONDS}, /* (short generic) */
208 {"minute", MINUTES}, /* minutes multiplier */
209 {"minutes", MINUTES}, /* (pluralized) */
210 {"min", MINUTES}, /* (generic) */
211 {"m", MONTHS_MINUTES}, /* (short generic) */
212 {"hour", HOURS}, /* hours ... */
213 {"hours", HOURS}, /* (pluralized) */
214 {"hr", HOURS}, /* (generic) */
215 {"h", HOURS}, /* (short generic) */
216 {"day", DAYS}, /* days ... */
217 {"days", DAYS}, /* (pluralized) */
218 {"d", DAYS}, /* (short generic) */
219 {"week", WEEKS}, /* week ... */
220 {"weeks", WEEKS}, /* (pluralized) */
221 {"wk", WEEKS}, /* (generic) */
222 {"w", WEEKS}, /* (short generic) */
223 {"month", MONTHS}, /* week ... */
224 {"months", MONTHS}, /* (pluralized) */
225 {"mon", MONTHS}, /* (generic) */
226 {"year", YEARS}, /* year ... */
227 {"years", YEARS}, /* (pluralized) */
228 {"yr", YEARS}, /* (generic) */
229 {"y", YEARS}, /* (short generic) */
230 {NULL, 0} /*** SENTINEL ***/
233 /* File scope variables */
235 /* context dependent list of specials for parser to recognize,
236 * required for us to be able distinguish between 'mon' as 'month'
237 * and 'mon' as 'monday'
239 static const struct SpecialToken *Specials;
241 static const char **scp; /* scanner - pointer at arglist */
242 static char scc; /* scanner - count of remaining arguments */
243 static const char *sct; /* scanner - next char pointer in current argument */
244 static int need; /* scanner - need to advance to next argument */
246 static char *sc_token = NULL; /* scanner - token buffer */
247 static size_t sc_len; /* scanner - length of token buffer */
248 static int sc_tokid; /* scanner - token id */
250 /* Local functions */
251 static void EnsureMemFree(
254 static void EnsureMemFree(
264 * A hack to compensate for the lack of the C++ exceptions
266 * Every function func that might generate parsing "exception"
267 * should return TIME_OK (aka NULL) or pointer to the error message,
268 * and should be called like this: try(func(args));
270 * if the try is not successful it will reset the token pointer ...
272 * [NOTE: when try(...) is used as the only statement in the "if-true"
273 * part of the if statement that also has an "else" part it should be
274 * either enclosed in the curly braces (despite the fact that it looks
275 * like a single statement) or NOT followed by the ";"]
287 * The panic() function was used in the original code to die, we redefine
288 * it as macro to start the chain of ascending returns that in conjunction
289 * with the try(b) above will simulate a sort of "exception handling"
297 * ve() and e() are used to set the return error,
298 * the most appropriate use for these is inside panic(...)
300 #define MAX_ERR_MSG_LEN 1024
301 static char errmsg[MAX_ERR_MSG_LEN];
307 #ifdef HAVE_VSNPRINTF
308 vsnprintf(errmsg, MAX_ERR_MSG_LEN, fmt, ap);
310 vsprintf(errmsg, fmt, ap);
329 /* Compare S1 and S2, ignoring case, returning less than, equal to or
330 greater than zero if S1 is lexicographically less than,
331 equal to or greater than S2. -- copied from GNU libc*/
332 static int mystrcasecmp(
336 const unsigned char *p1 = (const unsigned char *) s1;
337 const unsigned char *p2 = (const unsigned char *) s2;
338 unsigned char c1, c2;
355 * parse a token, checking if it's something special to us
357 static int parse_token(
362 for (i = 0; Specials[i].name != NULL; i++)
363 if (mystrcasecmp(Specials[i].name, arg) == 0)
364 return sc_tokid = Specials[i].value;
366 /* not special - must be some random id */
367 return sc_tokid = ID;
373 * init_scanner() sets up the scanner to eat arguments
375 static char *init_scanner(
384 sc_len += strlen(*argv++);
386 sc_token = (char *) malloc(sc_len * sizeof(char));
387 if (sc_token == NULL)
388 return "Failed to allocate memory";
393 * token() fetches a token from the input stream
401 memset(sc_token, '\0', sc_len);
405 /* if we need to read another argument, walk along the argument list;
406 * when we fall off the arglist, we'll just return EOF forever
416 /* eat whitespace now - if we walk off the end of the argument,
417 * we'll continue, which puts us up at the top of the while loop
418 * to fetch the next argument in
420 while (isspace((unsigned char) *sct) || *sct == '_' || *sct == ',')
427 /* preserve the first character of the new token
429 sc_token[0] = *sct++;
431 /* then see what it is
433 if (isdigit((unsigned char) (sc_token[0]))) {
434 while (isdigit((unsigned char) (*sct)))
435 sc_token[++idx] = *sct++;
436 sc_token[++idx] = '\0';
437 return sc_tokid = NUMBER;
438 } else if (isalpha((unsigned char) (sc_token[0]))) {
439 while (isalpha((unsigned char) (*sct)))
440 sc_token[++idx] = *sct++;
441 sc_token[++idx] = '\0';
442 return parse_token(sc_token);
444 switch (sc_token[0]) {
446 return sc_tokid = COLON;
448 return sc_tokid = DOT;
450 return sc_tokid = PLUS;
452 return sc_tokid = MINUS;
454 return sc_tokid = SLASH;
456 /*OK, we did not make it ... */
458 return sc_tokid = EOF;
465 * expect2() gets a token and complains if it's not the token we want
467 static char *expect2(
474 va_start(ap, complain_fmt);
475 if (token() != desired) {
476 panic(ve(complain_fmt, ap));
485 * plus_minus() is used to parse a single NUMBER TIME-UNIT pair
486 * for the OFFSET-SPEC.
487 * It also applies those m-guessing heuristics.
489 static char *plus_minus(
490 rrd_time_value_t * ptv,
493 static int op = PLUS;
494 static int prev_multiplier = -1;
500 (NUMBER, "There should be number after '%c'",
501 op == PLUS ? '+' : '-'));
502 prev_multiplier = -1; /* reset months-minutes guessing mechanics */
504 /* if doop is < 0 then we repeat the previous op
505 * with the prefetched number */
507 delta = atoi(sc_token);
509 if (token() == MONTHS_MINUTES) {
510 /* hard job to guess what does that -5m means: -5mon or -5min? */
511 switch (prev_multiplier) {
526 if (delta < 6) /* it may be some other value but in the context
527 * of RRD who needs less than 6 min deltas? */
533 prev_multiplier = sc_tokid;
536 ptv->tm. tm_year += (
537 op == PLUS) ? delta : -delta;
542 op == PLUS) ? delta : -delta;
549 ptv->tm. tm_mday += (
550 op == PLUS) ? delta : -delta;
554 ptv->offset += (op == PLUS) ? delta * 60 * 60 : -delta * 60 * 60;
557 ptv->offset += (op == PLUS) ? delta * 60 : -delta * 60;
560 ptv->offset += (op == PLUS) ? delta : -delta;
562 default: /*default unit is seconds */
563 ptv->offset += (op == PLUS) ? delta : -delta;
566 panic(e("well-known time unit expected after %d", delta));
568 return TIME_OK; /* to make compiler happy :) */
573 * tod() computes the time of day (TIME-OF-DAY-SPEC)
576 rrd_time_value_t * ptv)
578 int hour, minute = 0;
581 /* save token status in case we must abort */
583 const char *sct_sv = sct;
584 int sc_tokid_sv = sc_tokid;
586 tlen = strlen(sc_token);
588 /* first pick out the time of day - we assume a HH (COLON|DOT) MM time
594 hour = atoi(sc_token);
597 if (sc_tokid == SLASH || sc_tokid == DOT) {
598 /* guess we are looking at a date */
601 sc_tokid = sc_tokid_sv;
602 snprintf(sc_token, sc_len, "%d", hour);
605 if (sc_tokid == COLON) {
607 "Parsing HH:MM syntax, expecting MM as number, got none"));
608 minute = atoi(sc_token);
610 panic(e("parsing HH:MM syntax, got MM = %d (>59!)", minute));
615 /* check if an AM or PM specifier was given
617 if (sc_tokid == AM || sc_tokid == PM) {
619 panic(e("there cannot be more than 12 AM or PM hours"));
621 if (sc_tokid == PM) {
622 if (hour != 12) /* 12:xx PM is 12:xx, not 24:xx */
625 if (hour == 12) /* 12:xx AM is 00:xx, not 12:xx */
629 } else if (hour > 23) {
630 /* guess it was not a time then ... */
633 sc_tokid = sc_tokid_sv;
634 snprintf(sc_token, sc_len, "%d", hour);
637 ptv->tm. tm_hour = hour;
638 ptv->tm. tm_min = minute;
641 if (ptv->tm.tm_hour == 24) {
642 ptv->tm. tm_hour = 0;
650 * assign_date() assigns a date, adjusting year as appropriate
652 static char *assign_date(
653 rrd_time_value_t * ptv,
662 panic(e("invalid year %d (should be either 00-99 or >1900)",
665 } else if (year >= 0 && year < 38) {
666 year += 100; /* Allow year 2000-2037 to be specified as */
668 /* 00-37 until the problem of 2038 year will */
669 /* arise for unices with 32-bit time_t :) */
671 panic(e("won't handle dates before epoch (01/01/1970), sorry"));
674 ptv->tm. tm_mday = mday;
675 ptv->tm. tm_mon = mon;
676 ptv->tm. tm_year = year;
683 * day() picks apart DAY-SPEC-[12]
686 rrd_time_value_t * ptv)
688 /* using time_t seems to help portability with 64bit oses */
689 time_t mday = 0, wday, mon, year = ptv->tm.tm_year;
696 case TODAY: /* force ourselves to stay in today - no further processing */
717 /* do month mday [year]
719 mon = (sc_tokid - JAN);
720 try(expect2(NUMBER, "the day of the month should follow month name"));
721 mday = atol(sc_token);
722 if (token() == NUMBER) {
723 year = atol(sc_token);
726 year = ptv->tm.tm_year;
728 try(assign_date(ptv, mday, mon, year));
738 /* do a particular day of the week
740 wday = (sc_tokid - SUN);
741 ptv->tm. tm_mday += (
742 wday - ptv->tm.tm_wday);
747 mday = ptv->tm.tm_mday;
748 mday += (wday - ptv->tm.tm_wday);
749 ptv->tm.tm_wday = wday;
751 try(assign_date(ptv, mday, ptv->tm.tm_mon, ptv->tm.tm_year));
756 /* get numeric <sec since 1970>, MM/DD/[YY]YY, or DD.MM.[YY]YY
758 mon = atol(sc_token);
759 if (mon > 10 * 365 * 24 * 60 * 60) {
760 ptv->tm = *localtime(&mon);
766 if (mon > 19700101 && mon < 24000101) { /*works between 1900 and 2400 */
767 char cmon[3], cmday[3], cyear[5];
769 strncpy(cyear, sc_token, 4);
772 strncpy(cmon, &(sc_token[4]), 2);
775 strncpy(cmday, &(sc_token[6]), 2);
782 if (mon <= 31 && (sc_tokid == SLASH || sc_tokid == DOT)) {
786 try(expect2(NUMBER, "there should be %s number after '%c'",
787 sep == DOT ? "month" : "day",
788 sep == DOT ? '.' : '/'));
789 mday = atol(sc_token);
790 if (token() == sep) {
792 (NUMBER, "there should be year number after '%c'",
793 sep == DOT ? '.' : '/'));
794 year = atol(sc_token);
798 /* flip months and days for European timing
810 if (mon < 0 || mon > 11) {
811 panic(e("did you really mean month %d?", mon + 1));
813 if (mday < 1 || mday > 31) {
814 panic(e("I'm afraid that %d is not a valid day of the month",
817 try(assign_date(ptv, mday, mon, year));
824 /* Global functions */
828 * rrd_parsetime() is the external interface that takes tspec, parses
829 * it and puts the result in the rrd_time_value structure *ptv.
830 * It can return either absolute times (these are ensured to be
831 * correct) or relative time references that are expected to be
832 * added to some absolute time value and then normalized by
833 * mktime() The return value is either TIME_OK (aka NULL) or
834 * the pointer to the error message in the case of problems
838 rrd_time_value_t * ptv)
840 time_t now = time(NULL);
843 /* this MUST be initialized to zero for midnight/noon/teatime */
845 Specials = VariousWords; /* initialize special words context */
847 try(init_scanner(1, &tspec));
849 /* establish the default time reference */
850 ptv->type = ABSOLUTE_TIME;
852 ptv->tm = *localtime(&now);
853 ptv->tm. tm_isdst = -1; /* mk time can figure dst by default ... */
859 break; /* jump to OFFSET-SPEC part */
862 ptv->type = RELATIVE_TO_EPOCH;
865 ptv->type = RELATIVE_TO_START_TIME;
868 ptv->type = RELATIVE_TO_END_TIME;
872 ptv->tm. tm_hour = 0;
873 ptv->tm. tm_mday = 0;
875 ptv->tm. tm_year = 0;
880 int time_reference = sc_tokid;
883 if (sc_tokid == PLUS || sc_tokid == MINUS)
885 if (time_reference != NOW) {
886 panic(e("'start' or 'end' MUST be followed by +|- offset"));
887 } else if (sc_tokid != EOF) {
888 panic(e("if 'now' is followed by a token it must be +|- offset"));
893 /* Only absolute time specifications below */
896 long hour_sv = ptv->tm.tm_hour;
897 long year_sv = ptv->tm.tm_year;
899 ptv->tm. tm_hour = 30;
900 ptv->tm. tm_year = 30000;
904 if (ptv->tm.tm_hour == 30 && ptv->tm.tm_year != 30000) {
907 if (ptv->tm.tm_hour == 30) {
908 ptv->tm. tm_hour = hour_sv;
910 if (ptv->tm.tm_year == 30000) {
911 ptv->tm. tm_year = year_sv;
915 /* fix month parsing */
929 if (sc_tokid != NUMBER)
934 /* evil coding for TEATIME|NOON|MIDNIGHT - we've initialized
935 * hr to zero up above, then fall into this case in such a
936 * way so we add +12 +4 hours to it for teatime, +12 hours
937 * to it for noon, and nothing at all for midnight, then
938 * set our rettime to that hour before leaping into the
948 /* if (ptv->tm.tm_hour >= hr) {
951 } *//* shifting does not makes sense here ... noon is noon */
952 ptv->tm. tm_hour = hr;
960 panic(e("unparsable time: %s%s", sc_token, sct));
962 } /* ugly case statement */
965 * the OFFSET-SPEC part
967 * (NOTE, the sc_tokid was prefetched for us by the previous code)
969 if (sc_tokid == PLUS || sc_tokid == MINUS) {
970 Specials = TimeMultipliers; /* switch special words context */
971 while (sc_tokid == PLUS || sc_tokid == MINUS || sc_tokid == NUMBER) {
972 if (sc_tokid == NUMBER) {
973 try(plus_minus(ptv, PREVIOUS_OP));
975 try(plus_minus(ptv, sc_tokid));
976 token(); /* We will get EOF eventually but that's OK, since
977 token() will return us as many EOFs as needed */
981 /* now we should be at EOF */
982 if (sc_tokid != EOF) {
983 panic(e("unparsable trailing text: '...%s%s'", sc_token, sct));
986 if (ptv->type == ABSOLUTE_TIME)
987 if (mktime(&ptv->tm) == -1) { /* normalize & check */
988 /* can happen for "nonexistent" times, e.g. around 3am */
989 /* when winter -> summer time correction eats a hour */
990 panic(e("the specified time is incorrect (out of range?)"));
994 } /* rrd_parsetime */
997 int rrd_proc_start_end(
998 rrd_time_value_t * start_tv,
999 rrd_time_value_t * end_tv,
1003 if (start_tv->type == RELATIVE_TO_END_TIME && /* same as the line above */
1004 end_tv->type == RELATIVE_TO_START_TIME) {
1005 rrd_set_error("the start and end times cannot be specified "
1006 "relative to each other");
1010 if (start_tv->type == RELATIVE_TO_START_TIME) {
1012 ("the start time cannot be specified relative to itself");
1016 if (end_tv->type == RELATIVE_TO_END_TIME) {
1017 rrd_set_error("the end time cannot be specified relative to itself");
1021 if (start_tv->type == RELATIVE_TO_END_TIME) {
1024 *end = mktime(&(end_tv->tm)) + end_tv->offset;
1025 tmtmp = *localtime(end); /* reinit end including offset */
1026 tmtmp.tm_mday += start_tv->tm.tm_mday;
1027 tmtmp.tm_mon += start_tv->tm.tm_mon;
1028 tmtmp.tm_year += start_tv->tm.tm_year;
1030 *start = mktime(&tmtmp) + start_tv->offset;
1032 *start = mktime(&(start_tv->tm)) + start_tv->offset;
1034 if (end_tv->type == RELATIVE_TO_START_TIME) {
1037 *start = mktime(&(start_tv->tm)) + start_tv->offset;
1038 tmtmp = *localtime(start);
1039 tmtmp.tm_mday += end_tv->tm.tm_mday;
1040 tmtmp.tm_mon += end_tv->tm.tm_mon;
1041 tmtmp.tm_year += end_tv->tm.tm_year;
1043 *end = mktime(&tmtmp) + end_tv->offset;
1045 *end = mktime(&(end_tv->tm)) + end_tv->offset;
1048 } /* rrd_proc_start_end */