libc, seconds: new time and date apis (try 2)
Redo date handling in libc almost entirely. This allows handling dates and times from outside your timezones, fixes timezone loading in multithreaded applications, and allows parsing and formatting using custom format strings. As a test of the APIs, we replace the formatting code in seconds(1), shrinking it massively. The last commit missed a few removals, and made it unnecessarily hard to do an update.
This commit is contained in:
parent
f380851ddb
commit
8b3efcfc4e
7 changed files with 1207 additions and 900 deletions
|
@ -314,22 +314,45 @@ extern double fmod(double, double);
|
||||||
/*
|
/*
|
||||||
* Time-of-day
|
* Time-of-day
|
||||||
*/
|
*/
|
||||||
|
typedef struct Tzone Tzone;
|
||||||
|
#pragma incomplete Tzone
|
||||||
|
|
||||||
|
|
||||||
typedef
|
typedef
|
||||||
struct Tm
|
struct Tm
|
||||||
{
|
{
|
||||||
int sec;
|
vlong abs; /* seconds since Jan 1 1970, GMT */
|
||||||
int min;
|
int nsec; /* nseconds (range 0...1e9) */
|
||||||
int hour;
|
int sec; /* seconds (range 0..60) */
|
||||||
int mday;
|
int min; /* minutes (0..59) */
|
||||||
int mon;
|
int hour; /* hours (0..23) */
|
||||||
int year;
|
int mday; /* day of the month (1..31) */
|
||||||
int wday;
|
int mon; /* month of the year (0..11) */
|
||||||
int yday;
|
int year; /* year A.D. */
|
||||||
char zone[4];
|
int wday; /* day of week (0..6, Sunday = 0) */
|
||||||
int tzoff;
|
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;
|
} 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* gmtime(long);
|
||||||
extern Tm* localtime(long);
|
extern Tm* localtime(long);
|
||||||
extern char* asctime(Tm*);
|
extern char* asctime(Tm*);
|
||||||
|
|
246
sys/man/2/tmdate
Normal file
246
sys/man/2/tmdate
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
.TH TMDATE 2
|
||||||
|
.SH NAME
|
||||||
|
tmnow, tmgetzone, 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 {
|
||||||
|
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 has no way to express leap seconds.
|
||||||
|
.PP
|
||||||
|
We provide no way to manipulate timezones.
|
|
@ -1,237 +1,37 @@
|
||||||
|
#include <u.h>
|
||||||
|
#include <libc.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* seconds absolute_date ... - convert absolute_date to seconds since epoch
|
* seconds absolute_date ... - convert absolute_date to seconds since epoch
|
||||||
*/
|
*/
|
||||||
|
char *formats[] = {
|
||||||
#include <u.h>
|
/* asctime */
|
||||||
#include <libc.h>
|
"W MMM DD hh:mm:ss ?Z YYYY",
|
||||||
#include <ctype.h>
|
/* RFC5322 */
|
||||||
|
"?W ?DD ?MMM ?YYYY hh:mm:ss ?Z",
|
||||||
typedef ulong Time;
|
"?W, DD-?MM-YY hh:mm:ss ?Z",
|
||||||
|
/* RFC822/RFC2822 */
|
||||||
enum {
|
"DD MMM YY hh:mm ZZZ",
|
||||||
AM, PM, HR24,
|
"DD MMM YY hh:mm Z",
|
||||||
|
/* RFC850 */
|
||||||
/* token types */
|
"W, DD-MMM-YY hh:mm:ss MST",
|
||||||
Month = 1,
|
/* RFC1123 */
|
||||||
Year,
|
"WW, DD MMM YYYY hh:mm:ss ZZZ",
|
||||||
Day,
|
/* RFC1123Z */
|
||||||
Timetok,
|
"WW, DD MMM YYYY hh:mm:ss ZZ",
|
||||||
Tz,
|
/* RFC3339 */
|
||||||
Dtz,
|
"YYYY-MM-DD[T]hh:mm:ss[Z]ZZ",
|
||||||
Ignore,
|
"YYYY-MM-DD[T]hh:mm:ss[Z]Z",
|
||||||
Ampm,
|
"YYYY-MM-DD[T]hh:mm:ss ZZ",
|
||||||
|
"YYYY-MM-DD[T]hh:mm:ss Z",
|
||||||
Maxtok = 6, /* only this many chars are stored in datetktbl */
|
/* RFC 3339 and human-readable variants */
|
||||||
Maxdateflds = 25,
|
"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
|
static void
|
||||||
usage(void)
|
usage(void)
|
||||||
{
|
{
|
||||||
|
@ -242,225 +42,31 @@ usage(void)
|
||||||
void
|
void
|
||||||
main(int argc, char **argv)
|
main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
int i, sts;
|
Tm tm;
|
||||||
|
char **f, *fmt;
|
||||||
|
int i;
|
||||||
|
|
||||||
sts = 0;
|
fmt = nil;
|
||||||
ARGBEGIN{
|
ARGBEGIN{
|
||||||
|
case 'f':
|
||||||
|
fmt = EARGF(usage());
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
usage();
|
usage();
|
||||||
}ARGEND
|
}ARGEND;
|
||||||
if (argc == 0)
|
|
||||||
usage();
|
|
||||||
for (i = 0; i < argc; i++)
|
|
||||||
sts |= convert(argv[i]);
|
|
||||||
exits(sts != 0? "bad": 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
for(i = 0; i < argc; i++){
|
||||||
* Binary search -- from Knuth (6.2.1) Algorithm B. Special case like this
|
if(fmt != nil){
|
||||||
* is WAY faster than the generic bsearch().
|
if(tmparse(&tm, fmt, argv[i], nil) != nil)
|
||||||
*/
|
goto Found;
|
||||||
Datetok *
|
}else{
|
||||||
datebsearch(char *key, Datetok *base, unsigned nel)
|
for(f = formats; *f != nil; f++)
|
||||||
{
|
if(tmparse(&tm, *f, argv[i], nil) != nil)
|
||||||
int cmp;
|
goto Found;
|
||||||
Datetok *last = base + nel - 1, *pos;
|
|
||||||
|
|
||||||
while (last >= base) {
|
|
||||||
pos = base + ((last - base) >> 1);
|
|
||||||
cmp = key[0] - pos->token[0];
|
|
||||||
if (cmp == 0) {
|
|
||||||
cmp = strncmp(key, pos->token, Maxtok);
|
|
||||||
if (cmp == 0)
|
|
||||||
return pos;
|
|
||||||
}
|
}
|
||||||
if (cmp < 0)
|
sysfatal("tmparse: %r");
|
||||||
last = pos - 1;
|
Found:
|
||||||
else
|
print("%lld\n", tm.abs);
|
||||||
base = pos + 1;
|
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
|
|
|
@ -33,269 +33,34 @@
|
||||||
#include <u.h>
|
#include <u.h>
|
||||||
#include <libc.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*
|
Tm*
|
||||||
localtime(long tim)
|
localtime(long tim)
|
||||||
{
|
{
|
||||||
Tm *ct;
|
static Tm tm;
|
||||||
long t, *p;
|
Tzone *tz;
|
||||||
int dlflag;
|
|
||||||
|
/* No error checking: the API doesn't allow it. */
|
||||||
|
tz = tmgetzone("local");
|
||||||
|
tmtime(&tm, tim, tz);
|
||||||
|
return &tm;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Tm*
|
Tm*
|
||||||
gmtime(long tim)
|
gmtime(long abs)
|
||||||
{
|
{
|
||||||
int d0, d1;
|
static Tm tm;
|
||||||
long hms, day;
|
return tmtime(&tm, abs, nil);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
char*
|
char*
|
||||||
asctime(Tm *t)
|
ctime(long abs)
|
||||||
{
|
{
|
||||||
char *ncp;
|
Tzone *tz;
|
||||||
static char cbuf[30];
|
Tm tm;
|
||||||
|
|
||||||
strcpy(cbuf, "Thu Jan 01 00:00:00 GMT 1970\n");
|
/* No error checking: the API doesn't allow it. */
|
||||||
ncp = &"SunMonTueWedThuFriSat"[t->wday*3];
|
tz = tmgetzone("local");
|
||||||
cbuf[0] = *ncp++;
|
tmtime(&tm, abs, tz);
|
||||||
cbuf[1] = *ncp++;
|
return asctime(&tm);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,202 +1,9 @@
|
||||||
#include <u.h>
|
#include <u.h>
|
||||||
#include <libc.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
|
long
|
||||||
tm2sec(Tm *tm)
|
tm2sec(Tm *tm)
|
||||||
{
|
{
|
||||||
long secs, *p;
|
tmnorm(tm);
|
||||||
int i, yday, year, *d2m;
|
return tm->abs;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
859
sys/src/libc/port/date.c
Normal file
859
sys/src/libc/port/date.c
Normal file
|
@ -0,0 +1,859 @@
|
||||||
|
#include <u.h>
|
||||||
|
#include <libc.h>
|
||||||
|
|
||||||
|
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 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);
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ CFILES=\
|
||||||
cleanname.c\
|
cleanname.c\
|
||||||
crypt.c\
|
crypt.c\
|
||||||
ctype.c\
|
ctype.c\
|
||||||
|
date.c\
|
||||||
encodefmt.c\
|
encodefmt.c\
|
||||||
execl.c\
|
execl.c\
|
||||||
exits.c\
|
exits.c\
|
||||||
|
|
Loading…
Reference in a new issue