diff options
Diffstat (limited to 'date.c')
-rw-r--r-- | date.c | 437 |
1 files changed, 325 insertions, 112 deletions
@@ -6,7 +6,10 @@ #include "cache.h" -static time_t my_mktime(struct tm *tm) +/* + * This is like mktime, but without normalization of tm_wday and tm_yday. + */ +static time_t tm_to_time_t(const struct tm *tm) { static const int mdays[] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 @@ -21,6 +24,8 @@ static time_t my_mktime(struct tm *tm) return -1; if (month < 2 || (year + 2) % 4) day--; + if (tm->tm_hour < 0 || tm->tm_min < 0 || tm->tm_sec < 0) + return -1; return (year * 365 + (year + 1) / 4 + mdays[month] + day) * 24*60*60UL + tm->tm_hour * 60*60 + tm->tm_min * 60 + tm->tm_sec; } @@ -67,7 +72,7 @@ static int local_tzoffset(unsigned long time) t = time; localtime_r(&t, &tm); - t_local = my_mktime(&tm); + t_local = tm_to_time_t(&tm); if (t_local < t) { eastwest = -1; @@ -81,51 +86,83 @@ static int local_tzoffset(unsigned long time) return offset * eastwest; } +const char *show_date_relative(unsigned long time, int tz, + const struct timeval *now, + char *timebuf, + size_t timebuf_size) +{ + unsigned long diff; + if (now->tv_sec < time) + return "in the future"; + diff = now->tv_sec - time; + if (diff < 90) { + snprintf(timebuf, timebuf_size, "%lu seconds ago", diff); + return timebuf; + } + /* Turn it into minutes */ + diff = (diff + 30) / 60; + if (diff < 90) { + snprintf(timebuf, timebuf_size, "%lu minutes ago", diff); + return timebuf; + } + /* Turn it into hours */ + diff = (diff + 30) / 60; + if (diff < 36) { + snprintf(timebuf, timebuf_size, "%lu hours ago", diff); + return timebuf; + } + /* We deal with number of days from here on */ + diff = (diff + 12) / 24; + if (diff < 14) { + snprintf(timebuf, timebuf_size, "%lu days ago", diff); + return timebuf; + } + /* Say weeks for the past 10 weeks or so */ + if (diff < 70) { + snprintf(timebuf, timebuf_size, "%lu weeks ago", (diff + 3) / 7); + return timebuf; + } + /* Say months for the past 12 months or so */ + if (diff < 365) { + snprintf(timebuf, timebuf_size, "%lu months ago", (diff + 15) / 30); + return timebuf; + } + /* Give years and months for 5 years or so */ + if (diff < 1825) { + unsigned long totalmonths = (diff * 12 * 2 + 365) / (365 * 2); + unsigned long years = totalmonths / 12; + unsigned long months = totalmonths % 12; + int n; + n = snprintf(timebuf, timebuf_size, "%lu year%s", + years, (years > 1 ? "s" : "")); + if (months) + snprintf(timebuf + n, timebuf_size - n, + ", %lu month%s ago", + months, (months > 1 ? "s" : "")); + else + snprintf(timebuf + n, timebuf_size - n, " ago"); + return timebuf; + } + /* Otherwise, just years. Centuries is probably overkill. */ + snprintf(timebuf, timebuf_size, "%lu years ago", (diff + 183) / 365); + return timebuf; +} + const char *show_date(unsigned long time, int tz, enum date_mode mode) { struct tm *tm; static char timebuf[200]; + if (mode == DATE_RAW) { + snprintf(timebuf, sizeof(timebuf), "%lu %+05d", time, tz); + return timebuf; + } + if (mode == DATE_RELATIVE) { - unsigned long diff; struct timeval now; gettimeofday(&now, NULL); - if (now.tv_sec < time) - return "in the future"; - diff = now.tv_sec - time; - if (diff < 90) { - snprintf(timebuf, sizeof(timebuf), "%lu seconds ago", diff); - return timebuf; - } - /* Turn it into minutes */ - diff = (diff + 30) / 60; - if (diff < 90) { - snprintf(timebuf, sizeof(timebuf), "%lu minutes ago", diff); - return timebuf; - } - /* Turn it into hours */ - diff = (diff + 30) / 60; - if (diff < 36) { - snprintf(timebuf, sizeof(timebuf), "%lu hours ago", diff); - return timebuf; - } - /* We deal with number of days from here on */ - diff = (diff + 12) / 24; - if (diff < 14) { - snprintf(timebuf, sizeof(timebuf), "%lu days ago", diff); - return timebuf; - } - /* Say weeks for the past 10 weeks or so */ - if (diff < 70) { - snprintf(timebuf, sizeof(timebuf), "%lu weeks ago", (diff + 3) / 7); - return timebuf; - } - /* Say months for the past 12 months or so */ - if (diff < 360) { - snprintf(timebuf, sizeof(timebuf), "%lu months ago", (diff + 15) / 30); - return timebuf; - } - /* Else fall back on absolute format.. */ + return show_date_relative(time, tz, &now, + timebuf, sizeof(timebuf)); } if (mode == DATE_LOCAL) @@ -193,6 +230,7 @@ static const struct { { "GMT", 0, 0, }, /* Greenwich Mean */ { "UTC", 0, 0, }, /* Universal (Coordinated) */ + { "Z", 0, 0, }, /* Zulu, alias for UTC */ { "WET", 0, 0, }, /* Western European */ { "BST", 0, 1, }, /* British Summer */ @@ -269,7 +307,7 @@ static int match_alpha(const char *date, struct tm *tm, int *offset) for (i = 0; i < ARRAY_SIZE(timezone_names); i++) { int match = match_string(date, timezone_names[i].name); - if (match >= 3) { + if (match >= 3 || match == strlen(timezone_names[i].name)) { int off = timezone_names[i].offset; /* This is bogus, but we like summer */ @@ -322,7 +360,7 @@ static int is_date(int year, int month, int day, struct tm *now_tm, time_t now, if (!now_tm) return 1; - specified = my_mktime(r); + specified = tm_to_time_t(r); /* Be it commit time or author time, it does not make * sense to specify timestamp way into the future. Make @@ -400,6 +438,21 @@ static int match_multi_number(unsigned long num, char c, const char *date, char } /* + * Have we filled in any part of the time/date yet? + * We just do a binary 'and' to see if the sign bit + * is set in all the values. + */ +static inline int nodate(struct tm *tm) +{ + return (tm->tm_year & + tm->tm_mon & + tm->tm_mday & + tm->tm_hour & + tm->tm_min & + tm->tm_sec) < 0; +} + +/* * We've seen a digit. Time? Year? Date? */ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt) @@ -415,7 +468,7 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt * more than 8 digits. This is because we don't want to rule out * numbers like 20070606 as a YYYYMMDD date. */ - if (num >= 100000000) { + if (num >= 100000000 && nodate(tm)) { time_t time = num; if (gmtime_r(&time, tm)) { *tm_gmt = 1; @@ -460,6 +513,13 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt } /* + * Ignore lots of numerals. We took care of 4-digit years above. + * Days or months must be one or two digits. + */ + if (n > 2) + return n; + + /* * NOTE! We will give precedence to day-of-month over month or * year numbers in the 1-12 range. So 05 is always "mday 5", * unless we already have a mday.. @@ -483,15 +543,8 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt } } - if (num > 0 && num < 32) { - tm->tm_mday = num; - } else if (num > 1900) { - tm->tm_year = num - 1900; - } else if (num > 70) { - tm->tm_year = num; - } else if (num > 0 && num < 13) { + if (num > 0 && num < 13 && tm->tm_mon < 0) tm->tm_mon = num-1; - } return n; } @@ -499,23 +552,35 @@ static int match_digit(const char *date, struct tm *tm, int *offset, int *tm_gmt static int match_tz(const char *date, int *offp) { char *end; - int offset = strtoul(date+1, &end, 10); - int min, hour; - int n = end - date - 1; + int hour = strtoul(date + 1, &end, 10); + int n = end - (date + 1); + int min = 0; - min = offset % 100; - hour = offset / 100; + if (n == 4) { + /* hhmm */ + min = hour % 100; + hour = hour / 100; + } else if (n != 2) { + min = 99; /* random crap */ + } else if (*end == ':') { + /* hh:mm? */ + min = strtoul(end + 1, &end, 10); + if (end - (date + 1) != 5) + min = 99; /* random crap */ + } /* otherwise we parsed "hh" */ /* - * Don't accept any random crap.. At least 3 digits, and - * a valid minute. We might want to check that the minutes - * are divisible by 30 or something too. + * Don't accept any random crap. Even though some places have + * offset larger than 12 hours (e.g. Pacific/Kiritimati is at + * UTC+14), there is something wrong if hour part is much + * larger than that. We might also want to check that the + * minutes are divisible by 15 or something too. (Offset of + * Kathmandu, Nepal is UTC+5:45) */ - if (min < 60 && n > 2) { - offset = hour*60+min; + if (min < 60 && hour < 24) { + int offset = hour * 60 + min; if (*date == '-') offset = -offset; - *offp = offset; } return end - date; @@ -532,22 +597,61 @@ static int date_string(unsigned long date, int offset, char *buf, int len) return snprintf(buf, len, "%lu %c%02d%02d", date, sign, offset / 60, offset % 60); } +/* + * Parse a string like "0 +0000" as ancient timestamp near epoch, but + * only when it appears not as part of any other string. + */ +static int match_object_header_date(const char *date, unsigned long *timestamp, int *offset) +{ + char *end; + unsigned long stamp; + int ofs; + + if (*date < '0' || '9' <= *date) + return -1; + stamp = strtoul(date, &end, 10); + if (*end != ' ' || stamp == ULONG_MAX || (end[1] != '+' && end[1] != '-')) + return -1; + date = end + 2; + ofs = strtol(date, &end, 10); + if ((*end != '\0' && (*end != '\n')) || end != date + 4) + return -1; + ofs = (ofs / 100) * 60 + (ofs % 100); + if (date[-1] == '-') + ofs = -ofs; + *timestamp = stamp; + *offset = ofs; + return 0; +} + /* Gr. strptime is crap for this; it doesn't have a way to require RFC2822 (i.e. English) day/month names, and it doesn't work correctly with %z. */ -int parse_date(const char *date, char *result, int maxlen) +int parse_date_basic(const char *date, unsigned long *timestamp, int *offset) { struct tm tm; - int offset, tm_gmt; - time_t then; + int tm_gmt; + unsigned long dummy_timestamp; + int dummy_offset; + + if (!timestamp) + timestamp = &dummy_timestamp; + if (!offset) + offset = &dummy_offset; memset(&tm, 0, sizeof(tm)); tm.tm_year = -1; tm.tm_mon = -1; tm.tm_mday = -1; tm.tm_isdst = -1; - offset = -1; + tm.tm_hour = -1; + tm.tm_min = -1; + tm.tm_sec = -1; + *offset = -1; tm_gmt = 0; + if (*date == '@' && + !match_object_header_date(date + 1, timestamp, offset)) + return 0; /* success */ for (;;) { int match = 0; unsigned char c = *date; @@ -557,11 +661,11 @@ int parse_date(const char *date, char *result, int maxlen) break; if (isalpha(c)) - match = match_alpha(date, &tm, &offset); + match = match_alpha(date, &tm, offset); else if (isdigit(c)) - match = match_digit(date, &tm, &offset, &tm_gmt); + match = match_digit(date, &tm, offset, &tm_gmt); else if ((c == '-' || c == '+') && isdigit(date[1])) - match = match_tz(date, &offset); + match = match_tz(date, offset); if (!match) { /* BAD CRAP */ @@ -572,16 +676,25 @@ int parse_date(const char *date, char *result, int maxlen) } /* mktime uses local timezone */ - then = my_mktime(&tm); - if (offset == -1) - offset = (then - mktime(&tm)) / 60; + *timestamp = tm_to_time_t(&tm); + if (*offset == -1) + *offset = ((time_t)*timestamp - mktime(&tm)) / 60; - if (then == -1) + if (*timestamp == -1) return -1; if (!tm_gmt) - then -= offset * 60; - return date_string(then, offset, result, maxlen); + *timestamp -= *offset * 60; + return 0; /* success */ +} + +int parse_date(const char *date, char *result, int maxlen) +{ + unsigned long timestamp; + int offset; + if (parse_date_basic(date, ×tamp, &offset)) + return -1; + return date_string(timestamp, offset, result, maxlen); } enum date_mode parse_date_format(const char *format) @@ -600,6 +713,8 @@ enum date_mode parse_date_format(const char *format) return DATE_LOCAL; else if (!strcmp(format, "default")) return DATE_NORMAL; + else if (!strcmp(format, "raw")) + return DATE_RAW; else die("unknown date format %s", format); } @@ -611,48 +726,70 @@ void datestamp(char *buf, int bufsize) time(&now); - offset = my_mktime(localtime(&now)) - now; + offset = tm_to_time_t(localtime(&now)) - now; offset /= 60; date_string(now, offset, buf, bufsize); } -static void update_tm(struct tm *tm, unsigned long sec) +/* + * Relative time update (eg "2 days ago"). If we haven't set the time + * yet, we need to set it from current time. + */ +static unsigned long update_tm(struct tm *tm, struct tm *now, unsigned long sec) { - time_t n = mktime(tm) - sec; + time_t n; + + if (tm->tm_mday < 0) + tm->tm_mday = now->tm_mday; + if (tm->tm_mon < 0) + tm->tm_mon = now->tm_mon; + if (tm->tm_year < 0) { + tm->tm_year = now->tm_year; + if (tm->tm_mon > now->tm_mon) + tm->tm_year--; + } + + n = mktime(tm) - sec; localtime_r(&n, tm); + return n; +} + +static void date_now(struct tm *tm, struct tm *now, int *num) +{ + update_tm(tm, now, 0); } -static void date_yesterday(struct tm *tm, int *num) +static void date_yesterday(struct tm *tm, struct tm *now, int *num) { - update_tm(tm, 24*60*60); + update_tm(tm, now, 24*60*60); } -static void date_time(struct tm *tm, int hour) +static void date_time(struct tm *tm, struct tm *now, int hour) { if (tm->tm_hour < hour) - date_yesterday(tm, NULL); + date_yesterday(tm, now, NULL); tm->tm_hour = hour; tm->tm_min = 0; tm->tm_sec = 0; } -static void date_midnight(struct tm *tm, int *num) +static void date_midnight(struct tm *tm, struct tm *now, int *num) { - date_time(tm, 0); + date_time(tm, now, 0); } -static void date_noon(struct tm *tm, int *num) +static void date_noon(struct tm *tm, struct tm *now, int *num) { - date_time(tm, 12); + date_time(tm, now, 12); } -static void date_tea(struct tm *tm, int *num) +static void date_tea(struct tm *tm, struct tm *now, int *num) { - date_time(tm, 17); + date_time(tm, now, 17); } -static void date_pm(struct tm *tm, int *num) +static void date_pm(struct tm *tm, struct tm *now, int *num) { int hour, n = *num; *num = 0; @@ -666,7 +803,7 @@ static void date_pm(struct tm *tm, int *num) tm->tm_hour = (hour % 12) + 12; } -static void date_am(struct tm *tm, int *num) +static void date_am(struct tm *tm, struct tm *now, int *num) { int hour, n = *num; *num = 0; @@ -680,7 +817,7 @@ static void date_am(struct tm *tm, int *num) tm->tm_hour = (hour % 12); } -static void date_never(struct tm *tm, int *num) +static void date_never(struct tm *tm, struct tm *now, int *num) { time_t n = 0; localtime_r(&n, tm); @@ -688,7 +825,7 @@ static void date_never(struct tm *tm, int *num) static const struct special { const char *name; - void (*fn)(struct tm *, int *); + void (*fn)(struct tm *, struct tm *, int *); } special[] = { { "yesterday", date_yesterday }, { "noon", date_noon }, @@ -697,6 +834,7 @@ static const struct special { { "PM", date_pm }, { "AM", date_am }, { "never", date_never }, + { "now", date_now }, { NULL } }; @@ -717,7 +855,7 @@ static const struct typelen { { NULL } }; -static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) +static const char *approxidate_alpha(const char *date, struct tm *tm, struct tm *now, int *num, int *touched) { const struct typelen *tl; const struct special *s; @@ -731,6 +869,7 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) int match = match_string(date, month_names[i]); if (match >= 3) { tm->tm_mon = i; + *touched = 1; return end; } } @@ -738,7 +877,8 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) for (s = special; s->name; s++) { int len = strlen(s->name); if (match_string(date, s->name) == len) { - s->fn(tm, num); + s->fn(tm, now, num); + *touched = 1; return end; } } @@ -748,11 +888,14 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) int len = strlen(number_name[i]); if (match_string(date, number_name[i]) == len) { *num = i; + *touched = 1; return end; } } - if (match_string(date, "last") == 4) + if (match_string(date, "last") == 4) { *num = 1; + *touched = 1; + } return end; } @@ -760,8 +903,9 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) while (tl->type) { int len = strlen(tl->type); if (match_string(date, tl->type) >= len-1) { - update_tm(tm, tl->length * *num); + update_tm(tm, now, tl->length * *num); *num = 0; + *touched = 1; return end; } tl++; @@ -778,25 +922,31 @@ static const char *approxidate_alpha(const char *date, struct tm *tm, int *num) n++; diff += 7*n; - update_tm(tm, diff * 24 * 60 * 60); + update_tm(tm, now, diff * 24 * 60 * 60); + *touched = 1; return end; } } if (match_string(date, "months") >= 5) { - int n = tm->tm_mon - *num; + int n; + update_tm(tm, now, 0); /* fill in date fields if needed */ + n = tm->tm_mon - *num; *num = 0; while (n < 0) { n += 12; tm->tm_year--; } tm->tm_mon = n; + *touched = 1; return end; } if (match_string(date, "years") >= 4) { + update_tm(tm, now, 0); /* fill in date fields if needed */ tm->tm_year -= *num; *num = 0; + *touched = 1; return end; } @@ -820,38 +970,101 @@ static const char *approxidate_digit(const char *date, struct tm *tm, int *num) } } - *num = number; + /* Accept zero-padding only for small numbers ("Dec 02", never "Dec 0002") */ + if (date[0] != '0' || end - date <= 2) + *num = number; return end; } -unsigned long approxidate(const char *date) +/* + * Do we have a pending number at the end, or when + * we see a new one? Let's assume it's a month day, + * as in "Dec 6, 1992" + */ +static void pending_number(struct tm *tm, int *num) +{ + int number = *num; + + if (number) { + *num = 0; + if (tm->tm_mday < 0 && number < 32) + tm->tm_mday = number; + else if (tm->tm_mon < 0 && number < 13) + tm->tm_mon = number-1; + else if (tm->tm_year < 0) { + if (number > 1969 && number < 2100) + tm->tm_year = number - 1900; + else if (number > 69 && number < 100) + tm->tm_year = number; + else if (number < 38) + tm->tm_year = 100 + number; + /* We screw up for number = 00 ? */ + } + } +} + +static unsigned long approxidate_str(const char *date, + const struct timeval *tv, + int *error_ret) { int number = 0; + int touched = 0; struct tm tm, now; - struct timeval tv; - char buffer[50]; + time_t time_sec; - if (parse_date(date, buffer, sizeof(buffer)) > 0) - return strtoul(buffer, NULL, 10); - - gettimeofday(&tv, NULL); - localtime_r(&tv.tv_sec, &tm); + time_sec = tv->tv_sec; + localtime_r(&time_sec, &tm); now = tm; + + tm.tm_year = -1; + tm.tm_mon = -1; + tm.tm_mday = -1; + for (;;) { unsigned char c = *date; if (!c) break; date++; if (isdigit(c)) { + pending_number(&tm, &number); date = approxidate_digit(date-1, &tm, &number); + touched = 1; continue; } if (isalpha(c)) - date = approxidate_alpha(date-1, &tm, &number); + date = approxidate_alpha(date-1, &tm, &now, &number, &touched); + } + pending_number(&tm, &number); + if (!touched) + *error_ret = 1; + return update_tm(&tm, &now, 0); +} + +unsigned long approxidate_relative(const char *date, const struct timeval *tv) +{ + unsigned long timestamp; + int offset; + int errors = 0; + + if (!parse_date_basic(date, ×tamp, &offset)) + return timestamp; + return approxidate_str(date, tv, &errors); +} + +unsigned long approxidate_careful(const char *date, int *error_ret) +{ + struct timeval tv; + unsigned long timestamp; + int offset; + int dummy = 0; + if (!error_ret) + error_ret = &dummy; + + if (!parse_date_basic(date, ×tamp, &offset)) { + *error_ret = 0; + return timestamp; } - if (number > 0 && number < 32) - tm.tm_mday = number; - if (tm.tm_mon > now.tm_mon && tm.tm_year == now.tm_year) - tm.tm_year--; - return mktime(&tm); + + gettimeofday(&tv, NULL); + return approxidate_str(date, &tv, error_ret); } |