Back before 1.0 we had a date parser written in Yacc that could parse all sorts of fancy strings such as "yesterday", "last month", or "two fortnights ago". This was dropped in r848401/r848402 because of maintenance concerns. The parser was missing some desired features and also did a lot more than needed. E.g. it could even parse future dates such as "tomorrow" or "next week" which makes no sense at all since future commits don't exist yet :) See http://svn.haxx.se/dev/archive-2003-12/0737.shtml (linked from issue #408) for details.
It has been pointed out to me that Subversion does not support such date specifications at all. I think they're rather useful because they allow users to easily get at commits that happened e.g. during the last N days or weeks relative to the local time of their machine without having to calculate appropriate date strings. The patch below adds support for simple word strings for specifying dates (see log message for details). There are a couple of restrictions: To keep things simple years always have 365 days and months always have 30 days. If more precision is desired normal dates can be used. Stephen Butler convinced me to take this route. I do not intend to grow the set of strings any further unless new strings open up very exciting possibilities. Localising the strings to other languages would also complicate things too much since the order of words might change (also pointed out by Steve). All command names and long options are always in English anyway. If the system clock on the client side is wrong revisions might not be resolved correctly. Combined with the new --diff option of svn log this makes it very easy to review commits which happened during e.g. the last 3 days: svn log --diff -r {"3 days ago"}:{"now"} ^/trunk | less I am posting this prior to commit because I have received mixed reactions to this idea. Like myself, Greg was surprised that we don't already support this. C-Mike said that this might open the door for potential maintenance problems to reappear in particular if we keep adding more strings. I don't think we will have problems if we don't go too far with adding complexity to this feature. But let's continue discussion based on this patch. Any comments or objections? [[[ Add support for the following revision { DATE } specifications: "now" resolves to the most recent revision as of the current time. "yesterday" resolves to the most recent revision prior to 00:00h of today. "N years|months|weeks|days|hours|minutes ago" resolve to the most recent revision prior to the specified time. For years, months, weeks, and days, round up to 00:00h of the day following the specified time. A year always has 365 days. A month always has 30 days. N may be a word from "zero" up to "twelve", or a non-negative digit. To help scripts, N=0 is allowed and produces the same result as "now", and if N=1 the final 's' of the unit name is allowed, but not required. * subversion/libsvn_subr/date.c (unit_words_table, number_words_table, words_match): New. (svn_parse_date): Parse new date specifications. ]]] Index: subversion/libsvn_subr/date.c =================================================================== --- subversion/libsvn_subr/date.c (revision 1103298) +++ subversion/libsvn_subr/date.c (working copy) @@ -22,6 +22,7 @@ #include "svn_time.h" #include "svn_error.h" +#include "svn_string.h" #include "svn_private_config.h" @@ -187,6 +188,146 @@ template_match(apr_time_exp_t *expt, svn_boolean_t return TRUE; } +static struct unit_words_table { + const char *word; + apr_time_t value; +} unit_words_table[] = { + /* Word matching does not concern itself with exact days of the month + * or leap years so these amounts are always fixed. */ + { "years", apr_time_from_sec(60 * 60 * 24 * 365) }, + { "months", apr_time_from_sec(60 * 60 * 24 * 30) }, + { "weeks", apr_time_from_sec(60 * 60 * 24 * 7) }, + { "days", apr_time_from_sec(60 * 60 * 24) }, + { "hours", apr_time_from_sec(60 * 60) }, + { "minutes", apr_time_from_sec(60) }, + { "mins", apr_time_from_sec(60) }, + { NULL , 0 } +}; + +static struct number_words_table { + const char *word; + int number; +} number_words_table[] = { + { "zero", 0 }, { "one", 1 }, { "two", 2 }, { "three", 3 }, { "four", 4 }, + { "five", 5 }, { "six", 6 }, { "seven", 7 }, { "eight", 8 }, { "nine", 9 }, + { "ten", 10 }, { "eleven", 11 }, { "twelve", 12 }, { NULL, 0 } +}; + +/* Attempt to match the date-string in TEXT according to the following rules: + * + * "now" resolves to the most recent revision as of the current time NOW. + * "yesterday" resolves to the most recent revision prior to 00:00h of today. + * "N years|months|weeks|days|hours|minutes ago" resolve to the most recent + * revision prior to the specified time. For years, months, weeks, and days, + * round up to 00:00h of the day following the specified time. + * N may either be a word from NUMBER_WORDS_TABLE defined above, or a + * non-negative digit. + * + * Return TRUE on successful match, FALSE otherwise. On successful match, + * fill in *EXP with the matched value and set *LOCALTZ to TRUE (this + * function always uses local time). Use POOL for temporary allocations. */ +static svn_boolean_t +words_match(apr_time_exp_t *expt, svn_boolean_t *localtz, + apr_time_t now, const char *text, apr_pool_t *pool) +{ + apr_time_t t = -1; + const char *word; + svn_boolean_t round_to_next_day = TRUE; + apr_array_header_t *words; + + words = svn_cstring_split(text, " ", TRUE /* chop_whitespace */, pool); + + if (words->nelts == 0) + return FALSE; + + word = APR_ARRAY_IDX(words, 0, const char *); + + if (words->nelts == 1) + { + if (!strcmp(word, "now")) + { + t = now; + round_to_next_day = FALSE; + } + else if (!strcmp(word, "yesterday")) + t = now - apr_time_from_sec(60 * 60 * 24); + } + else if (words->nelts == 3) + { + int i; + int n = -1; + const char *number_str; + const char *unit_str; + + /* Try to parse a number word. */ + for (i = 0, number_str = number_words_table[i].word; + number_str = number_words_table[i].word, number_str != NULL; i++) + { + if (!strcmp(word, number_str)) + { + n = number_words_table[i].number; + break; + } + } + + if (n < 0) + { + svn_error_t *err; + + /* Try to parse a digit. */ + err = svn_cstring_atoi(&n, word); + if (err) + { + svn_error_clear(err); + return FALSE; + } + if (n < 0) + return FALSE; + } + + /* Try to parse a unit. */ + word = APR_ARRAY_IDX(words, 1, const char *); + for (i = 0, unit_str = unit_words_table[i].word; + unit_str = unit_words_table[i].word, unit_str != NULL; i++) + { + /* Tolerate missing trailing 's' from unit for n=1. */ + if (!strcmp(word, unit_str) || + (n == 1 && !strncmp(word, unit_str, strlen(unit_str) - 1))) + { + t = now - (n * unit_words_table[i].value); + round_to_next_day = !(!strcmp(unit_str, "hours") || + !strcmp(unit_str, "minutes") || + !strcmp(unit_str, "mins")); + break; + } + } + if (t < 0) + return FALSE; + + /* Require trailing "ago". */ + word = APR_ARRAY_IDX(words, 2, const char *); + if (strcmp(word, "ago")) + return FALSE; + } + else + return FALSE; + + if (t >= 0) + { + if (apr_time_exp_lt(expt, t) != APR_SUCCESS) + return FALSE; + if (round_to_next_day) + { + expt->tm_usec = expt->tm_sec = expt->tm_min = expt->tm_hour = 0; + expt->tm_mday++; + } + *localtz = TRUE; + return TRUE; + } + + return FALSE; +} + static int valid_days_by_month[] = { 31, 29, 31, 30, @@ -244,7 +385,7 @@ svn_parse_date(svn_boolean_t *matched, apr_time_t expt.tm_mon = expnow.tm_mon; expt.tm_mday = expnow.tm_mday; } - else + else if (!words_match(&expt, &localtz, now, text, pool)) return SVN_NO_ERROR; /* Range validation, allowing for leap seconds */