2 * 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)
117 #include "rrd_tool.h"
120 /* Structures and unions */
123 MIDNIGHT, NOON, TEATIME,
124 PM, AM, YESTERDAY, TODAY, TOMORROW, NOW, START, END,
125 SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS,
127 NUMBER, PLUS, MINUS, DOT, COLON, SLASH, ID, JUNK,
128 JAN, FEB, MAR, APR, MAY, JUN,
129 JUL, AUG, SEP, OCT, NOV, DEC,
130 SUN, MON, TUE, WED, THU, FRI, SAT
133 /* the below is for plus_minus() */
134 #define PREVIOUS_OP (-1)
136 /* parse translation table - table driven parsers can be your FRIEND!
138 struct SpecialToken {
139 char *name; /* token name */
140 int value; /* token id */
142 static struct SpecialToken VariousWords[] = {
143 { "midnight", MIDNIGHT }, /* 00:00:00 of today or tomorrow */
144 { "noon", NOON }, /* 12:00:00 of today or tomorrow */
145 { "teatime", TEATIME }, /* 16:00:00 of today or tomorrow */
146 { "am", AM }, /* morning times for 0-12 clock */
147 { "pm", PM }, /* evening times for 0-12 clock */
148 { "tomorrow", TOMORROW },
149 { "yesterday", YESTERDAY },
178 { "september", SEP },
188 { "wednesday", WED },
196 { NULL, 0 } /*** SENTINEL ***/
199 static struct SpecialToken TimeMultipliers[] = {
200 { "second", SECONDS }, /* seconds multiplier */
201 { "seconds", SECONDS }, /* (pluralized) */
202 { "sec", SECONDS }, /* (generic) */
203 { "s", SECONDS }, /* (short generic) */
204 { "minute", MINUTES }, /* minutes multiplier */
205 { "minutes", MINUTES }, /* (pluralized) */
206 { "min", MINUTES }, /* (generic) */
207 { "m", MONTHS_MINUTES }, /* (short generic) */
208 { "hour", HOURS }, /* hours ... */
209 { "hours", HOURS }, /* (pluralized) */
210 { "hr", HOURS }, /* (generic) */
211 { "h", HOURS }, /* (short generic) */
212 { "day", DAYS }, /* days ... */
213 { "days", DAYS }, /* (pluralized) */
214 { "d", DAYS }, /* (short generic) */
215 { "week", WEEKS }, /* week ... */
216 { "weeks", WEEKS }, /* (pluralized) */
217 { "wk", WEEKS }, /* (generic) */
218 { "w", WEEKS }, /* (short generic) */
219 { "month", MONTHS }, /* week ... */
220 { "months", MONTHS }, /* (pluralized) */
221 { "mon", MONTHS }, /* (generic) */
222 { "year", YEARS }, /* year ... */
223 { "years", YEARS }, /* (pluralized) */
224 { "yr", YEARS }, /* (generic) */
225 { "y", YEARS }, /* (short generic) */
226 { NULL, 0 } /*** SENTINEL ***/
229 /* File scope variables */
231 /* context dependant list of specials for parser to recognize,
232 * required for us to be able distinguish between 'mon' as 'month'
233 * and 'mon' as 'monday'
235 static struct SpecialToken *Specials;
237 static char **scp; /* scanner - pointer at arglist */
238 static char scc; /* scanner - count of remaining arguments */
239 static char *sct; /* scanner - next char pointer in current argument */
240 static int need; /* scanner - need to advance to next argument */
242 static char *sc_token=NULL; /* scanner - token buffer */
243 static size_t sc_len; /* scanner - lenght of token buffer */
244 static int sc_tokid; /* scanner - token id */
246 static int need_to_free = 0; /* means that we need deallocating memory */
248 /* Local functions */
250 void EnsureMemFree ()
260 * A hack to compensate for the lack of the C++ exceptions
262 * Every function func that might generate parsing "exception"
263 * should return TIME_OK (aka NULL) or pointer to the error message,
264 * and should be called like this: try(func(args));
266 * if the try is not successfull it will reset the token pointer ...
268 * [NOTE: when try(...) is used as the only statement in the "if-true"
269 * part of the if statement that also has an "else" part it should be
270 * either enclosed in the curly braces (despite the fact that it looks
271 * like a single statement) or NOT follwed by the ";"]
283 * The panic() function was used in the original code to die, we redefine
284 * it as macro to start the chain of ascending returns that in conjunction
285 * with the try(b) above will simulate a sort of "exception handling"
293 * ve() and e() are used to set the return error,
294 * the most aprropriate use for these is inside panic(...)
296 #define MAX_ERR_MSG_LEN 1024
297 static char errmsg[ MAX_ERR_MSG_LEN ];
300 ve ( char *fmt, va_list ap )
302 #ifdef HAVE_VSNPRINTF
303 vsnprintf( errmsg, MAX_ERR_MSG_LEN, fmt, ap );
305 vsprintf( errmsg, fmt, ap );
322 /* Compare S1 and S2, ignoring case, returning less than, equal to or
323 greater than zero if S1 is lexiographically less than,
324 equal to or greater than S2. -- copied from GNU libc*/
326 mystrcasecmp (s1, s2)
330 const unsigned char *p1 = (const unsigned char *) s1;
331 const unsigned char *p2 = (const unsigned char *) s2;
332 unsigned char c1, c2;
339 c1 = tolower (*p1++);
340 c2 = tolower (*p2++);
350 * parse a token, checking if it's something special to us
353 parse_token(char *arg)
357 for (i=0; Specials[i].name != NULL; i++)
358 if (mystrcasecmp(Specials[i].name, arg) == 0)
359 return sc_tokid = Specials[i].value;
361 /* not special - must be some random id */
362 return sc_tokid = ID;
368 * init_scanner() sets up the scanner to eat arguments
371 init_scanner(int argc, char **argv)
378 sc_len += strlen(*argv++);
380 sc_token = (char *) malloc(sc_len*sizeof(char));
381 if( sc_token == NULL )
382 return "Failed to allocate memory";
388 * token() fetches a token from the input stream
396 memset(sc_token, '\0', sc_len);
400 /* if we need to read another argument, walk along the argument list;
401 * when we fall off the arglist, we'll just return EOF forever
411 /* eat whitespace now - if we walk off the end of the argument,
412 * we'll continue, which puts us up at the top of the while loop
413 * to fetch the next argument in
415 while (isspace((unsigned char)*sct) || *sct == '_' || *sct == ',' )
422 /* preserve the first character of the new token
424 sc_token[0] = *sct++;
426 /* then see what it is
428 if (isdigit((unsigned char)(sc_token[0]))) {
429 while (isdigit((unsigned char)(*sct)))
430 sc_token[++idx] = *sct++;
431 sc_token[++idx] = '\0';
432 return sc_tokid = NUMBER;
434 else if (isalpha((unsigned char)(sc_token[0]))) {
435 while (isalpha((unsigned char)(*sct)))
436 sc_token[++idx] = *sct++;
437 sc_token[++idx] = '\0';
438 return parse_token(sc_token);
440 else switch(sc_token[0]) {
441 case ':': return sc_tokid = COLON;
442 case '.': return sc_tokid = DOT;
443 case '+': return sc_tokid = PLUS;
444 case '-': return sc_tokid = MINUS;
445 case '/': return sc_tokid = SLASH;
447 /*OK, we did not make it ... */
449 return sc_tokid = EOF;
456 * expect2() gets a token and complins if it's not the token we want
459 expect2(int desired, char *complain_fmt, ...)
462 va_start( ap, complain_fmt );
463 if (token() != desired) {
464 panic(ve( complain_fmt, ap ));
473 * plus_minus() is used to parse a single NUMBER TIME-UNIT pair
474 * for the OFFSET-SPEC.
475 * It allso applies those m-guessing euristics.
478 plus_minus(struct rrd_time_value *ptv, int doop)
480 static int op = PLUS;
481 static int prev_multiplier = -1;
487 try(expect2(NUMBER,"There should be number after '%c'", op == PLUS ? '+' : '-'));
488 prev_multiplier = -1; /* reset months-minutes guessing mechanics */
490 /* if doop is < 0 then we repeat the previous op
491 * with the prefetched number */
493 delta = atoi(sc_token);
495 if( token() == MONTHS_MINUTES )
497 /* hard job to guess what does that -5m means: -5mon or -5min? */
498 switch(prev_multiplier)
514 if( delta < 6 ) /* it may be some other value but in the context
515 * of RRD who needs less than 6 min deltas? */
521 prev_multiplier = sc_tokid;
524 ptv->tm.tm_year += (op == PLUS) ? delta : -delta;
527 ptv->tm.tm_mon += (op == PLUS) ? delta : -delta;
533 ptv->tm.tm_mday += (op == PLUS) ? delta : -delta;
536 ptv->offset += (op == PLUS) ? delta*60*60 : -delta*60*60;
539 ptv->offset += (op == PLUS) ? delta*60 : -delta*60;
542 ptv->offset += (op == PLUS) ? delta : -delta;
544 default: /*default unit is seconds */
545 ptv->offset += (op == PLUS) ? delta : -delta;
548 panic(e("well-known time unit expected after %d", delta));
550 return TIME_OK; /* to make compiler happy :) */
555 * tod() computes the time of day (TIME-OF-DAY-SPEC)
558 tod(struct rrd_time_value *ptv)
560 int hour, minute = 0;
562 /* save token status in case we must abort */
565 int sc_tokid_sv = sc_tokid;
567 tlen = strlen(sc_token);
569 /* first pick out the time of day - we assume a HH (COLON|DOT) MM time
575 hour = atoi(sc_token);
578 if (sc_tokid == SLASH || sc_tokid == DOT) {
579 /* guess we are looking at a date */
582 sc_tokid = sc_tokid_sv;
583 sprintf (sc_token,"%d", hour);
586 if (sc_tokid == COLON ) {
588 "Parsing HH:MM syntax, expecting MM as number, got none"));
589 minute = atoi(sc_token);
591 panic(e("parsing HH:MM syntax, got MM = %d (>59!)", minute ));
596 /* check if an AM or PM specifier was given
598 if (sc_tokid == AM || sc_tokid == PM) {
600 panic(e("there cannot be more than 12 AM or PM hours"));
602 if (sc_tokid == PM) {
603 if (hour != 12) /* 12:xx PM is 12:xx, not 24:xx */
606 if (hour == 12) /* 12:xx AM is 00:xx, not 12:xx */
611 else if (hour > 23) {
612 /* guess it was not a time then ... */
615 sc_tokid = sc_tokid_sv;
616 sprintf (sc_token,"%d", hour);
619 ptv->tm.tm_hour = hour;
620 ptv->tm.tm_min = minute;
622 if (ptv->tm.tm_hour == 24) {
631 * assign_date() assigns a date, adjusting year as appropriate
634 assign_date(struct rrd_time_value *ptv, long mday, long mon, long year)
640 panic(e("invalid year %d (should be either 00-99 or >1900)",
643 } else if( year >= 0 && year < 38 ) {
644 year += 100; /* Allow year 2000-2037 to be specified as */
645 } /* 00-37 until the problem of 2038 year will */
646 /* arise for unices with 32-bit time_t :) */
648 panic(e("won't handle dates before epoch (01/01/1970), sorry"));
651 ptv->tm.tm_mday = mday;
652 ptv->tm.tm_mon = mon;
653 ptv->tm.tm_year = year;
659 * day() picks apart DAY-SPEC-[12]
662 day(struct rrd_time_value *ptv)
664 long mday=0, wday, mon, year = ptv->tm.tm_year;
671 case TODAY: /* force ourselves to stay in today - no further processing */
679 case JAN: case FEB: case MAR: case APR: case MAY: case JUN:
680 case JUL: case AUG: case SEP: case OCT: case NOV: case DEC:
681 /* do month mday [year]
683 mon = (sc_tokid-JAN);
685 "the day of the month should follow month name"));
686 mday = atol(sc_token);
687 if (token() == NUMBER) {
688 year = atol(sc_token);
692 year = ptv->tm.tm_year;
693 try(assign_date(ptv, mday, mon, year));
696 case SUN: case MON: case TUE:
697 case WED: case THU: case FRI:
699 /* do a particular day of the week
701 wday = (sc_tokid-SUN);
702 ptv->tm.tm_mday += (wday - ptv->tm.tm_wday);
706 mday = ptv->tm.tm_mday;
707 mday += (wday - ptv->tm.tm_wday);
708 ptv->tm.tm_wday = wday;
710 try(assign_date(ptv, mday, ptv->tm.tm_mon, ptv->tm.tm_year));
715 /* get numeric <sec since 1970>, MM/DD/[YY]YY, or DD.MM.[YY]YY
717 tlen = strlen(sc_token);
718 mon = atol(sc_token);
719 if (mon > 10*356*24*60*60) {
720 ptv->tm=*localtime(&mon);
725 if (mon > 19700101 && mon < 24000101){ /*works between 1900 and 2400 */
726 char cmon[3],cmday[3],cyear[5];
727 strncpy(cyear,sc_token,4);cyear[4]='\0';
729 strncpy(cmon,&(sc_token[4]),2);cmon[2]='\0';
731 strncpy(cmday,&(sc_token[6]),2);cmday[2]='\0';
737 if (mon <= 31 && (sc_tokid == SLASH || sc_tokid == DOT)) {
740 try(expect2(NUMBER,"there should be %s number after '%c'",
741 sep == DOT ? "month" : "day", sep == DOT ? '.' : '/'));
742 mday = atol(sc_token);
743 if (token() == sep) {
744 try(expect2(NUMBER,"there should be year number after '%c'",
745 sep == DOT ? '.' : '/'));
746 year = atol(sc_token);
750 /* flip months and days for european timing
761 if(mon < 0 || mon > 11 ) {
762 panic(e("did you really mean month %d?", mon+1));
764 if(mday < 1 || mday > 31) {
765 panic(e("I'm afraid that %d is not a valid day of the month",
768 try(assign_date(ptv, mday, mon, year));
775 /* Global functions */
779 * parsetime() is the external interface that takes tspec, parses
780 * it and puts the result in the rrd_time_value structure *ptv.
781 * It can return either absolute times (these are ensured to be
782 * correct) or relative time references that are expected to be
783 * added to some absolute time value and then normalized by
784 * mktime() The return value is either TIME_OK (aka NULL) or
785 * the pointer to the error message in the case of problems
788 parsetime(char *tspec, struct rrd_time_value *ptv)
790 time_t now = time(NULL);
792 /* this MUST be initialized to zero for midnight/noon/teatime */
794 Specials = VariousWords; /* initialize special words context */
796 try(init_scanner( 1, &tspec ));
798 /* establish the default time reference */
799 ptv->type = ABSOLUTE_TIME;
801 ptv->tm = *localtime(&now);
802 ptv->tm.tm_isdst = -1; /* mk time can figure this out for us ... */
808 break; /* jump to OFFSET-SPEC part */
811 ptv->type = RELATIVE_TO_START_TIME;
814 ptv->type = RELATIVE_TO_END_TIME;
825 int time_reference = sc_tokid;
827 if( sc_tokid == PLUS || sc_tokid == MINUS )
829 if( time_reference != NOW ) {
830 panic(e("'start' or 'end' MUST be followed by +|- offset"));
833 if( sc_tokid != EOF ) {
834 panic(e("if 'now' is followed by a token it must be +|- offset"));
839 /* Only absolute time specifications below */
844 /* fix month parsing */
845 case JAN: case FEB: case MAR: case APR: case MAY: case JUN:
846 case JUL: case AUG: case SEP: case OCT: case NOV: case DEC:
848 if (sc_tokid != NUMBER) break;
852 /* evil coding for TEATIME|NOON|MIDNIGHT - we've initialised
853 * hr to zero up above, then fall into this case in such a
854 * way so we add +12 +4 hours to it for teatime, +12 hours
855 * to it for noon, and nothing at all for midnight, then
856 * set our rettime to that hour before leaping into the
866 /* if (ptv->tm.tm_hour >= hr) {
869 } */ /* shifting does not makes sense here ... noon is noon */
870 ptv->tm.tm_hour = hr;
877 panic(e("unparsable time: %s%s",sc_token,sct));
879 } /* ugly case statement */
882 * the OFFSET-SPEC part
884 * (NOTE, the sc_tokid was prefetched for us by the previous code)
886 if( sc_tokid == PLUS || sc_tokid == MINUS ) {
887 Specials = TimeMultipliers; /* switch special words context */
888 while( sc_tokid == PLUS || sc_tokid == MINUS ||
889 sc_tokid == NUMBER ) {
890 if( sc_tokid == NUMBER ) {
891 try(plus_minus(ptv, PREVIOUS_OP ));
893 try(plus_minus(ptv, sc_tokid));
894 token(); /* We will get EOF eventually but that's OK, since
895 token() will return us as many EOFs as needed */
899 /* now we should be at EOF */
900 if( sc_tokid != EOF ) {
901 panic(e("unparsable trailing text: '...%s%s'", sc_token, sct));
904 ptv->tm.tm_isdst = -1; /* for mktime to guess DST status */
905 if( ptv->type == ABSOLUTE_TIME )
906 if( mktime( &ptv->tm ) == -1 ) { /* normalize & check */
907 /* can happen for "nonexistent" times, e.g. around 3am */
908 /* when winter -> summer time correction eats a hour */
909 panic(e("the specified time is incorrect (out of range?)"));
916 int proc_start_end (struct rrd_time_value *start_tv,
917 struct rrd_time_value *end_tv,
920 if (start_tv->type == RELATIVE_TO_END_TIME && /* same as the line above */
921 end_tv->type == RELATIVE_TO_START_TIME) {
922 rrd_set_error("the start and end times cannot be specified "
923 "relative to each other");
927 if (start_tv->type == RELATIVE_TO_START_TIME) {
928 rrd_set_error("the start time cannot be specified relative to itself");
932 if (end_tv->type == RELATIVE_TO_END_TIME) {
933 rrd_set_error("the end time cannot be specified relative to itself");
937 if( start_tv->type == RELATIVE_TO_END_TIME) {
939 *end = mktime(&(end_tv->tm)) + end_tv->offset;
940 tmtmp = *localtime(end); /* reinit end including offset */
941 tmtmp.tm_mday += start_tv->tm.tm_mday;
942 tmtmp.tm_mon += start_tv->tm.tm_mon;
943 tmtmp.tm_year += start_tv->tm.tm_year;
944 *start = mktime(&tmtmp) + start_tv->offset;
946 *start = mktime(&(start_tv->tm)) + start_tv->offset;
948 if (end_tv->type == RELATIVE_TO_START_TIME) {
950 *start = mktime(&(start_tv->tm)) + start_tv->offset;
951 tmtmp = *localtime(start);
952 tmtmp.tm_mday += end_tv->tm.tm_mday;
953 tmtmp.tm_mon += end_tv->tm.tm_mon;
954 tmtmp.tm_year += end_tv->tm.tm_year;
955 *end = mktime(&tmtmp) + end_tv->offset;
957 *end = mktime(&(end_tv->tm)) + end_tv->offset;
960 } /* proc_start_end */