From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mimir.eigenstate.org ([206.124.132.107]) by ewsd; Sun May 31 00:57:41 EDT 2020 Received: from abbatoir.fios-router.home (pool-162-83-132-245.nycmny.fios.verizon.net [162.83.132.245]) by mimir.eigenstate.org (OpenSMTPD) with ESMTPSA id e616bd36 (TLSv1.2:ECDHE-RSA-AES256-SHA:256:NO); Sat, 30 May 2020 21:57:31 -0700 (PDT) Message-ID: <865929461018EA402453EDE353D7838B@eigenstate.org> To: ori@eigenstate.org, 9front@9front.org Subject: Re: [9front] libc: date handling improvements Date: Sat, 30 May 2020 21:57:30 -0700 From: ori@eigenstate.org In-Reply-To: MIME-Version: 1.0 Content-Type: text/plain; charset="US-ASCII" Content-Transfer-Encoding: 7bit List-ID: <9front.9front.org> List-Help: X-Glyph: ➈ X-Bullshit: method engine-oriented pipelining backend > I implemented libdate -- but the right place to put > it is in libc, replacing our ctime and gmtime apis. > > Follow ups will replace seconds(1), upas time parsing > code, and whatever other instances of hand rolled date > parsing and formatting can be found. > > Here's a patch that does this. Testing appreciated, > and follow ups that I haven't done yet would also be > great :) And here's a rough replacement of seconds(1). Testing needed, especially since it's likely we no longer parse some date formats that we used to. The old code was hacky and heuristic filled, and the grammar wasn't very clear. If there's a date format this no longer parses for you, let me know and I'll add it. diff -r d45cfe9072cd sys/src/cmd/seconds.c --- a/sys/src/cmd/seconds.c Sat May 30 13:03:05 2020 +0200 +++ b/sys/src/cmd/seconds.c Sat May 30 21:56:13 2020 -0700 @@ -1,237 +1,38 @@ +#include +#include +#include + /* * seconds absolute_date ... - convert absolute_date to seconds since epoch */ - -#include -#include -#include - -typedef ulong Time; - -enum { - AM, PM, HR24, - - /* token types */ - Month = 1, - Year, - Day, - Timetok, - Tz, - Dtz, - Ignore, - Ampm, - - Maxtok = 6, /* only this many chars are stored in datetktbl */ - Maxdateflds = 25, +char *formats[] = { + /* asctime */ + "W MMM DD hh:mm:ss ZZZ YYYY", + /* RFC5322 */ + "?W ?DD ?MMM ?YYYY hh:mm:ss ?Z", + "?W, DD-?MM-YY hh:mm:ss ?Z", + /* RFC822/RFC8222 */ + "DD MMM YY hh:mm ZZZ", + "DD MMM YY hh:mm Z", + /* RFC850 */ + "W, DD-MMM-YY hh:mm:ss MST", + /* RFC1123 */ + "WW, DD MMM YYYY hh:mm:ss ZZZ", + /* RFC1123Z */ + "WW, DD MMM YYYY hh:mm:ss ZZ", + /* RFC3339 */ + "YYYY-MM-DD[T]hh:mm:ss[Z]ZZ", + "YYYY-MM-DD[T]hh:mm:ss[Z]Z", + "YYYY-MM-DD[T]hh:mm:ss ZZ", + "YYYY-MM-DD[T]hh:mm:ss Z", + /* RFC 3339 and human-readable variants */ + "YYYY-MM-DD hh:mm:ss", + "YYYY-MM-DD hh:mm:ss ?Z", + "YYYY-MM-DD [@] hh:mm:ss", + "YYYY-MM-DD [@] hh:mm:ss ?Z", + nil }; -/* - * macros for squeezing values into low 7 bits of "value". - * all timezones we care about are divisible by 10, and the largest value - * (780) when divided is 78. - */ -#define TOVAL(tp, v) ((tp)->value = (v) / 10) -#define FROMVAL(tp) ((tp)->value * 10) /* uncompress */ - -/* keep this struct small since we have an array of them */ -typedef struct { - char token[Maxtok]; - char type; - schar value; -} Datetok; - -int dtok_numparsed; - -/* forwards */ -Datetok *datetoktype(char *s, int *bigvalp); - -static Datetok datetktbl[]; -static unsigned szdatetktbl; - -/* parse 1- or 2-digit number, advance *cpp past it */ -static int -eatnum(char **cpp) -{ - int c, x; - char *cp; - - cp = *cpp; - c = *cp; - if (!isascii(c) || !isdigit(c)) - return -1; - x = c - '0'; - - c = *++cp; - if (isascii(c) && isdigit(c)) { - x = 10*x + c - '0'; - cp++; - } - *cpp = cp; - return x; -} - -/* return -1 on failure */ -int -parsetime(char *time, Tm *tm) -{ - tm->hour = eatnum(&time); - if (tm->hour == -1 || *time++ != ':') - return -1; /* only hour; too short */ - - tm->min = eatnum(&time); - if (tm->min == -1) - return -1; - if (*time++ != ':') { - tm->sec = 0; - return 0; /* no seconds; okay */ - } - - tm->sec = eatnum(&time); - if (tm->sec == -1) - return -1; - - /* this may be considered too strict. garbage at end of time? */ - return *time == '\0' || isascii(*time) && isspace(*time)? 0: -1; -} - -/* - * try to parse pre-split timestr in fields as an absolute date - */ -int -tryabsdate(char **fields, int nf, Tm *now, Tm *tm) -{ - int i, mer = HR24, bigval = -1; - long flg = 0, ty; - Datetok *tp; - - now = localtime(time(0)); /* default to local time (zone) */ - tm->tzoff = now->tzoff; - strncpy(tm->zone, now->zone, sizeof tm->zone); - - tm->mday = tm->mon = tm->year = -1; /* mandatory */ - tm->hour = tm->min = tm->sec = 0; - dtok_numparsed = 0; - - for (i = 0; i < nf; i++) { - if (fields[i][0] == '\0') - continue; - tp = datetoktype(fields[i], &bigval); - ty = (1L << tp->type) & ~(1L << Ignore); - if (flg & ty) - return -1; /* repeated type */ - flg |= ty; - switch (tp->type) { - case Year: - tm->year = bigval; - if (tm->year < 1970 || tm->year > 2106) - return -1; /* can't represent in ulong */ - /* convert 4-digit year to 1900 origin */ - if (tm->year >= 1900) - tm->year -= 1900; - break; - case Day: - tm->mday = bigval; - break; - case Month: - tm->mon = tp->value - 1; /* convert to zero-origin */ - break; - case Timetok: - if (parsetime(fields[i], tm) < 0) - return -1; - break; - case Dtz: - case Tz: - /* tm2sec mangles timezones, so we do our own handling */ - tm->tzoff = FROMVAL(tp); - snprint(tm->zone, sizeof(tm->zone), "GMT"); - break; - case Ignore: - break; - case Ampm: - mer = tp->value; - break; - default: - return -1; /* bad token type: CANTHAPPEN */ - } - } - if (tm->year == -1 || tm->mon == -1 || tm->mday == -1) - return -1; /* missing component */ - if (mer == PM) - tm->hour += 12; - return 0; -} - -int -prsabsdate(char *timestr, Tm *now, Tm *tm) -{ - int nf; - char *fields[Maxdateflds]; - static char delims[] = "- \t\n/,"; - - nf = gettokens(timestr, fields, nelem(fields), delims+1); - if (nf > nelem(fields)) - return -1; - if (tryabsdate(fields, nf, now, tm) < 0) { - char *p = timestr; - - /* - * could be a DEC-date; glue it all back together, split it - * with dash as a delimiter and try again. Yes, this is a - * hack, but so are DEC-dates. - */ - while (--nf > 0) { - while (*p++ != '\0') - ; - p[-1] = ' '; - } - nf = gettokens(timestr, fields, nelem(fields), delims); - if (nf > nelem(fields) || tryabsdate(fields, nf, now, tm) < 0) - return -1; - } - return 0; -} - -int -validtm(Tm *tm) -{ - if (tm->year < 0 || tm->mon < 0 || tm->mon > 11 || - tm->mday < 1 || tm->hour < 0 || tm->hour >= 24 || - tm->min < 0 || tm->min > 59 || - tm->sec < 0 || tm->sec > 61) /* allow 2 leap seconds */ - return 0; - return 1; -} - -Time -seconds(char *timestr) -{ - Tm date; - - memset(&date, 0, sizeof date); - if (prsabsdate(timestr, localtime(time(0)), &date) < 0) - return -1; - return validtm(&date)? tm2sec(&date) - 60*date.tzoff: -1; -} - -int -convert(char *timestr) -{ - char *copy; - Time tstime; - - copy = strdup(timestr); - if (copy == nil) - sysfatal("out of memory"); - tstime = seconds(copy); - free(copy); - if (tstime == -1) { - fprint(2, "%s: `%s' not a valid date\n", argv0, timestr); - return 1; - } - print("%lud\n", tstime); - return 0; -} - static void usage(void) { @@ -242,225 +43,31 @@ void main(int argc, char **argv) { - int i, sts; + Tmd tm; + char **f, *fmt; + int i; - sts = 0; + fmt = nil; ARGBEGIN{ + case 'f': + fmt = EARGF(usage()); + break; default: usage(); - }ARGEND - if (argc == 0) - usage(); - for (i = 0; i < argc; i++) - sts |= convert(argv[i]); - exits(sts != 0? "bad": 0); + }ARGEND; + + for(i = 0; i < argc; i++){ + if(fmt != nil){ + if(tmparse(fmt, argv[i], nil, &tm) != nil) + goto Found; + }else{ + for(f = formats; *f != nil; f++) + if(tmparse(&tm, *f, argv[i], nil) != nil) + goto Found; + } + sysfatal("tmparse: %r"); +Found: + print("%lld\n", tm.abs); + } + exits(nil); } - -/* - * Binary search -- from Knuth (6.2.1) Algorithm B. Special case like this - * is WAY faster than the generic bsearch(). - */ -Datetok * -datebsearch(char *key, Datetok *base, unsigned nel) -{ - int cmp; - Datetok *last = base + nel - 1, *pos; - - while (last >= base) { - pos = base + ((last - base) >> 1); - cmp = key[0] - pos->token[0]; - if (cmp == 0) { - cmp = strncmp(key, pos->token, Maxtok); - if (cmp == 0) - return pos; - } - if (cmp < 0) - last = pos - 1; - else - base = pos + 1; - } - return 0; -} - -Datetok * -datetoktype(char *s, int *bigvalp) -{ - char *cp = s; - char c = *cp; - static Datetok t; - Datetok *tp = &t; - - if (isascii(c) && isdigit(c)) { - int len = strlen(cp); - - if (len > 3 && (cp[1] == ':' || cp[2] == ':')) - tp->type = Timetok; - else { - if (bigvalp != nil) - *bigvalp = atoi(cp); /* won't fit in tp->value */ - if (len == 4) - tp->type = Year; - else if (++dtok_numparsed == 1) - tp->type = Day; - else - tp->type = Year; - } - } else if (c == '-' || c == '+') { - int val = atoi(cp + 1); - int hr = val / 100; - int min = val % 100; - - val = hr*60 + min; - TOVAL(tp, c == '-'? -val: val); - tp->type = Tz; - } else { - char lowtoken[Maxtok+1]; - char *ltp = lowtoken, *endltp = lowtoken+Maxtok; - - /* copy to lowtoken to avoid modifying s */ - while ((c = *cp++) != '\0' && ltp < endltp) - *ltp++ = (isascii(c) && isupper(c)? tolower(c): c); - *ltp = '\0'; - tp = datebsearch(lowtoken, datetktbl, szdatetktbl); - if (tp == nil) { - tp = &t; - tp->type = Ignore; - } - } - return tp; -} - - -/* - * to keep this table reasonably small, we divide the lexval for Tz and Dtz - * entries by 10 and truncate the text field at MAXTOKLEN characters. - * the text field is not guaranteed to be NUL-terminated. - */ -static Datetok datetktbl[] = { -/* text token lexval */ - "acsst", Dtz, 63, /* Cent. Australia */ - "acst", Tz, 57, /* Cent. Australia */ - "adt", Dtz, -18, /* Atlantic Daylight Time */ - "aesst", Dtz, 66, /* E. Australia */ - "aest", Tz, 60, /* Australia Eastern Std Time */ - "ahst", Tz, 60, /* Alaska-Hawaii Std Time */ - "am", Ampm, AM, - "apr", Month, 4, - "april", Month, 4, - "ast", Tz, -24, /* Atlantic Std Time (Canada) */ - "at", Ignore, 0, /* "at" (throwaway) */ - "aug", Month, 8, - "august", Month, 8, - "awsst", Dtz, 54, /* W. Australia */ - "awst", Tz, 48, /* W. Australia */ - "bst", Tz, 6, /* British Summer Time */ - "bt", Tz, 18, /* Baghdad Time */ - "cadt", Dtz, 63, /* Central Australian DST */ - "cast", Tz, 57, /* Central Australian ST */ - "cat", Tz, -60, /* Central Alaska Time */ - "cct", Tz, 48, /* China Coast */ - "cdt", Dtz, -30, /* Central Daylight Time */ - "cet", Tz, 6, /* Central European Time */ - "cetdst", Dtz, 12, /* Central European Dayl.Time */ - "cst", Tz, -36, /* Central Standard Time */ - "dec", Month, 12, - "decemb", Month, 12, - "dnt", Tz, 6, /* Dansk Normal Tid */ - "dst", Ignore, 0, - "east", Tz, -60, /* East Australian Std Time */ - "edt", Dtz, -24, /* Eastern Daylight Time */ - "eet", Tz, 12, /* East. Europe, USSR Zone 1 */ - "eetdst", Dtz, 18, /* Eastern Europe */ - "est", Tz, -30, /* Eastern Standard Time */ - "feb", Month, 2, - "februa", Month, 2, - "fri", Ignore, 5, - "friday", Ignore, 5, - "fst", Tz, 6, /* French Summer Time */ - "fwt", Dtz, 12, /* French Winter Time */ - "gmt", Tz, 0, /* Greenwish Mean Time */ - "gst", Tz, 60, /* Guam Std Time, USSR Zone 9 */ - "hdt", Dtz, -54, /* Hawaii/Alaska */ - "hmt", Dtz, 18, /* Hellas ? ? */ - "hst", Tz, -60, /* Hawaii Std Time */ - "idle", Tz, 72, /* Intl. Date Line, East */ - "idlw", Tz, -72, /* Intl. Date Line, West */ - "ist", Tz, 12, /* Israel */ - "it", Tz, 22, /* Iran Time */ - "jan", Month, 1, - "januar", Month, 1, - "jst", Tz, 54, /* Japan Std Time,USSR Zone 8 */ - "jt", Tz, 45, /* Java Time */ - "jul", Month, 7, - "july", Month, 7, - "jun", Month, 6, - "june", Month, 6, - "kst", Tz, 54, /* Korea Standard Time */ - "ligt", Tz, 60, /* From Melbourne, Australia */ - "mar", Month, 3, - "march", Month, 3, - "may", Month, 5, - "mdt", Dtz, -36, /* Mountain Daylight Time */ - "mest", Dtz, 12, /* Middle Europe Summer Time */ - "met", Tz, 6, /* Middle Europe Time */ - "metdst", Dtz, 12, /* Middle Europe Daylight Time*/ - "mewt", Tz, 6, /* Middle Europe Winter Time */ - "mez", Tz, 6, /* Middle Europe Zone */ - "mon", Ignore, 1, - "monday", Ignore, 1, - "mst", Tz, -42, /* Mountain Standard Time */ - "mt", Tz, 51, /* Moluccas Time */ - "ndt", Dtz, -15, /* Nfld. Daylight Time */ - "nft", Tz, -21, /* Newfoundland Standard Time */ - "nor", Tz, 6, /* Norway Standard Time */ - "nov", Month, 11, - "novemb", Month, 11, - "nst", Tz, -21, /* Nfld. Standard Time */ - "nt", Tz, -66, /* Nome Time */ - "nzdt", Dtz, 78, /* New Zealand Daylight Time */ - "nzst", Tz, 72, /* New Zealand Standard Time */ - "nzt", Tz, 72, /* New Zealand Time */ - "oct", Month, 10, - "octobe", Month, 10, - "on", Ignore, 0, /* "on" (throwaway) */ - "pdt", Dtz, -42, /* Pacific Daylight Time */ - "pm", Ampm, PM, - "pst", Tz, -48, /* Pacific Standard Time */ - "sadt", Dtz, 63, /* S. Australian Dayl. Time */ - "sast", Tz, 57, /* South Australian Std Time */ - "sat", Ignore, 6, - "saturd", Ignore, 6, - "sep", Month, 9, - "sept", Month, 9, - "septem", Month, 9, - "set", Tz, -6, /* Seychelles Time ?? */ - "sst", Dtz, 12, /* Swedish Summer Time */ - "sun", Ignore, 0, - "sunday", Ignore, 0, - "swt", Tz, 6, /* Swedish Winter Time */ - "thu", Ignore, 4, - "thur", Ignore, 4, - "thurs", Ignore, 4, - "thursd", Ignore, 4, - "tue", Ignore, 2, - "tues", Ignore, 2, - "tuesda", Ignore, 2, - "ut", Tz, 0, - "utc", Tz, 0, - "wadt", Dtz, 48, /* West Australian DST */ - "wast", Tz, 42, /* West Australian Std Time */ - "wat", Tz, -6, /* West Africa Time */ - "wdt", Dtz, 54, /* West Australian DST */ - "wed", Ignore, 3, - "wednes", Ignore, 3, - "weds", Ignore, 3, - "wet", Tz, 0, /* Western Europe */ - "wetdst", Dtz, 6, /* Western Europe */ - "wst", Tz, 48, /* West Australian Std Time */ - "ydt", Dtz, -48, /* Yukon Daylight Time */ - "yst", Tz, -54, /* Yukon Standard Time */ - "zp4", Tz, -24, /* GMT +4 hours. */ - "zp5", Tz, -30, /* GMT +5 hours. */ - "zp6", Tz, -36, /* GMT +6 hours. */ -}; -static unsigned szdatetktbl = nelem(datetktbl);