code: plan9front

ref: 61c6a24d350a09f4a45b31b97ab91af6aea47958
dir: /sys/src/libc/port/date.c/

View raw version
#include <u.h>
#include <libc.h>
#include <ctype.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;
};

/* 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));
	f = open(path, OREAD|OCEXEC);
	if(f < 0)
		return -1;
	r = read(f, buf, sizeof(buf));
	close(f);
	if(r < 0 || r >= sizeof(buf))
		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;
	}
	abs += tz->stdiff;
	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 - 1;
	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;
		/* Ignore '?' so we can share parse and format strings */
		case '?':
			continue;
		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 'o':
			if(w != 1) goto badfmt;
			switch(tm->mday){
			case 1: case 21: case 31: n += fmtprint(f, "st");		break;
			case 2: case 22: n += fmtprint(f, "nd");			break;
			case 3: case 23: n += fmtprint(f, "rd");			break;
			default: n += fmtprint(f, "th");
			}
			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, *local;
	Tzabbrev *a;
	Tzoffpair *m;

	p = fmt;
	s = str;
	tzo = 0;
	ampm = -1;
	zoned = 0;
	zparsed = nil;
	sloppy = 0;
	c0 = -1;
	/* Default all fields */
	tmtime(tm, 0, nil);
	if(*p == '~'){
		sloppy = 1;
		p++;
	}

	/* Skip whitespace */
	for(;; s++) {
		switch(*s) {
		case ' ':
		case '\t':
		case '\n':
		case '\f':
		case '\r':
		case '\v':
			continue;
		}
		break;
	}
	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 'o':
			if(strncmp(s, "th", 2) == 0
			|| strncmp(s, "rd", 2) == 0
			|| strncmp(s, "nd", 2) == 0
			|| strncmp(s, "st", 2) == 0)
				s += 2;
			else
				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:
				/*
				 * Ugly Hack:
				 * Ctime is defined as printing a 3-character timezone
				 * name. The timezone name is ambiguous. For example,
				 * EST refers to both Australian and American eastern
				 * time. On top of that, we don't want to make the
				 * tzabbrev table exhaustive. So, we put in this hack:
				 *
				 * Before we consult the well known table of timezones,
				 * we check if the local time matches the timezone name.
				 *
				 * If you want unambiguous timezone parsing, use numeric
				 * timezone offsets (Z, ZZ formats).
				 */
				if((local = tzload("local")) != nil){
					if(cistrncmp(s, local->stname, strlen(local->stname)) == 0){
						s += strlen(local->stname);
						zparsed = local;
						goto Zoneparsed;
					}
					if(cistrncmp(s, local->dlname, strlen(local->dlname)) == 0){
						s += strlen(local->dlname);
						zparsed = local;
						goto Zoneparsed;
					}
				}
				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 %c component in '%s'", c0, 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);
}