xref: /plan9/sys/src/cmd/seconds.c (revision 6084a089d07240b0be60cd120ef40fa169c93cdf)
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