diff --git a/sys/include/libc.h b/sys/include/libc.h index 7a2700539..820d684b2 100644 --- a/sys/include/libc.h +++ b/sys/include/libc.h @@ -314,22 +314,45 @@ 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; + vlong abs; /* seconds since Jan 1 1970, GMT */ + 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* tmgetzone(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*); +extern Tm* 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..127a4e186 --- /dev/null +++ b/sys/man/2/tmdate @@ -0,0 +1,244 @@ +.TH TMDATE 2 +.SH NAME +tmnow, tmgetzone, 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 { + vlong abs; /* seconds since Jan 1 1970, UTC */ + 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 *tmgetzone(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); +void 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. +Times are represented as an absolute instant in time, combined with a time zone. +.PP +Time zones are loaded by as 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 +Tmgetzone 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 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 +The day of week in 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 z, Z, ZZ +The timezone in named, [+-]HHMM and [+-]HH:MM 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. +For example, +.I __h +will format to a width of 3. +.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,RFC-3339 and RFC-2822 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 recal- +culates the absolute time from the +.I year, mon, mday, hr, min +and +.I sec +fields. Other fields are ignored. +This recalculation respects the time zone stored in struct tm. +Out of range values are wrapped. For example, December 32nd +becomes January 1st. + +.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 = tmgetzone("local") == nil) + sysfatal("load zone: %r"); +if((zp = tmgetzone("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. + +.IP +.EX +Tm a, b; + +tmparse(&a, nil, "Tue Dec 10 12:36:00 PST 2019"); +tmparse(&b, nil, "Tue Dec 10 15:36:00 EST 2019"); +if(a.abs == b.abs) + 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 = tmgetzone("local")) == nil) + sysfatal("load zone: %r"); +if((zp = tmgetzone("US_Pacific")) == nil) + sysfatal("load zone: %r"); +if(tmnow(&here, zl) == nil) + sysfatal("get time: %r"); +if(tmtime(&there, here.abs, zp) == nil) + sysfatal("shift time: %r"); +.EE + +.PP +Add a day to two times. Because we picked daylight savings +time to adjust over, only 23 hours are added. + +.EX +Tm t; +tmparse(&t, "W MMM D hh:mm:ss z YYYY, "Sun Nov 2 13:11:11 PST 2019"); +tm.day++; +tmrecalc(&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 can't express leap seconds. diff --git a/sys/src/cmd/seconds.c b/sys/src/cmd/seconds.c index 69e243301..2c5fc3333 100644 --- a/sys/src/cmd/seconds.c +++ b/sys/src/cmd/seconds.c @@ -1,237 +1,37 @@ +#include +#include + /* * seconds absolute_date ... - convert absolute_date to seconds since epoch */ - -#include -#include -#include - -typedef ulong Time; - -enum { - AM, PM, HR24, - - /* token types */ - Month = 1, - Year, - Day, - Timetok, - Tz, - Dtz, - Ignore, - Ampm, - - Maxtok = 6, /* only this many chars are stored in datetktbl */ - Maxdateflds = 25, +char *formats[] = { + /* asctime */ + "W MMM DD hh:mm:ss ?Z YYYY", + /* RFC5322 */ + "?W ?DD ?MMM ?YYYY hh:mm:ss ?Z", + "?W, DD-?MM-YY hh:mm:ss ?Z", + /* RFC822/RFC8222 */ + "DD MMM YY hh:mm ZZZ", + "DD MMM YY hh:mm Z", + /* RFC850 */ + "W, DD-MMM-YY hh:mm:ss MST", + /* RFC1123 */ + "WW, DD MMM YYYY hh:mm:ss ZZZ", + /* RFC1123Z */ + "WW, DD MMM YYYY hh:mm:ss ZZ", + /* RFC3339 */ + "YYYY-MM-DD[T]hh:mm:ss[Z]ZZ", + "YYYY-MM-DD[T]hh:mm:ss[Z]Z", + "YYYY-MM-DD[T]hh:mm:ss ZZ", + "YYYY-MM-DD[T]hh:mm:ss Z", + /* RFC 3339 and human-readable variants */ + "YYYY-MM-DD hh:mm:ss", + "YYYY-MM-DD hh:mm:ss ?Z", + "YYYY-MM-DD [@] hh:mm:ss", + "YYYY-MM-DD [@] hh:mm:ss ?Z", + nil }; -/* - * macros for squeezing values into low 7 bits of "value". - * all timezones we care about are divisible by 10, and the largest value - * (780) when divided is 78. - */ -#define TOVAL(tp, v) ((tp)->value = (v) / 10) -#define FROMVAL(tp) ((tp)->value * 10) /* uncompress */ - -/* keep this struct small since we have an array of them */ -typedef struct { - char token[Maxtok]; - char type; - schar value; -} Datetok; - -int dtok_numparsed; - -/* forwards */ -Datetok *datetoktype(char *s, int *bigvalp); - -static Datetok datetktbl[]; -static unsigned szdatetktbl; - -/* parse 1- or 2-digit number, advance *cpp past it */ -static int -eatnum(char **cpp) -{ - int c, x; - char *cp; - - cp = *cpp; - c = *cp; - if (!isascii(c) || !isdigit(c)) - return -1; - x = c - '0'; - - c = *++cp; - if (isascii(c) && isdigit(c)) { - x = 10*x + c - '0'; - cp++; - } - *cpp = cp; - return x; -} - -/* return -1 on failure */ -int -parsetime(char *time, Tm *tm) -{ - tm->hour = eatnum(&time); - if (tm->hour == -1 || *time++ != ':') - return -1; /* only hour; too short */ - - tm->min = eatnum(&time); - if (tm->min == -1) - return -1; - if (*time++ != ':') { - tm->sec = 0; - return 0; /* no seconds; okay */ - } - - tm->sec = eatnum(&time); - if (tm->sec == -1) - return -1; - - /* this may be considered too strict. garbage at end of time? */ - return *time == '\0' || isascii(*time) && isspace(*time)? 0: -1; -} - -/* - * try to parse pre-split timestr in fields as an absolute date - */ -int -tryabsdate(char **fields, int nf, Tm *now, Tm *tm) -{ - int i, mer = HR24, bigval = -1; - long flg = 0, ty; - Datetok *tp; - - now = localtime(time(0)); /* default to local time (zone) */ - tm->tzoff = now->tzoff; - strncpy(tm->zone, now->zone, sizeof tm->zone); - - tm->mday = tm->mon = tm->year = -1; /* mandatory */ - tm->hour = tm->min = tm->sec = 0; - dtok_numparsed = 0; - - for (i = 0; i < nf; i++) { - if (fields[i][0] == '\0') - continue; - tp = datetoktype(fields[i], &bigval); - ty = (1L << tp->type) & ~(1L << Ignore); - if (flg & ty) - return -1; /* repeated type */ - flg |= ty; - switch (tp->type) { - case Year: - tm->year = bigval; - if (tm->year < 1970 || tm->year > 2106) - return -1; /* can't represent in ulong */ - /* convert 4-digit year to 1900 origin */ - if (tm->year >= 1900) - tm->year -= 1900; - break; - case Day: - tm->mday = bigval; - break; - case Month: - tm->mon = tp->value - 1; /* convert to zero-origin */ - break; - case Timetok: - if (parsetime(fields[i], tm) < 0) - return -1; - break; - case Dtz: - case Tz: - /* tm2sec mangles timezones, so we do our own handling */ - tm->tzoff = FROMVAL(tp); - snprint(tm->zone, sizeof(tm->zone), "GMT"); - break; - case Ignore: - break; - case Ampm: - mer = tp->value; - break; - default: - return -1; /* bad token type: CANTHAPPEN */ - } - } - if (tm->year == -1 || tm->mon == -1 || tm->mday == -1) - return -1; /* missing component */ - if (mer == PM) - tm->hour += 12; - return 0; -} - -int -prsabsdate(char *timestr, Tm *now, Tm *tm) -{ - int nf; - char *fields[Maxdateflds]; - static char delims[] = "- \t\n/,"; - - nf = gettokens(timestr, fields, nelem(fields), delims+1); - if (nf > nelem(fields)) - return -1; - if (tryabsdate(fields, nf, now, tm) < 0) { - char *p = timestr; - - /* - * could be a DEC-date; glue it all back together, split it - * with dash as a delimiter and try again. Yes, this is a - * hack, but so are DEC-dates. - */ - while (--nf > 0) { - while (*p++ != '\0') - ; - p[-1] = ' '; - } - nf = gettokens(timestr, fields, nelem(fields), delims); - if (nf > nelem(fields) || tryabsdate(fields, nf, now, tm) < 0) - return -1; - } - return 0; -} - -int -validtm(Tm *tm) -{ - if (tm->year < 0 || tm->mon < 0 || tm->mon > 11 || - tm->mday < 1 || tm->hour < 0 || tm->hour >= 24 || - tm->min < 0 || tm->min > 59 || - tm->sec < 0 || tm->sec > 61) /* allow 2 leap seconds */ - return 0; - return 1; -} - -Time -seconds(char *timestr) -{ - Tm date; - - memset(&date, 0, sizeof date); - if (prsabsdate(timestr, localtime(time(0)), &date) < 0) - return -1; - return validtm(&date)? tm2sec(&date) - 60*date.tzoff: -1; -} - -int -convert(char *timestr) -{ - char *copy; - Time tstime; - - copy = strdup(timestr); - if (copy == nil) - sysfatal("out of memory"); - tstime = seconds(copy); - free(copy); - if (tstime == -1) { - fprint(2, "%s: `%s' not a valid date\n", argv0, timestr); - return 1; - } - print("%lud\n", tstime); - return 0; -} - static void usage(void) { @@ -242,225 +42,31 @@ usage(void) void main(int argc, char **argv) { - int i, sts; + Tm tm; + char **f, *fmt; + int i; - sts = 0; + fmt = nil; ARGBEGIN{ + case 'f': + fmt = EARGF(usage()); + break; default: usage(); - }ARGEND - if (argc == 0) - usage(); - for (i = 0; i < argc; i++) - sts |= convert(argv[i]); - exits(sts != 0? "bad": 0); -} + }ARGEND; -/* - * Binary search -- from Knuth (6.2.1) Algorithm B. Special case like this - * is WAY faster than the generic bsearch(). - */ -Datetok * -datebsearch(char *key, Datetok *base, unsigned nel) -{ - int cmp; - Datetok *last = base + nel - 1, *pos; - - while (last >= base) { - pos = base + ((last - base) >> 1); - cmp = key[0] - pos->token[0]; - if (cmp == 0) { - cmp = strncmp(key, pos->token, Maxtok); - if (cmp == 0) - return pos; + for(i = 0; i < argc; i++){ + if(fmt != nil){ + if(tmparse(&tm, fmt, argv[i], nil) != nil) + goto Found; + }else{ + for(f = formats; *f != nil; f++) + if(tmparse(&tm, *f, argv[i], nil) != nil) + goto Found; } - if (cmp < 0) - last = pos - 1; - else - base = pos + 1; + sysfatal("tmparse: %r"); +Found: + print("%lld\n", tm.abs); } - return 0; + exits(nil); } - -Datetok * -datetoktype(char *s, int *bigvalp) -{ - char *cp = s; - char c = *cp; - static Datetok t; - Datetok *tp = &t; - - if (isascii(c) && isdigit(c)) { - int len = strlen(cp); - - if (len > 3 && (cp[1] == ':' || cp[2] == ':')) - tp->type = Timetok; - else { - if (bigvalp != nil) - *bigvalp = atoi(cp); /* won't fit in tp->value */ - if (len == 4) - tp->type = Year; - else if (++dtok_numparsed == 1) - tp->type = Day; - else - tp->type = Year; - } - } else if (c == '-' || c == '+') { - int val = atoi(cp + 1); - int hr = val / 100; - int min = val % 100; - - val = hr*60 + min; - TOVAL(tp, c == '-'? -val: val); - tp->type = Tz; - } else { - char lowtoken[Maxtok+1]; - char *ltp = lowtoken, *endltp = lowtoken+Maxtok; - - /* copy to lowtoken to avoid modifying s */ - while ((c = *cp++) != '\0' && ltp < endltp) - *ltp++ = (isascii(c) && isupper(c)? tolower(c): c); - *ltp = '\0'; - tp = datebsearch(lowtoken, datetktbl, szdatetktbl); - if (tp == nil) { - tp = &t; - tp->type = Ignore; - } - } - return tp; -} - - -/* - * to keep this table reasonably small, we divide the lexval for Tz and Dtz - * entries by 10 and truncate the text field at MAXTOKLEN characters. - * the text field is not guaranteed to be NUL-terminated. - */ -static Datetok datetktbl[] = { -/* text token lexval */ - "acsst", Dtz, 63, /* Cent. Australia */ - "acst", Tz, 57, /* Cent. Australia */ - "adt", Dtz, -18, /* Atlantic Daylight Time */ - "aesst", Dtz, 66, /* E. Australia */ - "aest", Tz, 60, /* Australia Eastern Std Time */ - "ahst", Tz, 60, /* Alaska-Hawaii Std Time */ - "am", Ampm, AM, - "apr", Month, 4, - "april", Month, 4, - "ast", Tz, -24, /* Atlantic Std Time (Canada) */ - "at", Ignore, 0, /* "at" (throwaway) */ - "aug", Month, 8, - "august", Month, 8, - "awsst", Dtz, 54, /* W. Australia */ - "awst", Tz, 48, /* W. Australia */ - "bst", Tz, 6, /* British Summer Time */ - "bt", Tz, 18, /* Baghdad Time */ - "cadt", Dtz, 63, /* Central Australian DST */ - "cast", Tz, 57, /* Central Australian ST */ - "cat", Tz, -60, /* Central Alaska Time */ - "cct", Tz, 48, /* China Coast */ - "cdt", Dtz, -30, /* Central Daylight Time */ - "cet", Tz, 6, /* Central European Time */ - "cetdst", Dtz, 12, /* Central European Dayl.Time */ - "cst", Tz, -36, /* Central Standard Time */ - "dec", Month, 12, - "decemb", Month, 12, - "dnt", Tz, 6, /* Dansk Normal Tid */ - "dst", Ignore, 0, - "east", Tz, -60, /* East Australian Std Time */ - "edt", Dtz, -24, /* Eastern Daylight Time */ - "eet", Tz, 12, /* East. Europe, USSR Zone 1 */ - "eetdst", Dtz, 18, /* Eastern Europe */ - "est", Tz, -30, /* Eastern Standard Time */ - "feb", Month, 2, - "februa", Month, 2, - "fri", Ignore, 5, - "friday", Ignore, 5, - "fst", Tz, 6, /* French Summer Time */ - "fwt", Dtz, 12, /* French Winter Time */ - "gmt", Tz, 0, /* Greenwish Mean Time */ - "gst", Tz, 60, /* Guam Std Time, USSR Zone 9 */ - "hdt", Dtz, -54, /* Hawaii/Alaska */ - "hmt", Dtz, 18, /* Hellas ? ? */ - "hst", Tz, -60, /* Hawaii Std Time */ - "idle", Tz, 72, /* Intl. Date Line, East */ - "idlw", Tz, -72, /* Intl. Date Line, West */ - "ist", Tz, 12, /* Israel */ - "it", Tz, 22, /* Iran Time */ - "jan", Month, 1, - "januar", Month, 1, - "jst", Tz, 54, /* Japan Std Time,USSR Zone 8 */ - "jt", Tz, 45, /* Java Time */ - "jul", Month, 7, - "july", Month, 7, - "jun", Month, 6, - "june", Month, 6, - "kst", Tz, 54, /* Korea Standard Time */ - "ligt", Tz, 60, /* From Melbourne, Australia */ - "mar", Month, 3, - "march", Month, 3, - "may", Month, 5, - "mdt", Dtz, -36, /* Mountain Daylight Time */ - "mest", Dtz, 12, /* Middle Europe Summer Time */ - "met", Tz, 6, /* Middle Europe Time */ - "metdst", Dtz, 12, /* Middle Europe Daylight Time*/ - "mewt", Tz, 6, /* Middle Europe Winter Time */ - "mez", Tz, 6, /* Middle Europe Zone */ - "mon", Ignore, 1, - "monday", Ignore, 1, - "mst", Tz, -42, /* Mountain Standard Time */ - "mt", Tz, 51, /* Moluccas Time */ - "ndt", Dtz, -15, /* Nfld. Daylight Time */ - "nft", Tz, -21, /* Newfoundland Standard Time */ - "nor", Tz, 6, /* Norway Standard Time */ - "nov", Month, 11, - "novemb", Month, 11, - "nst", Tz, -21, /* Nfld. Standard Time */ - "nt", Tz, -66, /* Nome Time */ - "nzdt", Dtz, 78, /* New Zealand Daylight Time */ - "nzst", Tz, 72, /* New Zealand Standard Time */ - "nzt", Tz, 72, /* New Zealand Time */ - "oct", Month, 10, - "octobe", Month, 10, - "on", Ignore, 0, /* "on" (throwaway) */ - "pdt", Dtz, -42, /* Pacific Daylight Time */ - "pm", Ampm, PM, - "pst", Tz, -48, /* Pacific Standard Time */ - "sadt", Dtz, 63, /* S. Australian Dayl. Time */ - "sast", Tz, 57, /* South Australian Std Time */ - "sat", Ignore, 6, - "saturd", Ignore, 6, - "sep", Month, 9, - "sept", Month, 9, - "septem", Month, 9, - "set", Tz, -6, /* Seychelles Time ?? */ - "sst", Dtz, 12, /* Swedish Summer Time */ - "sun", Ignore, 0, - "sunday", Ignore, 0, - "swt", Tz, 6, /* Swedish Winter Time */ - "thu", Ignore, 4, - "thur", Ignore, 4, - "thurs", Ignore, 4, - "thursd", Ignore, 4, - "tue", Ignore, 2, - "tues", Ignore, 2, - "tuesda", Ignore, 2, - "ut", Tz, 0, - "utc", Tz, 0, - "wadt", Dtz, 48, /* West Australian DST */ - "wast", Tz, 42, /* West Australian Std Time */ - "wat", Tz, -6, /* West Africa Time */ - "wdt", Dtz, 54, /* West Australian DST */ - "wed", Ignore, 3, - "wednes", Ignore, 3, - "weds", Ignore, 3, - "wet", Tz, 0, /* Western Europe */ - "wetdst", Dtz, 6, /* Western Europe */ - "wst", Tz, 48, /* West Australian Std Time */ - "ydt", Dtz, -48, /* Yukon Daylight Time */ - "yst", Tz, -54, /* Yukon Standard Time */ - "zp4", Tz, -24, /* GMT +4 hours. */ - "zp5", Tz, -30, /* GMT +5 hours. */ - "zp6", Tz, -36, /* GMT +6 hours. */ -}; -static unsigned szdatetktbl = nelem(datetktbl); diff --git a/sys/src/libc/9sys/mkfile b/sys/src/libc/9sys/mkfile index 686a7bd49..2f33a149a 100644 --- a/sys/src/libc/9sys/mkfile +++ b/sys/src/libc/9sys/mkfile @@ -10,7 +10,6 @@ OFILES=\ convM2S.$O\ convS2M.$O\ cputime.$O\ - ctime.$O\ dial.$O\ dirfstat.$O\ dirfwstat.$O\ @@ -47,7 +46,6 @@ OFILES=\ sysname.$O\ time.$O\ times.$O\ - tm2sec.$O\ truerand.$O\ wait.$O\ waitpid.$O\ diff --git a/sys/src/libc/port/date.c b/sys/src/libc/port/date.c new file mode 100644 index 000000000..3f06c18cf --- /dev/null +++ b/sys/src/libc/port/date.c @@ -0,0 +1,872 @@ +#include +#include + +typedef struct Tzabbrev Tzabbrev; +typedef struct Tzoffpair Tzoffpair; + +#define Ctimefmt "W MMM _D hh:mm:ss ZZZ YYYY\n" +enum { + Tzsize = 150, + Nsec = 1000*1000*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[16]; + char stname[4]; + char dlname[4]; + long stdiff; + long dldiff; + long dlpairs[Tzsize]; +}; + +static char tfmt[128]; +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 int +isleap(int y) +{ + return y % 4 == 0 && (y % 100 != 0 || y % 400 == 0); +} + +static int +rdname(char **f, char *p) +{ + int c, i; + + while((c = *(*f)++) != 0) + 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 +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') + 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; + + 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; + p = buf; + if(rdname(&p, tz->stname)) + return -1; + if(rdlong(&p, &tz->stdiff)) + return -1; + if(rdname(&p, tz->dlname)) + return -1; + if(rdlong(&p, &tz->dldiff)) + return -1; + for(i=0; i < Tzsize; i++) { + if(rdlong(&p, &tz->dlpairs[i])) + return -1; + if(tz->dlpairs[i] == 0) + return 0; + } + return -1; +} + +Tzone* +tmgetzone(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 +getzoneoff(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; + 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; + + tm->abs = abs; + zrel = abs + tm->tzoff; + t = zrel % Daysec; + e = zrel / Daysec; + if(t < 0){ + t += Daysec; + e -= 1; + } + + t += nsec/Nsec; + tm->sec = t % 60; + t /= 60; + tm->min = t % 60; + t /= 60; + tm->hour = t; + tm->wday = (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 && y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) + tm->yday++; + tm->year = y - 1900; + tm->mon = m - 1; + tm->mday = d; + tm->nsec = 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; + getzoneoff(tz, abs, tm); + return tmfill(tm, abs, ns); +} + +Tm* +tmnow(Tm *tm, Tzone *tz) +{ + vlong ns; + + ns = nsec(); + return tmtimens(tm, nsec()/Nsec, ns%Nsec, tz); +} + +Tm* +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 - 1901; + } + 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; + abs -= tm->tzoff; + return tmfill(tm, abs, tm->nsec); +} + +static int +τconv(Fmt *f) +{ + int depth, n, 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, "%*.3s", pad, wday[tm->wday]); break; + case 2: 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 '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: + 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) +{ + int depth, w, c0, zs, z0, z1, ampm, zoned, sloppy, tzo, ok; + 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); + break; + case 1: tm->wday = lookup(&s, wday, 3, &ok); break; + case 2: 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 '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; + } + switch(w){ + case -1: + case 3: + for(a = tzabbrev; a->abbr; a++) + if(strncmp(s, a->abbr, strlen(a->abbr)) == 0) + break; + if(a->abbr != nil){ + s += strlen(a->abbr); + zparsed = tmgetzone(a->name); + if(zparsed == nil){ + werrstr("unloadable zone %s (%s)", a->abbr, a->name); + return nil; + } + break; + } + for(m = milabbrev; m->abbr != nil; m++) + if(strncmp(s, m->abbr, strlen(m->abbr)) == 0) + break; + if(m->abbr != nil){ + snprint(tm->zone, sizeof(tm->zone), "%s", m->abbr); + tzo = m->off; + break; + } + /* fall through */ + case 1: + /* offset: [+-]hhmm */ + q = s; + 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); + break; + } + if(w != -1) + goto baddate; + s = q; + /* fall through */ + case 2: + /* offset: [+-]hh:mm */ + z0 = getnum(&s, 2, &ok); + if(*s++ != ':') + goto baddate; + z1 = getnum(&s, 2, &ok); + if(z1 > 60) + goto baddate; + tzo = zs*(3600*z0 + 60*z1); + snprint(tm->zone, sizeof(tm->zone), "%c%d02:%02d", zs<0?'-':'+', z0, z1); + break; + } + 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; + 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 '\t': + if(*s != ' ' && *s != '\t' && *s != ',') + goto baddate; + while(*p == ' ' || *p == '\t' || *p == ',') + p++; + while(*s == ' ' || *s == '\t' || *s == ',') + s++; + break; + default: + if(*s == 0) + goto baddate; + if(*s++ != c0) + goto baddate; + break; + } + if(!ok) + goto baddate; + } + + if(!sloppy && ampm != -1 && tm->hour > 12) + goto baddate; + if(ampm == 1) + 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; + if(tm->mday < 0 || tm->mday > mdays[tm->mon]) + 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; + } + + /* + * 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. + */ + tmnorm(tm); + tm->tzoff = tzo; + if(!zoned) + getzoneoff(tz, tm->abs, tm); + else if(zparsed != nil) + getzoneoff(zparsed, tm->abs, tm); + tm->abs -= tm->tzoff; + if(tz != nil || !zoned) + tmtime(tm, tm->abs, tz); + return tm; +baddate: + werrstr("invalid date %s near '%s'", str, s); + 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, nil); + return dotmfmt(&f, tf); +} + +char* +ctime(long abs) +{ + Tzone *tz; + Tm tm; + + /* No error checking: the API doesn't allow it. */ + tz = tmgetzone("local"); + tmtime(&tm, abs, tz); + return asctime(&tm); +} diff --git a/sys/src/libc/port/gmtime.c b/sys/src/libc/port/gmtime.c new file mode 100644 index 000000000..c13234cce --- /dev/null +++ b/sys/src/libc/port/gmtime.c @@ -0,0 +1,27 @@ +#include +#include + +Tm* +gmtime(long abs) +{ + static Tm tm; + return tmtime(&tm, abs, nil); +} + +Tm* +localtime(long abs) +{ + Tzone *tz; + static Tm tm; + + /* No error checking: the API doesn't allow it. */ + tz = tmgetzone("local"); + return tmtime(&tm, abs, tz); +} + +long +tm2sec(Tm *tm) +{ + tmnorm(tm); + return tm->abs; +} diff --git a/sys/src/libc/port/mkfile b/sys/src/libc/port/mkfile index 402990422..8c9eb0574 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\ @@ -32,6 +33,7 @@ CFILES=\ getcallerpc.c\ getfields.c\ getuser.c\ + gmtime.c\ hangup.c\ hypot.c\ lnrand.c\