1 /*
2 * seconds absolute_date ... - convert absolute_date to seconds since epoch
3 */
4
5 #include <u.h>
6 #include <libc.h>
7 #include <ctype.h>
8
9 typedef ulong Time;
10
11 enum {
12 AM, PM, HR24,
13
14 /* token types */
15 Month = 1,
16 Year,
17 Day,
18 Timetok,
19 Tz,
20 Dtz,
21 Ignore,
22 Ampm,
23
24 Maxtok = 6, /* only this many chars are stored in datetktbl */
25 Maxdateflds = 25,
26 };
27
28 /*
29 * macros for squeezing values into low 7 bits of "value".
30 * all timezones we care about are divisible by 10, and the largest value
31 * (780) when divided is 78.
32 */
33 #define TOVAL(tp, v) ((tp)->value = (v) / 10)
34 #define FROMVAL(tp) ((tp)->value * 10) /* uncompress */
35
36 /* keep this struct small since we have an array of them */
37 typedef struct {
38 char token[Maxtok];
39 char type;
40 schar value;
41 } Datetok;
42
43 int dtok_numparsed;
44
45 /* forwards */
46 Datetok *datetoktype(char *s, int *bigvalp);
47
48 static Datetok datetktbl[];
49 static unsigned szdatetktbl;
50
51 /* parse 1- or 2-digit number, advance *cpp past it */
52 static int
eatnum(char ** cpp)53 eatnum(char **cpp)
54 {
55 int c, x;
56 char *cp;
57
58 cp = *cpp;
59 c = *cp;
60 if (!isascii(c) || !isdigit(c))
61 return -1;
62 x = c - '0';
63
64 c = *++cp;
65 if (isascii(c) && isdigit(c)) {
66 x = 10*x + c - '0';
67 cp++;
68 }
69 *cpp = cp;
70 return x;
71 }
72
73 /* return -1 on failure */
74 int
parsetime(char * time,Tm * tm)75 parsetime(char *time, Tm *tm)
76 {
77 tm->hour = eatnum(&time);
78 if (tm->hour == -1 || *time++ != ':')
79 return -1; /* only hour; too short */
80
81 tm->min = eatnum(&time);
82 if (tm->min == -1)
83 return -1;
84 if (*time++ != ':') {
85 tm->sec = 0;
86 return 0; /* no seconds; okay */
87 }
88
89 tm->sec = eatnum(&time);
90 if (tm->sec == -1)
91 return -1;
92
93 /* this may be considered too strict. garbage at end of time? */
94 return *time == '\0' || isascii(*time) && isspace(*time)? 0: -1;
95 }
96
97 /*
98 * try to parse pre-split timestr in fields as an absolute date
99 */
100 int
tryabsdate(char ** fields,int nf,Tm * now,Tm * tm)101 tryabsdate(char **fields, int nf, Tm *now, Tm *tm)
102 {
103 int i, mer = HR24, bigval = -1;
104 long flg = 0, ty;
105 char *p;
106 Datetok *tp;
107
108 now = localtime(time(0)); /* default to local time (zone) */
109 tm->tzoff = now->tzoff;
110 strncpy(tm->zone, now->zone, sizeof tm->zone - 1);
111 tm->zone[sizeof tm->zone - 1] = '\0';
112
113 tm->mday = tm->mon = tm->year = -1; /* mandatory */
114 tm->hour = tm->min = tm->sec = 0;
115 dtok_numparsed = 0;
116
117 for (i = 0; i < nf; i++) {
118 if (fields[i][0] == '\0')
119 continue;
120 tp = datetoktype(fields[i], &bigval);
121 ty = (1L << tp->type) & ~(1L << Ignore);
122 if (flg & ty)
123 return -1; /* repeated type */
124 flg |= ty;
125 switch (tp->type) {
126 case Year:
127 tm->year = bigval;
128 if (tm->year < 1970 || tm->year > 2106)
129 return -1; /* can't represent in ulong */
130 /* convert 4-digit year to 1900 origin */
131 if (tm->year >= 1900)
132 tm->year -= 1900;
133 break;
134 case Day:
135 tm->mday = bigval;
136 break;
137 case Month:
138 tm->mon = tp->value - 1; /* convert to zero-origin */
139 break;
140 case Timetok:
141 if (parsetime(fields[i], tm) < 0)
142 return -1;
143 break;
144 case Dtz:
145 case Tz:
146 tm->tzoff = FROMVAL(tp);
147 /* tm2sec needs the name in upper case */
148 strncpy(tm->zone, fields[i], sizeof tm->zone - 1);
149 tm->zone[sizeof tm->zone - 1] = '\0';
150 for (p = tm->zone; *p; p++)
151 if (isascii(*p) && islower(*p))
152 *p = toupper(*p);
153 break;
154 case Ignore:
155 break;
156 case Ampm:
157 mer = tp->value;
158 break;
159 default:
160 return -1; /* bad token type: CANTHAPPEN */
161 }
162 }
163 if (tm->year == -1 || tm->mon == -1 || tm->mday == -1)
164 return -1; /* missing component */
165 if (mer == PM)
166 tm->hour += 12;
167 return 0;
168 }
169
170 int
prsabsdate(char * timestr,Tm * now,Tm * tm)171 prsabsdate(char *timestr, Tm *now, Tm *tm)
172 {
173 int nf;
174 char *fields[Maxdateflds];
175 static char delims[] = "- \t\n/,";
176
177 nf = gettokens(timestr, fields, nelem(fields), delims+1);
178 if (nf > nelem(fields))
179 return -1;
180 if (tryabsdate(fields, nf, now, tm) < 0) {
181 char *p = timestr;
182
183 /*
184 * could be a DEC-date; glue it all back together, split it
185 * with dash as a delimiter and try again. Yes, this is a
186 * hack, but so are DEC-dates.
187 */
188 while (--nf > 0) {
189 while (*p++ != '\0')
190 ;
191 p[-1] = ' ';
192 }
193 nf = gettokens(timestr, fields, nelem(fields), delims);
194 if (nf > nelem(fields) || tryabsdate(fields, nf, now, tm) < 0)
195 return -1;
196 }
197 return 0;
198 }
199
200 int
validtm(Tm * tm)201 validtm(Tm *tm)
202 {
203 if (tm->year < 0 || tm->mon < 0 || tm->mon > 11 ||
204 tm->mday < 1 || tm->hour < 0 || tm->hour >= 24 ||
205 tm->min < 0 || tm->min > 59 ||
206 tm->sec < 0 || tm->sec > 61) /* allow 2 leap seconds */
207 return 0;
208 return 1;
209 }
210
211 Time
seconds(char * timestr)212 seconds(char *timestr)
213 {
214 Tm date;
215
216 memset(&date, 0, sizeof date);
217 if (prsabsdate(timestr, localtime(time(0)), &date) < 0)
218 return -1;
219 return validtm(&date)? tm2sec(&date): -1;
220 }
221
222 int
convert(char * timestr)223 convert(char *timestr)
224 {
225 char *copy;
226 Time tstime;
227
228 copy = strdup(timestr);
229 if (copy == nil)
230 sysfatal("out of memory");
231 tstime = seconds(copy);
232 free(copy);
233 if (tstime == -1) {
234 fprint(2, "%s: `%s' not a valid date\n", argv0, timestr);
235 return 1;
236 }
237 print("%lud\n", tstime);
238 return 0;
239 }
240
241 static void
usage(void)242 usage(void)
243 {
244 fprint(2, "usage: %s date-time ...\n", argv0);
245 exits("usage");
246 }
247
248 void
main(int argc,char ** argv)249 main(int argc, char **argv)
250 {
251 int i, sts;
252
253 sts = 0;
254 ARGBEGIN{
255 default:
256 usage();
257 }ARGEND
258 if (argc == 0)
259 usage();
260 for (i = 0; i < argc; i++)
261 sts |= convert(argv[i]);
262 exits(sts != 0? "bad": 0);
263 }
264
265 /*
266 * Binary search -- from Knuth (6.2.1) Algorithm B. Special case like this
267 * is WAY faster than the generic bsearch().
268 */
269 Datetok *
datebsearch(char * key,Datetok * base,unsigned nel)270 datebsearch(char *key, Datetok *base, unsigned nel)
271 {
272 int cmp;
273 Datetok *last = base + nel - 1, *pos;
274
275 while (last >= base) {
276 pos = base + ((last - base) >> 1);
277 cmp = key[0] - pos->token[0];
278 if (cmp == 0) {
279 cmp = strncmp(key, pos->token, Maxtok);
280 if (cmp == 0)
281 return pos;
282 }
283 if (cmp < 0)
284 last = pos - 1;
285 else
286 base = pos + 1;
287 }
288 return 0;
289 }
290
291 Datetok *
datetoktype(char * s,int * bigvalp)292 datetoktype(char *s, int *bigvalp)
293 {
294 char *cp = s;
295 char c = *cp;
296 static Datetok t;
297 Datetok *tp = &t;
298
299 if (isascii(c) && isdigit(c)) {
300 int len = strlen(cp);
301
302 if (len > 3 && (cp[1] == ':' || cp[2] == ':'))
303 tp->type = Timetok;
304 else {
305 if (bigvalp != nil)
306 *bigvalp = atoi(cp); /* won't fit in tp->value */
307 if (len == 4)
308 tp->type = Year;
309 else if (++dtok_numparsed == 1)
310 tp->type = Day;
311 else
312 tp->type = Year;
313 }
314 } else if (c == '-' || c == '+') {
315 int val = atoi(cp + 1);
316 int hr = val / 100;
317 int min = val % 100;
318
319 val = hr*60 + min;
320 TOVAL(tp, c == '-'? -val: val);
321 tp->type = Tz;
322 } else {
323 char lowtoken[Maxtok+1];
324 char *ltp = lowtoken, *endltp = lowtoken+Maxtok;
325
326 /* copy to lowtoken to avoid modifying s */
327 while ((c = *cp++) != '\0' && ltp < endltp)
328 *ltp++ = (isascii(c) && isupper(c)? tolower(c): c);
329 *ltp = '\0';
330 tp = datebsearch(lowtoken, datetktbl, szdatetktbl);
331 if (tp == nil) {
332 tp = &t;
333 tp->type = Ignore;
334 }
335 }
336 return tp;
337 }
338
339
340 /*
341 * to keep this table reasonably small, we divide the lexval for Tz and Dtz
342 * entries by 10 and truncate the text field at MAXTOKLEN characters.
343 * the text field is not guaranteed to be NUL-terminated.
344 */
345 static Datetok datetktbl[] = {
346 /* text token lexval */
347 "acsst", Dtz, 63, /* Cent. Australia */
348 "acst", Tz, 57, /* Cent. Australia */
349 "adt", Dtz, -18, /* Atlantic Daylight Time */
350 "aesst", Dtz, 66, /* E. Australia */
351 "aest", Tz, 60, /* Australia Eastern Std Time */
352 "ahst", Tz, 60, /* Alaska-Hawaii Std Time */
353 "am", Ampm, AM,
354 "apr", Month, 4,
355 "april", Month, 4,
356 "ast", Tz, -24, /* Atlantic Std Time (Canada) */
357 "at", Ignore, 0, /* "at" (throwaway) */
358 "aug", Month, 8,
359 "august", Month, 8,
360 "awsst", Dtz, 54, /* W. Australia */
361 "awst", Tz, 48, /* W. Australia */
362 "bst", Tz, 6, /* British Summer Time */
363 "bt", Tz, 18, /* Baghdad Time */
364 "cadt", Dtz, 63, /* Central Australian DST */
365 "cast", Tz, 57, /* Central Australian ST */
366 "cat", Tz, -60, /* Central Alaska Time */
367 "cct", Tz, 48, /* China Coast */
368 "cdt", Dtz, -30, /* Central Daylight Time */
369 "cet", Tz, 6, /* Central European Time */
370 "cetdst", Dtz, 12, /* Central European Dayl.Time */
371 "cst", Tz, -36, /* Central Standard Time */
372 "dec", Month, 12,
373 "decemb", Month, 12,
374 "dnt", Tz, 6, /* Dansk Normal Tid */
375 "dst", Ignore, 0,
376 "east", Tz, -60, /* East Australian Std Time */
377 "edt", Dtz, -24, /* Eastern Daylight Time */
378 "eet", Tz, 12, /* East. Europe, USSR Zone 1 */
379 "eetdst", Dtz, 18, /* Eastern Europe */
380 "est", Tz, -30, /* Eastern Standard Time */
381 "feb", Month, 2,
382 "februa", Month, 2,
383 "fri", Ignore, 5,
384 "friday", Ignore, 5,
385 "fst", Tz, 6, /* French Summer Time */
386 "fwt", Dtz, 12, /* French Winter Time */
387 "gmt", Tz, 0, /* Greenwish Mean Time */
388 "gst", Tz, 60, /* Guam Std Time, USSR Zone 9 */
389 "hdt", Dtz, -54, /* Hawaii/Alaska */
390 "hmt", Dtz, 18, /* Hellas ? ? */
391 "hst", Tz, -60, /* Hawaii Std Time */
392 "idle", Tz, 72, /* Intl. Date Line, East */
393 "idlw", Tz, -72, /* Intl. Date Line, West */
394 "ist", Tz, 12, /* Israel */
395 "it", Tz, 22, /* Iran Time */
396 "jan", Month, 1,
397 "januar", Month, 1,
398 "jst", Tz, 54, /* Japan Std Time,USSR Zone 8 */
399 "jt", Tz, 45, /* Java Time */
400 "jul", Month, 7,
401 "july", Month, 7,
402 "jun", Month, 6,
403 "june", Month, 6,
404 "kst", Tz, 54, /* Korea Standard Time */
405 "ligt", Tz, 60, /* From Melbourne, Australia */
406 "mar", Month, 3,
407 "march", Month, 3,
408 "may", Month, 5,
409 "mdt", Dtz, -36, /* Mountain Daylight Time */
410 "mest", Dtz, 12, /* Middle Europe Summer Time */
411 "met", Tz, 6, /* Middle Europe Time */
412 "metdst", Dtz, 12, /* Middle Europe Daylight Time*/
413 "mewt", Tz, 6, /* Middle Europe Winter Time */
414 "mez", Tz, 6, /* Middle Europe Zone */
415 "mon", Ignore, 1,
416 "monday", Ignore, 1,
417 "mst", Tz, -42, /* Mountain Standard Time */
418 "mt", Tz, 51, /* Moluccas Time */
419 "ndt", Dtz, -15, /* Nfld. Daylight Time */
420 "nft", Tz, -21, /* Newfoundland Standard Time */
421 "nor", Tz, 6, /* Norway Standard Time */
422 "nov", Month, 11,
423 "novemb", Month, 11,
424 "nst", Tz, -21, /* Nfld. Standard Time */
425 "nt", Tz, -66, /* Nome Time */
426 "nzdt", Dtz, 78, /* New Zealand Daylight Time */
427 "nzst", Tz, 72, /* New Zealand Standard Time */
428 "nzt", Tz, 72, /* New Zealand Time */
429 "oct", Month, 10,
430 "octobe", Month, 10,
431 "on", Ignore, 0, /* "on" (throwaway) */
432 "pdt", Dtz, -42, /* Pacific Daylight Time */
433 "pm", Ampm, PM,
434 "pst", Tz, -48, /* Pacific Standard Time */
435 "sadt", Dtz, 63, /* S. Australian Dayl. Time */
436 "sast", Tz, 57, /* South Australian Std Time */
437 "sat", Ignore, 6,
438 "saturd", Ignore, 6,
439 "sep", Month, 9,
440 "sept", Month, 9,
441 "septem", Month, 9,
442 "set", Tz, -6, /* Seychelles Time ?? */
443 "sst", Dtz, 12, /* Swedish Summer Time */
444 "sun", Ignore, 0,
445 "sunday", Ignore, 0,
446 "swt", Tz, 6, /* Swedish Winter Time */
447 "thu", Ignore, 4,
448 "thur", Ignore, 4,
449 "thurs", Ignore, 4,
450 "thursd", Ignore, 4,
451 "tue", Ignore, 2,
452 "tues", Ignore, 2,
453 "tuesda", Ignore, 2,
454 "ut", Tz, 0,
455 "utc", Tz, 0,
456 "wadt", Dtz, 48, /* West Australian DST */
457 "wast", Tz, 42, /* West Australian Std Time */
458 "wat", Tz, -6, /* West Africa Time */
459 "wdt", Dtz, 54, /* West Australian DST */
460 "wed", Ignore, 3,
461 "wednes", Ignore, 3,
462 "weds", Ignore, 3,
463 "wet", Tz, 0, /* Western Europe */
464 "wetdst", Dtz, 6, /* Western Europe */
465 "wst", Tz, 48, /* West Australian Std Time */
466 "ydt", Dtz, -48, /* Yukon Daylight Time */
467 "yst", Tz, -54, /* Yukon Standard Time */
468 "zp4", Tz, -24, /* GMT +4 hours. */
469 "zp5", Tz, -30, /* GMT +5 hours. */
470 "zp6", Tz, -36, /* GMT +6 hours. */
471 };
472 static unsigned szdatetktbl = nelem(datetktbl);
473