ref: 56e869ac7004a635a7d63596ee751275484f28c3
parent: 3ba1d83d2026ebac616ab17a2126df97c0a7a24c
author: Ori Bernstein <[email protected]>
date: Sun Aug 9 14:58:44 EDT 2020
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.
--- a/sys/include/libc.h
+++ b/sys/include/libc.h
@@ -314,21 +314,43 @@
/*
* 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);
--- /dev/null
+++ b/sys/man/2/tmdate
@@ -1,0 +1,275 @@
+.TH TMDATE 2
+.SH NAME
+tmnow, tzload, tmtime, tmparse, tmfmt, tmnorm - convert date and time
+.SH SYNOPSIS
+.B #include <u.h>
+.br
+.B #include <libc.h>
+.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.
--- 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 <u.h>
#include <libc.h>
-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<TZSIZE; i++) {
- if(rd_long(&p, &timezone.dlpairs[i]))
- goto error;
- if(timezone.dlpairs[i] == 0)
- return;
- }
-
-error:
- timezone.stdiff = 0;
- strcpy(timezone.stname, "GMT");
- timezone.dlpairs[0] = 0;
-}
-
-static
-rd_name(char **f, char *p)
-{
- int c, i;
-
- for(;;) {
- c = *(*f)++;
- if(c != ' ' && c != '\n')
- break;
- }
- for(i=0; i<3; i++) {
- if(c == ' ' || c == '\n')
- return 1;
- *p++ = c;
- c = *(*f)++;
- }
- if(c != ' ' && c != '\n')
- return 1;
- *p = 0;
- return 0;
-}
-
-static
-rd_long(char **f, long *p)
-{
- int c, s;
- long l;
-
- s = 0;
- for(;;) {
- c = *(*f)++;
- if(c == '-') {
- s++;
- continue;
- }
- 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')
- 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);
}
--- a/sys/src/libc/9sys/tm2sec.c
+++ b/sys/src/libc/9sys/tm2sec.c
@@ -1,202 +1,11 @@
#include <u.h>
#include <libc.h>
-#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; i<tm->mon; 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<TZSIZE; i++) {
- if(rd_long(&p, &timezone.dlpairs[i]))
- goto error;
- if(timezone.dlpairs[i] == 0)
- return;
- }
-
-error:
- timezone.stdiff = 0;
- strcpy(timezone.stname, "GMT");
- timezone.dlpairs[0] = 0;
-}
-
-static int
-rd_name(char **f, char *p)
-{
- int c, i;
-
- for(;;) {
- c = *(*f)++;
- if(c != ' ' && c != '\n')
- break;
- }
- for(i=0; i<3; i++) {
- if(c == ' ' || c == '\n')
- return 1;
- *p++ = c;
- c = *(*f)++;
- }
- if(c != ' ' && c != '\n')
- return 1;
- *p = 0;
- return 0;
-}
-
-static int
-rd_long(char **f, long *p)
-{
- int c, s;
- long l;
-
- s = 0;
- for(;;) {
- c = *(*f)++;
- if(c == '-') {
- s++;
- continue;
- }
- 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')
- return 1;
- l = l*10 + c-'0';
- c = *(*f)++;
- }
- if(s)
- l = -l;
- *p = l;
- return 0;
+ tt = *tm;
+ return tmnorm(&tt);
}
--- /dev/null
+++ b/sys/src/libc/port/date.c
@@ -1,0 +1,996 @@
+#include <u.h>
+#include <libc.h>
+
+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);
+}
+
--- a/sys/src/libc/port/mkfile
+++ b/sys/src/libc/port/mkfile
@@ -20,6 +20,7 @@
cleanname.c\
crypt.c\
ctype.c\
+ date.c\
encodefmt.c\
execl.c\
exits.c\