From 56e869ac7004a635a7d63596ee751275484f28c3 Mon Sep 17 00:00:00 2001 From: Ori Bernstein Date: Sun, 9 Aug 2020 18:58:44 -0700 Subject: [PATCH] libc: new date apis The current date and time APIs on Plan 9 are not good. They're inflexible, non-threadsafe, and don't expose timezone information. This commit adds new time APIs that allow parsing arbitrary dates, work from multiple threads, and can handle timezones effectively. --- sys/include/libc.h | 42 +- sys/man/2/tmdate | 275 ++++++++++ sys/src/libc/9sys/ctime.c | 306 +----------- sys/src/libc/9sys/tm2sec.c | 197 +------- sys/src/libc/port/date.c | 996 +++++++++++++++++++++++++++++++++++++ sys/src/libc/port/mkfile | 1 + 6 files changed, 1329 insertions(+), 488 deletions(-) create mode 100644 sys/man/2/tmdate create mode 100644 sys/src/libc/port/date.c diff --git a/sys/include/libc.h b/sys/include/libc.h index 7a2700539..4eecd028f 100644 --- a/sys/include/libc.h +++ b/sys/include/libc.h @@ -314,22 +314,44 @@ extern double fmod(double, double); /* * Time-of-day */ +typedef struct Tzone Tzone; +#pragma incomplete Tzone + typedef struct Tm { - int sec; - int min; - int hour; - int mday; - int mon; - int year; - int wday; - int yday; - char zone[4]; - int tzoff; + int nsec; /* nseconds (range 0...1e9) */ + int sec; /* seconds (range 0..60) */ + int min; /* minutes (0..59) */ + int hour; /* hours (0..23) */ + int mday; /* day of the month (1..31) */ + int mon; /* month of the year (0..11) */ + int year; /* year A.D. */ + int wday; /* day of week (0..6, Sunday = 0) */ + int yday; /* day of year (0..365) */ + char zone[16]; /* time zone name */ + int tzoff; /* time zone delta from GMT */ + Tzone *tz; /* time zone associated with this date */ } Tm; +typedef +struct Tmfmt { + char *fmt; + Tm *tm; +} Tmfmt; + +#pragma varargck type "τ" Tmfmt + +extern Tzone* tzload(char *name); +extern Tm* tmnow(Tm*, Tzone*); +extern Tm* tmtime(Tm*, vlong, Tzone*); +extern Tm* tmtimens(Tm*, vlong, int, Tzone*); +extern Tm* tmparse(Tm*, char*, char*, Tzone*, char **ep); +extern vlong tmnorm(Tm*); +extern Tmfmt tmfmt(Tm*, char*); +extern void tmfmtinstall(void); + extern Tm* gmtime(long); extern Tm* localtime(long); extern char* asctime(Tm*); diff --git a/sys/man/2/tmdate b/sys/man/2/tmdate new file mode 100644 index 000000000..0b314a100 --- /dev/null +++ b/sys/man/2/tmdate @@ -0,0 +1,275 @@ +.TH TMDATE 2 +.SH NAME +tmnow, tzload, tmtime, tmparse, tmfmt, tmnorm - convert date and time +.SH SYNOPSIS +.B #include +.br +.B #include +.PP +.ft L +.nf +.EX +typedef struct Tmd Tmd; +typedef struct Tmfmt Tmfmt; + +struct { + int nsec; /* nanoseconds (range 0..1e9) */ + int sec; /* seconds (range 0..59) */ + int min; /* minutes (0..59) */ + int hour; /* hours (0..23) */ + int mday; /* day of the month (1..31) */ + int mon; /* month of the year (0..11) */ + int year; /* C.E year - 1900 */ + int wday; /* day of week (0..6, Sunday = 0) */ + int yday; /* day of year (0..365) */ + char zone[]; /* time zone name */ + int tzoff; /* time zone delta from GMT, seconds */ +}; + +Tzone *tzload(char *name); +Tm *tmnow(Tm *tm, char *tz); +Tm *tmtime(Tm *tm, vlong abs, Tzone *tz); +Tm *tmtimens(Tm *tm, vlong abs, int ns, Tzone *tz); +Tm *tmparse(Tm *dst, char *fmt, char *tm, Tzone *zone, char **ep); +vlong tmnorm(Tm *tm); +Tmfmt tmfmt(Tm *tm, char *fmt); +void tmfmtinstall(void); +.EE +.SH DESCRIPTION +.PP +This family of functions handles simple date and time manipulation. +.PP +Time zones are loaded by name. +They can be specified as the abbreviated timezone name, +the full timezone name, the path to a timezone file, +or an absolute offset in the HHMM form. +.PP +When given as a timezone, any instant-dependent adjustments such as leap +seconds and daylight savings time will be applied to the derived fields of +struct Tm, but will not affect the absolute time. +The time zone name local always refers to the time in /env/timezone. +The nil timezone always refers to GMT. +.PP +Tzload loads a timezone by name. The returned timezone is +cached for the lifetime of the program, and should not be freed. +Loading a timezone repeatedly by name loads from the cache, and +does not leak. +.PP +Tmnow gets the current time of day in the requested time zone. +.PP +Tmtime converts the millisecond-resolution timestamp 'abs' +into a Tm struct in the requested timezone. +Tmtimens does the same, but with a nanosecond accuracy. +.PP +Tmstime is identical to tmtime, but accepts the time in sec- +onds. +.PP +Tmparse parses a time from a string according to the format argument. +The point at which the parsing stopped is returned in +.IR ep . +If +.I ep +is nil, trailing garbage is ignored. +The result is returned in the timezone requested. +If there is a timezone in the date, and a timezone is provided +when parsing, then the zone is shifted to the provided timezone. +Parsing is case-insensitive +.PP +The format argument contains zero or more of the following components: +.TP +.B Y, YY, YYYY +Represents the year. +.I YY +prints the year in 2 digit form. +.TP +.B M, MM, MMM, MMMM +The month of the year, in unpadded numeric, padded numeric, short name, or long name, +respectively. +.TP +.B D, DD +The day of month in unpadded or padded numeric form, respectively. +.TP +.B W, WW, WWW +The day of week in numeric, short or long name form, respectively. +.TP +.B h, hh +The hour in unpadded or padded form, respectively +.TP +.B m, mm +The minute in unpadded or padded form, respectively +.TP +.B s, ss +The second in unpadded or padded form, respectively +.TP +.B t, tt +The milliseconds in unpadded and padded form, respectively. +.B u, uu, uuu, uuuu +The microseconds in unpadded. padded form modulo milliseconds, +or unpadded, padded forms of the complete value, respectively. +.B n, nn, nnn, nnnn, nnnnn, nnnnnn +The nanoseconds in unpadded and padded form modulo milliseconds, +the unpadded and padded form modulo microseconds, +and the unpadded and padded complete value, respectively. +.TP +.B Z, ZZ, ZZZ +The timezone in [+-]HHMM and [+-]HH:MM, and named form, respectively. +.TP +.B a, A +Lower and uppercase 'am' and 'pm' specifiers, respectively. +.TP +.B [...] +Quoted text, copied directly to the output. +.TP +.B _ +When formatting, this inserts padding into the date format. +The padded width of a field is the sum of format and specifier +characters combined. When +For example, +.I __h +will format to a width of 3. When parsing, this acts as whitespace. +.TP +.B ? +When parsing, this makes the following argument match fuzzily. +Fuzzy matching means that all formats are tried, from most to least specific. +For example, +.I ?M +will match +.IR January , +.IR Jan , +.IR 01 , +and +.IR 1 , +in that order of preference. +.TP +.B ~ +When parsing a date, this slackens range enforcement, accepting +out of range values such as January +.IR 32 , +which would get normalized to February 1st. +.PP +Any characters not specified above are copied directly to output, +without modification. + + + +.PP +If the format argument is nil, it makes an +attempt to parse common human readable date formats. These +formats include ISO-8601, RFC3339 and RFC2822 dates. +. +.PP +Tmfmt produces a format description structure suitable for passing +to +.IR fmtprint (2) . +If fmt is nil, we default to the format used in +.IR ctime (2). +The format of the format string is identical to +.IR tmparse. + +.PP +When parsing, any amount of whitespace is treated as a single token. +All string matches are case insensitive, and zero padding is optional. + +.PP +Tmnorm takes a manually adjusted Tm structure, and normalizes it, +returning the absolute timestamp that the date represents. +Normalizing recomputes the +.I year, mon, mday, hr, min, sec +and +.I tzoff +fields. +If +.I tz +is non-nil, then +.I tzoff +will be recomputed, taking into account daylight savings +for the absolute time. +The values not used in the computation are recomputed for +the resulting absolute time. +All out of range values are wrapped. +For example, December 32 will roll over to Jan 1 of the +following year. +.PP +Tmfmtinstall installs a time format specifier %τ. The time +format behaves as in tmfmt + +.SH EXAMPLES +.PP +All examples assume tmfmtinstall has been called. +.PP +Get the current date in the local timezone, UTC, and +US_Pacific time. Print it using the default format. + +.IP +.EX +Tm t; +Tzone *zl, *zp; +if((zl = tzload("local") == nil) + sysfatal("load zone: %r"); +if((zp = tzload("US_Pacific") == nil) + sysfatal("load zone: %r"); +print("local: %τ\\n", tmfmt(tmnow(&t, zl), nil)); +print("gmt: %τ\\n", tmfmt(tmnow(&t, nil), nil)); +print("eastern: %τ\\n", tmfmt(tmnow(&t, zp), nil)); +.EE +.PP +Compare if two times are the same, regardless of timezone. +Done with full, strict error checking. + +.IP +.EX +Tm a, b; + +if(tmparse(&a, nil, "Tue Dec 10 12:36:00 PST 2019", &e) == nil) + sysfatal("failed to parse: %r"); +if(*e != '\0') + sysfatal("trailing junk %s", e); +if(tmparse(&b, nil, "Tue Dec 10 15:36:00 EST 2019", &e) == nil) + sysfata("failed to parse: %r"); +if(*e != '\0') + sysfatal("trailing junk %s", e); +if(tmnorm(a) == tmnorm(b) && a.nsec == b.nsec) + print("same\\n"); +else + print("different\\n"); +.EE + +.PP +Convert from one timezone to another. + +.IP +.EX +Tm here, there; +Tzone *zl, *zp; +if((zl = tzload("local")) == nil) + sysfatal("load zone: %r"); +if((zp = tzload("US_Pacific")) == nil) + sysfatal("load zone: %r"); +if(tmnow(&here, zl) == nil) + sysfatal("get time: %r"); +if(tmtime(&there, tmnorm(&tm), zp) == nil) + sysfatal("shift time: %r"); +.EE + +.PP +Add a day. Because cross daylight savings, only 23 hours are added. + +.EX +Tm t; +char *date = "Sun Nov 2 13:11:11 PST 2019"; +if(tmparse(&t, "W MMM D hh:mm:ss z YYYY, date, &e) == nil) + print("failed top parse"); +tm.day++; +tmnorm(&t); +print("%τ", &t); /* Mon Nov 3 13:11:11 PST 2019 */ +.EE + +.SH BUGS +.PP +There is no way to format specifier for subsecond precision. +.PP +The timezone information that we ship is out of date. +.PP +The Plan 9 timezone format has no way to express leap seconds. +.PP +We provide no way to manipulate timezones. diff --git a/sys/src/libc/9sys/ctime.c b/sys/src/libc/9sys/ctime.c index 11340ea41..b334c9847 100644 --- a/sys/src/libc/9sys/ctime.c +++ b/sys/src/libc/9sys/ctime.c @@ -1,301 +1,39 @@ -/* - * This routine converts time as follows. - * The epoch is 0000 Jan 1 1970 GMT. - * The argument time is in seconds since then. - * The localtime(t) entry returns a pointer to an array - * containing - * - * seconds (0-59) - * minutes (0-59) - * hours (0-23) - * day of month (1-31) - * month (0-11) - * year-1970 - * weekday (0-6, Sun is 0) - * day of the year - * daylight savings flag - * - * The routine gets the daylight savings time from the environment. - * - * asctime(tvec)) - * where tvec is produced by localtime - * returns a ptr to a character string - * that has the ascii time in the form - * - * \\ - * Thu Jan 01 00:00:00 GMT 1970n0 - * 012345678901234567890123456789 - * 0 1 2 - * - * ctime(t) just calls localtime, then asctime. - */ - #include #include -static char dmsize[12] = -{ - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 -}; - -/* - * The following table is used for 1974 and 1975 and - * gives the day number of the first day after the Sunday of the - * change. - */ - -static int dysize(int); -static void ct_numb(char*, int); - -#define TZSIZE 150 -static void readtimezone(void); -static int rd_name(char**, char*); -static int rd_long(char**, long*); -static -struct -{ - char stname[4]; - char dlname[4]; - long stdiff; - long dldiff; - long dlpairs[TZSIZE]; -} timezone; - -char* -ctime(long t) -{ - return asctime(localtime(t)); -} - Tm* localtime(long tim) { - Tm *ct; - long t, *p; - int dlflag; + static Tm tm; + Tzone *tz; - if(timezone.stname[0] == 0) - readtimezone(); - t = tim + timezone.stdiff; - dlflag = 0; - for(p = timezone.dlpairs; *p; p += 2) - if(t >= p[0]) - if(t < p[1]) { - t = tim + timezone.dldiff; - dlflag++; - break; - } - ct = gmtime(t); - if(dlflag){ - strcpy(ct->zone, timezone.dlname); - ct->tzoff = timezone.dldiff; - } else { - strcpy(ct->zone, timezone.stname); - ct->tzoff = timezone.stdiff; - } - return ct; + /* + * We have no way to report errors, + * so we just ignore them here. + */ + tz = tzload("local"); + tmtime(&tm, tim, tz); + return &tm; } Tm* -gmtime(long tim) +gmtime(long abs) { - int d0, d1; - long hms, day; - static Tm xtime; - - /* - * break initial number into days - */ - hms = tim % 86400L; - day = tim / 86400L; - if(hms < 0) { - hms += 86400L; - day -= 1; - } - - /* - * generate hours:minutes:seconds - */ - xtime.sec = hms % 60; - d1 = hms / 60; - xtime.min = d1 % 60; - d1 /= 60; - xtime.hour = d1; - - /* - * day is the day number. - * generate day of the week. - * The addend is 4 mod 7 (1/1/1970 was Thursday) - */ - - xtime.wday = (day + 7340036L) % 7; - - /* - * year number - */ - if(day >= 0) - for(d1 = 1970; day >= dysize(d1); d1++) - day -= dysize(d1); - else - for (d1 = 1970; day < 0; d1--) - day += dysize(d1-1); - xtime.year = d1-1900; - xtime.yday = d0 = day; - - /* - * generate month - */ - - if(dysize(d1) == 366) - dmsize[1] = 29; - for(d1 = 0; d0 >= dmsize[d1]; d1++) - d0 -= dmsize[d1]; - dmsize[1] = 28; - xtime.mday = d0 + 1; - xtime.mon = d1; - strcpy(xtime.zone, "GMT"); - return &xtime; + static Tm tm; + return tmtime(&tm, abs, nil); } char* -asctime(Tm *t) +ctime(long abs) { - char *ncp; - static char cbuf[30]; + Tzone *tz; + Tm tm; - strcpy(cbuf, "Thu Jan 01 00:00:00 GMT 1970\n"); - ncp = &"SunMonTueWedThuFriSat"[t->wday*3]; - cbuf[0] = *ncp++; - cbuf[1] = *ncp++; - cbuf[2] = *ncp; - ncp = &"JanFebMarAprMayJunJulAugSepOctNovDec"[t->mon*3]; - cbuf[4] = *ncp++; - cbuf[5] = *ncp++; - cbuf[6] = *ncp; - ct_numb(cbuf+8, t->mday); - ct_numb(cbuf+11, t->hour+100); - ct_numb(cbuf+14, t->min+100); - ct_numb(cbuf+17, t->sec+100); - ncp = t->zone; - cbuf[20] = *ncp++; - cbuf[21] = *ncp++; - cbuf[22] = *ncp; - ct_numb(cbuf+24, (t->year+1900) / 100 + 100); - ct_numb(cbuf+26, t->year+100); - return cbuf; -} - -static -dysize(int y) -{ - - if(y%4 == 0 && (y%100 != 0 || y%400 == 0)) - return 366; - return 365; -} - -static -void -ct_numb(char *cp, int n) -{ - - cp[0] = ' '; - if(n >= 10) - cp[0] = (n/10)%10 + '0'; - cp[1] = n%10 + '0'; -} - -static -void -readtimezone(void) -{ - char buf[TZSIZE*11+30], *p; - int i; - - memset(buf, 0, sizeof(buf)); - i = open("/env/timezone", 0); - if(i < 0) - goto error; - if(read(i, buf, sizeof(buf)) >= sizeof(buf)){ - close(i); - goto error; - } - close(i); - p = buf; - if(rd_name(&p, timezone.stname)) - goto error; - if(rd_long(&p, &timezone.stdiff)) - goto error; - if(rd_name(&p, timezone.dlname)) - goto error; - if(rd_long(&p, &timezone.dldiff)) - goto error; - for(i=0; i '9') - return 1; - l = l*10 + c-'0'; - c = *(*f)++; - } - if(s) - l = -l; - *p = l; - return 0; + /* + * We have no way to report errors, + * so we just ignore them here. + */ + tz = tzload("local"); + tmtime(&tm, abs, tz); + return asctime(&tm); } diff --git a/sys/src/libc/9sys/tm2sec.c b/sys/src/libc/9sys/tm2sec.c index 20bebd0c8..8b3a9d967 100644 --- a/sys/src/libc/9sys/tm2sec.c +++ b/sys/src/libc/9sys/tm2sec.c @@ -1,202 +1,11 @@ #include #include -#define TZSIZE 150 -static void readtimezone(void); -static int rd_name(char**, char*); -static int rd_long(char**, long*); -static -struct -{ - char stname[4]; - char dlname[4]; - long stdiff; - long dldiff; - long dlpairs[TZSIZE]; -} timezone; - -#define SEC2MIN 60L -#define SEC2HOUR (60L*SEC2MIN) -#define SEC2DAY (24L*SEC2HOUR) - -/* - * days per month plus days/year - */ -static int dmsize[] = -{ - 365, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 -}; -static int ldmsize[] = -{ - 366, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 -}; - -/* - * return the days/month for the given year - */ -static int * -yrsize(int y) -{ - if((y%4) == 0 && ((y%100) != 0 || (y%400) == 0)) - return ldmsize; - else - return dmsize; -} - -/* - * compute seconds since Jan 1 1970 GMT - * and convert to our timezone. - */ long tm2sec(Tm *tm) { - long secs, *p; - int i, yday, year, *d2m; + Tm tt; - if(strcmp(tm->zone, "GMT") != 0 && timezone.stname[0] == 0) - readtimezone(); - secs = 0; - - /* - * seconds per year - */ - year = tm->year + 1900; - for(i = 1970; i < year; i++){ - d2m = yrsize(i); - secs += d2m[0] * SEC2DAY; - } - - /* - * if mday is set, use mon and mday to compute yday - */ - if(tm->mday){ - yday = 0; - d2m = yrsize(year); - for(i=0; imon; i++) - yday += d2m[i+1]; - yday += tm->mday-1; - }else{ - yday = tm->yday; - } - secs += yday * SEC2DAY; - - /* - * hours, minutes, seconds - */ - secs += tm->hour * SEC2HOUR; - secs += tm->min * SEC2MIN; - secs += tm->sec; - - /* - * Only handles zones mentioned in /env/timezone, - * but things get too ambiguous otherwise. - */ - if(strcmp(tm->zone, timezone.stname) == 0) - secs -= timezone.stdiff; - else if(strcmp(tm->zone, timezone.dlname) == 0) - secs -= timezone.dldiff; - else if(tm->zone[0] == 0){ - secs -= timezone.dldiff; - for(p = timezone.dlpairs; *p; p += 2) - if(secs >= p[0] && secs < p[1]) - break; - if(*p == 0){ - secs += timezone.dldiff; - secs -= timezone.stdiff; - } - } - return secs; -} - -static -void -readtimezone(void) -{ - char buf[TZSIZE*11+30], *p; - int i; - - memset(buf, 0, sizeof(buf)); - i = open("/env/timezone", 0); - if(i < 0) - goto error; - if(read(i, buf, sizeof(buf)) >= sizeof(buf)) - goto error; - close(i); - p = buf; - if(rd_name(&p, timezone.stname)) - goto error; - if(rd_long(&p, &timezone.stdiff)) - goto error; - if(rd_name(&p, timezone.dlname)) - goto error; - if(rd_long(&p, &timezone.dldiff)) - goto error; - for(i=0; i '9') - return 1; - l = l*10 + c-'0'; - c = *(*f)++; - } - if(s) - l = -l; - *p = l; - return 0; + tt = *tm; + return tmnorm(&tt); } diff --git a/sys/src/libc/port/date.c b/sys/src/libc/port/date.c new file mode 100644 index 000000000..0e9669eeb --- /dev/null +++ b/sys/src/libc/port/date.c @@ -0,0 +1,996 @@ +#include +#include + +typedef struct Tzabbrev Tzabbrev; +typedef struct Tzoffpair Tzoffpair; + +#define Ctimefmt "WW MMM _D hh:mm:ss ZZZ YYYY" +#define P(pad, w) ((pad) < (w) ? 0 : pad - w) + +enum { + Tzsize = 150, + Nsec = 1000*1000*1000, + Usec = 1000*1000, + Msec = 1000, + Daysec = (vlong)24*3600, + Days400y = 365*400 + 4*25 - 3, + Days4y = 365*4 + 1, +}; + +enum { + Cend, + Cspace, + Cnum, + Cletter, + Cpunct, +}; + +struct Tzone { + char tzname[32]; + char stname[16]; + char dlname[16]; + long stdiff; + long dldiff; + long dlpairs[150]; +}; + +static QLock zlock; +static int nzones; +static Tzone **zones; +static int mdays[] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 +}; +static char *wday[] = { + "Sunday","Monday","Tuesday", + "Wednesday","Thursday","Friday", + "Saturday", nil, +}; +static char *month[] = { + "January", "February", "March", + "April", "May", "June", "July", + "August", "September", "October", + "November", "December", nil +}; + +struct Tzabbrev { + char *abbr; + char *name; +}; + +struct Tzoffpair { + char *abbr; + int off; +}; + +#define isalpha(c)\ + (((c)|0x60) >= 'a' && ((c)|0x60) <= 'z') + +/* Obsolete time zone names. Hardcoded to match RFC5322 */ +static Tzabbrev tzabbrev[] = { + {"UT", "GMT"}, {"GMT", "GMT"}, {"UTC", "GMT"}, + {"EST", "US_Eastern"}, {"EDT", "US_Eastern"}, + {"CST", "US_Central"}, {"CDT", "US_Central"}, + {"MST", "US_Mountain"}, {"MDT", "US_Mountain"}, + {"PST", "US_Pacific"}, {"PDT", "US_Pacific"}, + {nil}, +}; + +/* Military timezone names */ +static Tzoffpair milabbrev[] = { + {"A", -1*3600}, {"B", -2*3600}, {"C", -3*3600}, + {"D", -4*3600}, {"E", -5*3600}, {"F", -6*3600}, + {"G", -7*3600}, {"H", -8*3600}, {"I", -9*3600}, + {"K", -10*3600}, {"L", -11*3600}, {"M", -12*3600}, + {"N", +1*3600}, {"O", +2*3600}, {"P", +3*3600}, + {"Q", +4*3600}, {"R", +5*3600}, {"S", +6*3600}, + {"T", +7*3600}, {"U", +8*3600}, {"V", +9*3600}, + {"W", +10*3600}, {"X", +11*3600}, {"Y", +12*3600}, + {"Z", 0}, {nil, 0} +}; + +static vlong +mod(vlong a, vlong b) +{ + vlong r; + + r = a % b; + if(r < 0) + r += b; + return r; +} + +static int +isleap(int y) +{ + return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); +} + +static int +rdname(char **f, char *p, int n) +{ + char *s, *e; + + for(s = *f; *s; s++) + if(*s != ' ' && *s != '\t' && *s != '\n') + break; + e = s + n; + for(; *s && s != e; s++) { + if(*s == ' ' || *s == '\t' || *s == '\n') + break; + *p++ = *s; + } + *p = 0; + if(n - (e - s) < 3 || *s != ' ' && *s != '\t' && *s != '\n'){ + werrstr("truncated name"); + return -1; + } + *f = s; + return 0; +} + +static int +rdlong(char **f, long *p) +{ + int c, s; + long l; + + s = 0; + while((c = *(*f)++) != 0){ + if(c == '-') + s++; + else if(c != ' ' && c != '\n') + break; + } + if(c == 0) { + *p = 0; + return 0; + } + l = 0; + for(;;) { + if(c == ' ' || c == '\n') + break; + if(c < '0' || c > '9'){ + werrstr("non-number %c in name", c); + return -1; + } + l = l*10 + c-'0'; + c = *(*f)++; + } + if(s) + l = -l; + *p = l; + return 0; +} + +static int +loadzone(Tzone *tz, char *name) +{ + char buf[Tzsize*11+30], path[128], *p; + int i, f, r; + + memset(tz, 0, sizeof(Tzone)); + if(strcmp(name, "local") == 0) + snprint(path, sizeof(path), "/env/timezone"); + else + snprint(path, sizeof(path), "/adm/timezone/%s", name); + memset(buf, 0, sizeof(buf)); + if((f = open(path, 0)) == -1) + return -1; + r = read(f, buf, sizeof(buf)); + close(f); + if(r == sizeof(buf) || r == -1) + return -1; + buf[r] = 0; + p = buf; + if(rdname(&p, tz->stname, sizeof(tz->stname)) == -1) + return -1; + if(rdlong(&p, &tz->stdiff) == -1) + return -1; + if(rdname(&p, tz->dlname, sizeof(tz->dlname)) == -1) + return -1; + if(rdlong(&p, &tz->dldiff) == -1) + return -1; + for(i=0; i < Tzsize; i++) { + if(rdlong(&p, &tz->dlpairs[i]) == -1){ + werrstr("invalid transition time"); + return -1; + } + if(tz->dlpairs[i] == 0) + return 0; + } + werrstr("invalid timezone %s", name); + return -1; +} + +Tzone* +tzload(char *tzname) +{ + Tzone *tz, **newzones; + int i; + + if(tzname == nil) + tzname = "GMT"; + qlock(&zlock); + for(i = 0; i < nzones; i++){ + tz = zones[i]; + if(strcmp(tz->stname, tzname) == 0) + goto found; + if(strcmp(tz->dlname, tzname) == 0) + goto found; + if(strcmp(tz->tzname, tzname) == 0) + goto found; + } + + tz = malloc(sizeof(Tzone)); + if(tz == nil) + goto error; + newzones = realloc(zones, (nzones + 1) * sizeof(Tzone*)); + if(newzones == nil) + goto error; + if(loadzone(tz, tzname) != 0) + goto error; + if(snprint(tz->tzname, sizeof(tz->tzname), tzname) >= sizeof(tz->tzname)){ + werrstr("timezone name too long"); + return nil; + } + zones = newzones; + zones[nzones] = tz; + nzones++; +found: + qunlock(&zlock); + return tz; +error: + free(tz); + qunlock(&zlock); + return nil; +} + +static void +tzoffset(Tzone *tz, vlong abs, Tm *tm) +{ + long dl, *p; + dl = 0; + if(tz == nil){ + snprint(tm->zone, sizeof(tm->zone), "GMT"); + tm->tzoff = 0; + return; + } + for(p = tz->dlpairs; *p; p += 2) + if(abs > p[0] && abs <= p[1]){ + dl = 1; + break; + } + if(dl){ + snprint(tm->zone, sizeof(tm->zone), tz->dlname); + tm->tzoff = tz->dldiff; + }else{ + snprint(tm->zone, sizeof(tm->zone), tz->stname); + tm->tzoff = tz->stdiff; + } +} + +static Tm* +tmfill(Tm *tm, vlong abs, vlong nsec) +{ + vlong zrel, j, y, m, d, t, e; + int i; + + zrel = abs + tm->tzoff; + t = zrel % Daysec; + e = zrel / Daysec; + if(t < 0){ + t += Daysec; + e -= 1; + } + + t += nsec/Nsec; + tm->sec = mod(t, 60); + t /= 60; + tm->min = mod(t, 60); + t /= 60; + tm->hour = mod(t, 24); + tm->wday = mod((e + 4), 7); + + /* + * Split up year, month, day. + * + * Implemented according to "Algorithm 199, + * conversions between calendar date and + * Julian day number", Robert G. Tantzen, + * Air Force Missile Development + * Center, Holloman AFB, New Mex. + * + * Lots of magic. + */ + j = (zrel + 2440588 * Daysec) / (Daysec) - 1721119; + y = (4 * j - 1) / Days400y; + j = 4 * j - 1 - Days400y * y; + d = j / 4; + j = (4 * d + 3) / Days4y; + d = 4 * d + 3 - Days4y * j; + d = (d + 4) / 4 ; + m = (5 * d - 3) / 153; + d = 5 * d - 3 - 153 * m; + d = (d + 5) / 5; + y = 100 * y + j; + + if(m < 10) + m += 3; + else{ + m -= 9; + y++; + } + + /* there's no year 0 */ + if(y <= 0) + y--; + /* and if j negative, the day and month are also negative */ + if(m < 0) + m += 12; + if(d < 0) + d += mdays[m - 1]; + + tm->yday = d; + for(i = 0; i < m - 1; i++) + tm->yday += mdays[i]; + if(m > 1 && isleap(y)) + tm->yday++; + tm->year = y - 1900; + tm->mon = m - 1; + tm->mday = d; + tm->nsec = mod(nsec, Nsec); + return tm; +} + + +Tm* +tmtime(Tm *tm, vlong abs, Tzone *tz) +{ + return tmtimens(tm, abs, 0, tz); +} + +Tm* +tmtimens(Tm *tm, vlong abs, int ns, Tzone *tz) +{ + tm->tz = tz; + tzoffset(tz, abs, tm); + return tmfill(tm, abs, ns); +} + +Tm* +tmnow(Tm *tm, Tzone *tz) +{ + vlong ns; + + ns = nsec(); + return tmtimens(tm, nsec()/Nsec, mod(ns, Nsec), tz); +} + +vlong +tmnorm(Tm *tm) +{ + vlong c, yadj, j, abs, y, m, d; + + if(tm->mon > 1){ + m = tm->mon - 2; + y = tm->year + 1900; + }else{ + m = tm->mon + 10; + y = tm->year + 1899; + } + d = tm->mday; + c = y / 100; + yadj = y - 100 * c; + j = (c * Days400y / 4 + + Days4y * yadj / 4 + + (153 * m + 2)/5 + d - + 719469); + abs = j * Daysec; + abs += tm->hour * 3600; + abs += tm->min * 60; + abs += tm->sec; + if(tm->tz){ + tzoffset(tm->tz, abs - tm->tzoff, tm); + tzoffset(tm->tz, abs - tm->tzoff, tm); + } + abs -= tm->tzoff; + tmfill(tm, abs, tm->nsec); + return abs; +} + +static int +τconv(Fmt *f) +{ + int depth, n, v, w, h, m, c0, sgn, pad, off; + char *p, *am; + Tmfmt tf; + Tm *tm; + + n = 0; + tf = va_arg(f->args, Tmfmt); + tm = tf.tm; + p = tf.fmt; + if(p == nil) + p = Ctimefmt; + while(*p){ + w = 1; + pad = 0; + while(*p == '_'){ + pad++; + p++; + } + c0 = *p++; + while(c0 && *p == c0){ + w++; + p++; + } + pad += w; + switch(c0){ + case 0: + break; + case 'Y': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->year + 1900); break; + case 2: n += fmtprint(f, "%*d", pad, tm->year % 100); break; + case 4: n += fmtprint(f, "%*d", pad, tm->year + 1900); break; + default: goto badfmt; + } + break; + case 'M': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->mon + 1); break; + case 2: n += fmtprint(f, "%*s%02d", pad-2, "", tm->mon + 1); break; + case 3: n += fmtprint(f, "%*.3s", pad, month[tm->mon]); break; + case 4: n += fmtprint(f, "%*s", pad, month[tm->mon]); break; + default: goto badfmt; + } + break; + case 'D': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->mday); break; + case 2: n += fmtprint(f, "%*s%02d", pad-2, "", tm->mday); break; + default: goto badfmt; + } + break; + case 'W': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->wday + 1); break; + case 2: n += fmtprint(f, "%*.3s", pad, wday[tm->wday]); break; + case 3: n += fmtprint(f, "%*s", pad, wday[tm->wday]); break; + default: goto badfmt; + } + break; + case 'H': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->hour % 12); break; + case 2: n += fmtprint(f, "%*s%02d", pad-2, "", tm->hour % 12); break; + default: goto badfmt; + } + break; + case 'h': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->hour); break; + case 2: n += fmtprint(f, "%*s%02d", pad-2, "", tm->hour); break; + default: goto badfmt; + } + break; + case 'm': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->min); break; + case 2: n += fmtprint(f, "%*s%02d", pad-2, "", tm->min); break; + default: goto badfmt; + } + break; + case 's': + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, tm->sec); break; + case 2: n += fmtprint(f, "%*s%02d", pad-2, "", tm->sec); break; + default: goto badfmt; + } + break; + case 't': + v = tm->nsec / (1000*1000); + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, v % 1000); break; + case 2: + case 3: n += fmtprint(f, "%*s%03d", P(pad, 3), "", v % 1000); break; + default: goto badfmt; + } + break; + case 'u': + v = tm->nsec / 1000; + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, v % 1000); break; + case 2: n += fmtprint(f, "%*s%03d", P(pad, 3), "", v % 1000); break; + case 3: n += fmtprint(f, "%*d", P(pad, 6), v); break; + case 4: n += fmtprint(f, "%*s%06d", P(pad, 6), "", v); break; + default: goto badfmt; + } + break; + case 'n': + v = tm->nsec; + switch(w){ + case 1: n += fmtprint(f, "%*d", pad, v%1000); break; + case 2: n += fmtprint(f, "%*s%03d", P(pad, 3), "", v % 1000); break; + case 3: n += fmtprint(f, "%*d", pad , v%(1000*1000)); break; + case 4: n += fmtprint(f, "%*s%06d", P(pad, 6), "", v%(1000000)); break; + case 5: n += fmtprint(f, "%*d", pad, v); break; + case 6: n += fmtprint(f, "%*s%09d", P(pad, 9), "", v); break; + default: goto badfmt; + } + break; + case 'z': + if(w != 1) + goto badfmt; + case 'Z': + sgn = (tm->tzoff < 0) ? '-' : '+'; + off = (tm->tzoff < 0) ? -tm->tzoff : tm->tzoff; + h = off/3600; + m = (off/60)%60; + if(w < 3 && pad < 5) + pad = 5; + switch(w){ + case 1: n += fmtprint(f, "%*s%c%02d%02d", pad-5, "", sgn, h, m); break; + case 2: n += fmtprint(f, "%*s%c%02d:%02d", pad-5, "", sgn, h, m); break; + case 3: n += fmtprint(f, "%*s", pad, tm->zone); break; + } + break; + case 'A': + case 'a': + if(w != 1) + goto badfmt; + if(c0 == 'a') + am = (tm->hour < 12) ? "am" : "pm"; + else + am = (tm->hour < 12) ? "AM" : "PM"; + n += fmtprint(f, "%*s", pad, am); + break; + case '[': + depth = 1; + while(*p){ + if(*p == '[') + depth++; + if(*p == ']') + depth--; + if(*p == '\\') + p++; + if(depth == 0) + break; + fmtrune(f, *p++); + } + if(*p++ != ']') + goto badfmt; + break; + default: + while(w-- > 0) + n += fmtrune(f, c0); + break; + } + } + return n; +badfmt: + werrstr("garbled format %s", tf.fmt); + return -1; +} + +static int +getnum(char **ps, int maxw, int *ok) +{ + char *s, *e; + int n; + + n = 0; + e = *ps + maxw; + for(s = *ps; s != e && *s >= '0' && *s <= '9'; s++){ + n *= 10; + n += *s - '0'; + } + *ok = s != *ps; + *ps = s; + return n; +} + +static int +lookup(char **s, char **tab, int len, int *ok) +{ + int nc, i; + + *ok = 0; + for(i = 0; *tab; tab++){ + nc = (len != -1) ? len : strlen(*tab); + if(cistrncmp(*s, *tab, nc) == 0){ + *s += nc; + *ok = 1; + return i; + } + i++; + } + *ok = 0; + return -1; +} + +Tm* +tmparse(Tm *tm, char *fmt, char *str, Tzone *tz, char **ep) +{ + int depth, n, w, c0, zs, z0, z1, md, ampm, zoned, sloppy, tzo, ok; + vlong abs; + char *s, *p, *q; + Tzone *zparsed; + Tzabbrev *a; + Tzoffpair *m; + + p = fmt; + s = str; + tzo = 0; + ampm = -1; + zoned = 0; + zparsed = nil; + sloppy = 0; + /* Default all fields */ + tmtime(tm, 0, nil); + if(*p == '~'){ + sloppy = 1; + p++; + } + while(*p){ + w = 1; + c0 = *p++; + if(c0 == '?'){ + w = -1; + c0 = *p++; + } + while(*p == c0){ + if(w != -1) w++; + p++; + } + ok = 1; + switch(c0){ + case 'Y': + switch(w){ + case -1: + tm->year = getnum(&s, 4, &ok); + if(tm->year > 100) tm->year -= 1900; + break; + case 1: tm->year = getnum(&s, 4, &ok) - 1900; break; + case 2: tm->year = getnum(&s, 2, &ok); break; + case 3: + case 4: tm->year = getnum(&s, 4, &ok) - 1900; break; + default: goto badfmt; + } + break; + case 'M': + switch(w){ + case -1: + tm->mon = getnum(&s, 2, &ok) - 1; + if(!ok) tm->mon = lookup(&s, month, -1, &ok); + if(!ok) tm->mon = lookup(&s, month, 3, &ok); + break; + case 1: + case 2: tm->mon = getnum(&s, 2, &ok) - 1; break; + case 3: tm->mon = lookup(&s, month, 3, &ok); break; + case 4: tm->mon = lookup(&s, month, -1, &ok); break; + default: goto badfmt; + } + break; + case 'D': + switch(w){ + case -1: + case 1: + case 2: tm->mday = getnum(&s, 2, &ok); break; + default: goto badfmt; + } + break; + case 'W': + switch(w){ + case -1: + tm->wday = lookup(&s, wday, -1, &ok); + if(!ok) tm->wday = lookup(&s, wday, 3, &ok); + if(!ok) tm->wday = getnum(&s, 1, &ok) - 1; + break; + case 1: tm->wday = getnum(&s, 1, &ok) - 1; break; + case 2: tm->wday = lookup(&s, wday, 3, &ok); break; + case 3: tm->wday = lookup(&s, wday, -1, &ok); break; + default: goto badfmt; + } + break; + case 'h': + switch(w){ + case -1: + case 1: + case 2: tm->hour = getnum(&s, 2, &ok); break; + default: goto badfmt; + } + break; + case 'm': + switch(w){ + case -1: + case 1: + case 2: tm->min = getnum(&s, 2, &ok); break; + default: goto badfmt; + } + break; + case 's': + switch(w){ + case -1: + case 1: + case 2: tm->sec = getnum(&s, 2, &ok); break; + default: goto badfmt; + } + break; + case 't': + switch(w){ + case -1: + case 1: + case 2: + case 3: tm->nsec += getnum(&s, 3, &ok)*1000000; break; + } + break; + case 'u': + switch(w){ + case -1: + case 1: + case 2: tm->nsec += getnum(&s, 3, &ok)*1000; break; + case 3: + case 4: tm->nsec += getnum(&s, 6, &ok)*1000; break; + } + break; + case 'n': + switch(w){ + case 1: + case 2: tm->nsec += getnum(&s, 3, &ok); break; + case 3: + case 4: tm->nsec += getnum(&s, 6, &ok); break; + case -1: + case 5: + case 6: tm->nsec += getnum(&s, 9, &ok); break; + } + break; + case 'z': + if(w != 1) + goto badfmt; + case 'Z': + zs = 0; + zoned = 1; + switch(*s++){ + case '+': zs = 1; break; + case '-': zs = -1; break; + default: s--; break; + } + q = s; + switch(w){ + case -1: + case 3: + for(a = tzabbrev; a->abbr; a++){ + n = strlen(a->abbr); + if(cistrncmp(s, a->abbr, n) == 0 && !isalpha(s[n])) + break; + } + if(a->abbr != nil){ + s += strlen(a->abbr); + zparsed = tzload(a->name); + if(zparsed == nil){ + werrstr("unloadable zone %s (%s)", a->abbr, a->name); + if(w != -1) + return nil; + } + goto Zoneparsed; + } + for(m = milabbrev; m->abbr != nil; m++){ + n = strlen(m->abbr); + if(cistrncmp(s, m->abbr, n) == 0 && !isalpha(s[n])) + break; + } + if(m->abbr != nil){ + snprint(tm->zone, sizeof(tm->zone), "%s", m->abbr); + tzo = m->off; + goto Zoneparsed; + } + if(w != -1) + break; + /* fall through */ + case 1: + /* offset: [+-]hhmm */ + z0 = getnum(&s, 4, &ok); + if(s - q == 4){ + z1 = z0 % 100; + if(z0/100 > 13 || z1 >= 60) + goto baddate; + tzo = zs*(3600*(z0/100) + 60*z1); + snprint(tm->zone, sizeof(tm->zone), "%c%02d%02d", zs<0?'-':'+', z0/100, z1); + goto Zoneparsed; + } + if(w != -1) + goto baddate; + /* fall through */ + case 2: + s = q; + /* offset: [+-]hh:mm */ + z0 = getnum(&s, 2, &ok); + if(*s++ != ':') + break; + z1 = getnum(&s, 2, &ok); + if(z1 > 60) + break; + tzo = zs*(3600*z0 + 60*z1); + snprint(tm->zone, sizeof(tm->zone), "%c%d02:%02d", zs<0?'-':'+', z0, z1); + goto Zoneparsed; + } + if(w != -1) + goto baddate; + /* + * Final fuzzy fallback: If we have what looks like an + * unknown timezone abbreviation, keep the zone name, + * but give it a timezone offset of 0. This allows us + * to avoid rejecting zones outside of RFC5322. + */ + for(s = q; *s; s++) + if(!isalpha(*s)) + break; + if(s - q >= 3 && !isalpha(*s)){ + strncpy(tm->zone, q, s - q); + tzo = 0; + ok = 1; + goto Zoneparsed; + } + goto baddate; +Zoneparsed: + break; + case 'A': + case 'a': + if(cistrncmp(s, "am", 2) == 0) + ampm = 0; + else if(cistrncmp(s, "pm", 2) == 0) + ampm = 1; + else + goto baddate; + s += 2; + break; + case '[': + depth = 1; + while(*p){ + if(*p == '[') + depth++; + if(*p == ']') + depth--; + if(*p == '\\') + p++; + if(depth == 0) + break; + if(*s == 0) + goto baddate; + if(*s++ != *p++) + goto baddate; + } + if(*p != ']') + goto badfmt; + p++; + break; + case '_': + case ',': + case ' ': + + if(*s != ' ' && *s != '\t' && *s != ',' && *s != '\n' && *s != '\0') + goto baddate; + p += strspn(p, " ,_\t\n"); + s += strspn(s, " ,\t\n"); + break; + default: + if(*s == 0) + goto baddate; + if(*s++ != c0) + goto baddate; + break; + } + if(!ok) + goto baddate; + } + if(*p != '\0') + goto baddate; + if(ep != nil) + *ep = s; + if(!sloppy && ampm != -1 && (tm->hour < 1 || tm->hour > 12)) + goto baddate; + if(ampm == 0 && tm->hour == 12) + tm->hour = 0; + else if(ampm == 1 && tm->hour < 12) + tm->hour += 12; + /* + * If we're allowing sloppy date ranges, + * we'll normalize out of range values. + */ + if(!sloppy){ + if(tm->yday < 0 || tm->yday > 365 + isleap(tm->year + 1900)) + goto baddate; + if(tm->wday < 0 || tm->wday > 6) + goto baddate; + if(tm->mon < 0 || tm->mon > 11) + goto baddate; + md = mdays[tm->mon]; + if(tm->mon == 1 && isleap(tm->year + 1900)) + md++; + if(tm->mday < 0 || tm->mday > md) + goto baddate; + if(tm->hour < 0 || tm->hour > 24) + goto baddate; + if(tm->min < 0 || tm->min > 59) + goto baddate; + if(tm->sec < 0 || tm->sec > 60) + goto baddate; + if(tm->nsec < 0 || tm->nsec > Nsec) + goto baddate; + if(tm->wday < 0 || tm->wday > 6) + goto baddate; + } + + /* + * Normalizing gives us the local time, + * but because we havnen't applied the + * timezone, we think we're GMT. So, we + * need to shift backwards. Then, we move + * the "GMT that was local" back to local + * time. + */ + abs = tmnorm(tm); + tm->tzoff = tzo; + if(!zoned) + tzoffset(tz, abs, tm); + else if(zparsed != nil){ + tzoffset(zparsed, abs, tm); + tzoffset(zparsed, abs + tm->tzoff, tm); + } + abs -= tm->tzoff; + if(tz != nil || !zoned) + tmtimens(tm, abs, tm->nsec, tz); + return tm; +baddate: + werrstr("invalid date %s", str); + return nil; +badfmt: + werrstr("garbled format %s near '%s'", fmt, p); + return nil; +} + +Tmfmt +tmfmt(Tm *d, char *fmt) +{ + return (Tmfmt){fmt, d}; +} + +void +tmfmtinstall(void) +{ + fmtinstall(L'τ', τconv); +} + +/* These legacy functions need access to τconv */ +static char* +dotmfmt(Fmt *f, ...) +{ + static char buf[30]; + va_list ap; + + va_start(ap, f); + f->runes = 0; + f->start = buf; + f->to = buf; + f->stop = buf + sizeof(buf) - 1; + f->flush = nil; + f->farg = nil; + f->nfmt = 0; + f->args = ap; + τconv(f); + va_end(ap); + buf[sizeof(buf) - 1] = 0; + return buf; +} + +char* +asctime(Tm* tm) +{ + Tmfmt tf; + Fmt f; + + tf = tmfmt(tm, "WW MMM _D hh:mm:ss ZZZ YYYY\n"); + return dotmfmt(&f, tf); +} + diff --git a/sys/src/libc/port/mkfile b/sys/src/libc/port/mkfile index 402990422..11aec6989 100644 --- a/sys/src/libc/port/mkfile +++ b/sys/src/libc/port/mkfile @@ -20,6 +20,7 @@ CFILES=\ cleanname.c\ crypt.c\ ctype.c\ + date.c\ encodefmt.c\ execl.c\ exits.c\