>From f347d3b3790fb548c7f1cad0275077f8ed45dd9d Mon Sep 17 00:00:00 2001 From: Assaf Gordon Date: Thu, 25 Jul 2019 04:32:36 -0600 Subject: [PATCH 2/2] date: add --arith-format=FORMAT option Follow-up to --date-format=FORMAT, allow the date string to contain date/time adjustment values. Example: 2019-07-26 18:49:59, +49 hours, -10 minutes, -30 seconds: $ date --date-format '%Y%m%d %H%M%S' \ --arith-format '%H %M %S' \ --date '20190726 184959 49 -10 -30' \ '+%F %T' 2019-07-28 19:39:29 TODO: coreutils.texi, NEWS, usage * src/date.c (long_options): Add ARITH_FORMAT. (dbg_printf, dbg_print_tm): New functions. (strptime_deltas): New function, similar to strptime but limited to %Y/m/d/H/M/S specifiers, and accepts negative values. (parse_datetime_string): Change flow to parse date-string for adjustment values, and adjust resulting date/time accordingly. (main): Handle ARITH_FORMAT argument. * tests/misc/date-strp.pl: Add tests. --- src/date.c | 257 +++++++++++++++++++++++++++++++++++++++++++++--- tests/misc/date-strp.pl | 28 ++++++ 2 files changed, 274 insertions(+), 11 deletions(-) diff --git a/src/date.c b/src/date.c index 4879474e3..944476fa7 100644 --- a/src/date.c +++ b/src/date.c @@ -33,6 +33,7 @@ #include "quote.h" #include "stat-time.h" #include "fprintftime.h" +#include "strftime.h" /* The official name of this program (e.g., no 'g' prefix). */ #define PROGRAM_NAME "date" @@ -81,7 +82,8 @@ enum { RFC_3339_OPTION = CHAR_MAX + 1, DEBUG_DATE_PARSING, - STRP_FORMAT + STRP_FORMAT, + ARITH_FORMAT }; static char const short_options[] = "d:f:I::r:Rs:u"; @@ -99,6 +101,7 @@ static struct option const long_options[] = {"rfc-3339", required_argument, NULL, RFC_3339_OPTION}, {"set", required_argument, NULL, 's'}, {"date-format", required_argument, NULL, STRP_FORMAT}, + {"arith-format", required_argument, NULL, ARITH_FORMAT}, {"uct", no_argument, NULL, 'u'}, {"utc", no_argument, NULL, 'u'}, {"universal", no_argument, NULL, 'u'}, @@ -112,6 +115,10 @@ static bool debug ; /* the strp format string specified by the user */ static char* strp_format; +/* the strp-like format string specified by the user + for date arithmetic */ +static char* arith_format; + #if LOCALTIME_CACHE # define TZSET tzset () @@ -289,36 +296,226 @@ Show the local time for 9AM next Friday on the west coast of the US\n\ exit (status); } +static void _GL_ATTRIBUTE_FORMAT ((__printf__, 1, 2)) +dbg_printf (char const *msg, ...) +{ + va_list args; + fputs ("date: ", stderr); + + va_start (args, msg); + vfprintf (stderr, msg, args); + va_end (args); + + fputc ('\n', stderr); +} + + +static char* +strptime_deltas (const char* str, const char* fmt, + struct tm* /*output*/ tm) +{ + int val; + int negate; + + while (*fmt != '\0') + { + /* A white space in the format string matches 0 more or white + space in the input string. */ + if (isspace (*fmt)) + { + while (isspace (*str)) + ++str; + ++fmt; + continue; + } + + /* Any character but '%' must be matched by the same character + in the iput string. */ + if (*fmt != '%') + { + if (*fmt != *str) + { + if (debug) + dbg_printf (_("date string does not match arithmetic format" \ + ", expecting '%c' got '%c'"), *fmt, *str); + return NULL; + } + ++fmt; + ++str; + continue; + } + + ++fmt; + if (*fmt == '%') + { + /* Match the '%' character itself. */ + if (*str != '%') + { + if (debug) + dbg_printf (_("date string does not match arithmetic format" \ + ", expecting '%c' got '%c'"), *fmt, *str); + return NULL; + } + ++str; + continue; + } + + /* Parse an integer value from STR. + Equivalent to (and copied from) gnulib's strptime get_number. + Since all expected values are numeric, extract them here, once, + instead of using a macro. */ + val = 0; + negate = 0; + while (*str == ' ') + ++str; + if (*str == '+') + ++str; + if (*str == '-') + { + negate = 1; + ++str; + } + if (*str < '0' || *str > '9') + { + if (debug) + dbg_printf (_("invalid digit '%c'"), *str); + return NULL; + } + do { + val *= 10; + val += *str++ - '0'; + } while (*str >= '0' && *str <= '9'); + if (negate) + val = -val; + + + /* Where to store the value (year/month/day/etc). */ + switch (*fmt) + { + case 'H': + tm->tm_hour = val; + break; + + case 'M': + tm->tm_min = val; + break; + + case 'S': + tm->tm_sec = val; + break; + + case 'Y': + tm->tm_year = val; + break; + + case 'm': + tm->tm_mon = val; + break; + + case 'd': + tm->tm_mday = val; + break; + + default: + if (debug) + dbg_printf (_("invalid date-arithmetic specifier '%c'"), *fmt); + return NULL; + } + ++fmt; + } + + return (char*)str; +} + + +static void +dbg_print_tm (const char *prefix, const struct tm* tm, timezone_t tz) +{ + char buf[40]; + nstrftime (buf, sizeof (buf), "%Y-%m-%d %H:%M:%S TZ=%z", tm, tz, 0); + dbg_printf ("%s%s", prefix, buf); +} + + /* A wrapper calling either gnulib's parse_datetime2() or strptime(3), depending on whether the user specified --date-format=FORMAT argument. */ static bool parse_datetime_string (struct timespec *result, char const *datestr, timezone_t tzdefault, char const *tzstring) { - if (strp_format) + if (strp_format || arith_format) { struct tm t; + struct tm delta_t; + const char *endp = datestr; + bool adj_date = false, adj_time = false; + time_t s = time (NULL); localtime_rz (tzdefault, &s, &t); - char *endp = strptime (datestr, strp_format, &t ); - if (!endp) + memset (&delta_t, 0, sizeof (delta_t)); + + if (debug) + dbg_print_tm (_("current date/time: "), &t, tzdefault); + + if (strp_format) { + endp = strptime (endp, strp_format, &t ); + if (!endp) + { + if (debug) + dbg_printf (_("date string %s does not match format '%s'"), + quote (datestr), + strp_format); + return false; + } + if (debug) - error (0, 0, _("date string %s does not match format '%s'"), - quotearg (datestr), - strp_format); - return false; + dbg_print_tm (_("parsed date/time: "), &t, tzdefault); + } + + + if (arith_format) + { + endp = strptime_deltas (endp, arith_format, &delta_t); + + /* strptime_deltas handles dbg_printfs, so just return */ + if (!endp) + return false; + + adj_date = delta_t.tm_year || delta_t.tm_mon || delta_t.tm_mday ; + adj_time = delta_t.tm_hour || delta_t.tm_min || delta_t.tm_sec ; } if (*endp) { if (debug) - error (EXIT_FAILURE, 0, _("extraneous characters in date " \ - "string: %s"), - quotearg (endp)); + dbg_printf (_("extraneous characters in date string: %s"), + quote (endp)); return false; } + if (adj_date) + { + if (debug) + { + dbg_printf (_("parsed date adjustment:")); + if (delta_t.tm_year) + dbg_printf (_("%5d year(s)"), delta_t.tm_year); + if (delta_t.tm_mon) + dbg_printf (_("%5d month(s)"), delta_t.tm_mon); + if (delta_t.tm_mday) + dbg_printf (_("%5d day(s)"), delta_t.tm_mday); + } + + /* Date arithmetics */ + t.tm_year += delta_t.tm_year; + t.tm_mon += delta_t.tm_mon; + t.tm_mday += delta_t.tm_mday; + + if (debug) + dbg_print_tm (_("adjusted date: "), &t, tzdefault); + } + s = mktime (&t); if (s == (time_t)-1) { @@ -326,7 +523,42 @@ parse_datetime_string (struct timespec *result, char const *datestr, return false; } + if (debug) + dbg_printf (_("seconds since epoch: %"PRIdMAX), (intmax_t)s); + + + if (adj_time) + { + if (debug) + { + dbg_printf (_("parsed time adjustment:")); + if (delta_t.tm_hour) + dbg_printf (_("%5d hour(s)"), delta_t.tm_hour); + if (delta_t.tm_min) + dbg_printf (_("%5d minute(s)"), delta_t.tm_min); + if (delta_t.tm_sec) + dbg_printf (_("%5d second(s)"), delta_t.tm_sec); + } + + /* Time arithmetics */ + /* TODO: avoid overflows, + see gnulib's parse_datetime.y line 2290 + using INT_ADD_WRAPV/INT_MULTIPLI_WRAPV */ + s += delta_t.tm_hour * 60 * 60 + delta_t.tm_min * 60 + delta_t.tm_sec; + + if (debug) + dbg_printf (_("seconds since epoch (after time adjustment): " \ + "%"PRIdMAX), (intmax_t)s); + } + *result = make_timespec (s, 0); + + if (debug) + { + localtime_rz (tzdefault, &s, &t); + dbg_print_tm (_("final date/time: "), &t, tzdefault); + } + return true; } else @@ -485,6 +717,9 @@ main (int argc, char **argv) case STRP_FORMAT: strp_format = optarg; break; + case ARITH_FORMAT: + arith_format = optarg; + break; case 'u': /* POSIX says that 'date -u' is equivalent to setting the TZ environment variable, so this option should do nothing other diff --git a/tests/misc/date-strp.pl b/tests/misc/date-strp.pl index 4fc247cee..eafe03e2f 100644 --- a/tests/misc/date-strp.pl +++ b/tests/misc/date-strp.pl @@ -95,6 +95,34 @@ my @Tests = {OUT=>"03:09:00"}], + ## + ## Date arithmetics + ## + ['a1', "--date-format '%Y %m %d' --arith-format '%d' " . + "--date '2019 07 26 6' +%F", {OUT=>"2019-08-01"}], + ['a2', "--date-format '%Y %m %d' --arith-format '%d' " . + "--date '2019 07 26 -6' +%F", {OUT=>"2019-07-20"}], + ['a3', "--date-format '%Y %m %d' --arith-format '%d' " . + "--date '2019 07 26 -26' +%F", {OUT=>"2019-06-30"}], + ['a4', "--date-format '%Y %m %d' --arith-format '%Y' " . + "--date '2019 07 26 11' +%F", {OUT=>"2030-07-26"}], + ['a5', "--date-format '%Y %m %d' --arith-format '%m' " . + "--date '2019 07 26 11' +%F", {OUT=>"2020-06-26"}], + ['a6', "--date-format '%Y %m %d' --arith-format '%m' " . + "--date '2019 07 26 -11' +%F", {OUT=>"2018-08-26"}], + ['a7', "--date-format '%Y %m %d' --arith-format '%Y %m %d' " . + "--date '2019 07 26 1 1 1' +%F", {OUT=>"2020-08-27"}], + + + ## + ## Time arithmetics + ## + + # +49 hours, -10 minutes, -30 seconds + ['a20', "--date-format '%Y%m%d %H%M%S' --arith-format '%H %M %S' " . + "--date '20190726 184959 49 -10 -30' '+%F %T'", + {OUT=>"2019-07-28 19:39:29"}], + ); # Append "\n" to each OUT=> RHS if the expected exit value is either -- 2.11.0