Hello,

Some time ago there was a discussion relating to diffuculties of using
GNU date's parsing. There was a mention of how using strptime(3) makes
parsing explicit and easy.

I like that idea, and decided to try my hand at adding such options.

Attached is a proof of concept.

The first patch adds '--date-format=FORMAT', where FORMAT is
strptime(3) format.
The second patch adds '--arith-format=FORMAT', where FORMAT is limited
to years/months/days/hours/minutes/seconds (%Y/%m/%d/%H/%M/%S).

Examples:

  # Specific date
  $ ./src/date --date-format '%d %b %Y' --date '17 Feb 1979' +%F
  1979-02-17

  # The 100th day of 2019
  $ ./src/date --date-format '%Y %j' --date '2019 100' +%F
  2019-04-10

  # Tuesday of the 10th week in 2018
  $ ./src/date --date-format '%Y %W %A' --date '2018 10 Tue' +%F
  2018-03-06

  # 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

The test file (date-strp.pl) contains more usage examples.

This is just a proof of concept, and of course many things can be
improved and changed (assuming this feature is desired).

Comments and suggestions very welcomed,
 - assaf
>From 82c8b42de7bf9c69432ff175838f01f10008a512 Mon Sep 17 00:00:00 2001
From: Assaf Gordon <assafgor...@gmail.com>
Date: Thu, 25 Jul 2019 02:35:46 -0600
Subject: [PATCH 1/2] date: add --date-format=FORMAT option

Parse -d=STRING dates using strptime(3) instead of gnulib's
parse_datetime.c heuristics.

Example: print the 100th day of 2019:

  $ date --date-format '%Y %j' --date '2019 100' +%F
  2019-04-10

TODO: coreutils.texi, NEWS, usage

* src/date.c (long_options): Add --date-format/STRP_FORMAT option.
(parse_datetime_flags): Replace with ...
(debug): ... new variable.
(strp_format): New variable to hold the user-specified FORMAT string.
(parse_datetime_string): New function, wrapper for
parse_datetime2/strptime.
(batch_convert, main): Call parse_datetime_string instead of
parse_datetime2.
(main): Handle STRP_FORMAT option.
* tests/misc/date-strp.pl: New tests.
* tests/local.mk (TESTS): Add date-strp.pl
---
 src/date.c              |  78 ++++++++++++++++++++++---
 tests/local.mk          |   1 +
 tests/misc/date-strp.pl | 151 ++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 221 insertions(+), 9 deletions(-)
 create mode 100644 tests/misc/date-strp.pl

diff --git a/src/date.c b/src/date.c
index d97d0ae52..4879474e3 100644
--- a/src/date.c
+++ b/src/date.c
@@ -80,7 +80,8 @@ static char const rfc_email_format[] = "%a, %d %b %Y %H:%M:%S 
%z";
 enum
 {
   RFC_3339_OPTION = CHAR_MAX + 1,
-  DEBUG_DATE_PARSING
+  DEBUG_DATE_PARSING,
+  STRP_FORMAT
 };
 
 static char const short_options[] = "d:f:I::r:Rs:u";
@@ -97,6 +98,7 @@ static struct option const long_options[] =
   {"rfc-2822", no_argument, NULL, 'R'},
   {"rfc-3339", required_argument, NULL, RFC_3339_OPTION},
   {"set", required_argument, NULL, 's'},
+  {"date-format", required_argument, NULL, STRP_FORMAT},
   {"uct", no_argument, NULL, 'u'},
   {"utc", no_argument, NULL, 'u'},
   {"universal", no_argument, NULL, 'u'},
@@ -105,8 +107,11 @@ static struct option const long_options[] =
   {NULL, 0, NULL, 0}
 };
 
-/* flags for parse_datetime2 */
-static unsigned int parse_datetime_flags;
+static bool debug ;
+
+/* the strp format string specified by the user */
+static char* strp_format;
+
 
 #if LOCALTIME_CACHE
 # define TZSET tzset ()
@@ -142,6 +147,9 @@ Display the current time in the given FORMAT, or set the 
system date.\n\
   -d, --date=STRING          display time described by STRING, not 'now'\n\
 "), stdout);
       fputs (_("\
+      --date-format=FORMAT   parse -d,-f values according to FORMAT\n\
+"), stdout);
+      fputs (_("\
       --debug                annotate the parsed date,\n\
                               and warn about questionable usage to stderr\n\
 "), stdout);
@@ -281,6 +289,57 @@ Show the local time for 9AM next Friday on the west coast 
of the US\n\
   exit (status);
 }
 
+/* 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)
+    {
+      struct tm t;
+      time_t s = time (NULL);
+      localtime_rz (tzdefault, &s, &t);
+      char *endp = strptime (datestr, strp_format, &t );
+      if (!endp)
+        {
+          if (debug)
+            error (0, 0, _("date string %s does not match format '%s'"),
+                   quotearg (datestr),
+                   strp_format);
+          return false;
+        }
+
+      if (*endp)
+        {
+          if (debug)
+            error (EXIT_FAILURE, 0, _("extraneous characters in date "  \
+                                      "string: %s"),
+                   quotearg (endp));
+          return false;
+        }
+
+      s = mktime (&t);
+      if (s == (time_t)-1)
+        {
+          error (0, errno, _("mktime failed"));
+          return false;
+        }
+
+      *result = make_timespec (s, 0);
+      return true;
+    }
+  else
+    {
+      unsigned int parse_datetime_flags = debug ? PARSE_DATETIME_DEBUG : 0 ;
+
+      return parse_datetime2 (result, datestr, NULL,
+                              parse_datetime_flags,
+                              tzdefault, tzstring);
+    }
+}
+
+
 /* Parse each line in INPUT_FILENAME as with --date and display each
    resulting time and date.  If the file cannot be opened, tell why
    then exit.  Issue a diagnostic for any lines that cannot be parsed.
@@ -322,8 +381,7 @@ batch_convert (const char *input_filename, const char 
*format,
           break;
         }
 
-      if (! parse_datetime2 (&when, line, NULL,
-                             parse_datetime_flags, tz, tzstring))
+      if (! parse_datetime_string (&when, line, tz, tzstring))
         {
           if (line[line_length - 1] == '\n')
             line[line_length - 1] = '\0';
@@ -378,7 +436,7 @@ main (int argc, char **argv)
           datestr = optarg;
           break;
         case DEBUG_DATE_PARSING:
-          parse_datetime_flags |= PARSE_DATETIME_DEBUG;
+          debug = true;
           break;
         case 'f':
           batch_file = optarg;
@@ -424,6 +482,9 @@ main (int argc, char **argv)
           set_datestr = optarg;
           set_date = true;
           break;
+        case STRP_FORMAT:
+          strp_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
@@ -548,9 +609,8 @@ main (int argc, char **argv)
             {
               if (set_datestr)
                 datestr = set_datestr;
-              valid_date = parse_datetime2 (&when, datestr, NULL,
-                                            parse_datetime_flags,
-                                            tz, tzstring);
+
+              valid_date = parse_datetime_string (&when, datestr, tz, 
tzstring);
             }
         }
 
diff --git a/tests/local.mk b/tests/local.mk
index e88d99f24..2a4f277ff 100644
--- a/tests/local.mk
+++ b/tests/local.mk
@@ -254,6 +254,7 @@ all_tests =                                 \
   tests/tail-2/tail-n0f.sh                     \
   tests/misc/ls-misc.pl                                \
   tests/misc/date.pl                           \
+  tests/misc/date-strp.pl                              \
   tests/misc/date-next-dow.pl                  \
   tests/misc/ptx-overrun.sh                    \
   tests/misc/xstrtol.pl                                \
diff --git a/tests/misc/date-strp.pl b/tests/misc/date-strp.pl
new file mode 100644
index 000000000..4fc247cee
--- /dev/null
+++ b/tests/misc/date-strp.pl
@@ -0,0 +1,151 @@
+#!/usr/bin/perl
+# Test date's --date-format=FORMAT feature
+
+# Copyright (C) 2019 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+use strict;
+
+(my $ME = $0) =~ s|.*/||;
+
+# Turn off localization of executable's output.
+@ENV{qw(LANGUAGE LANG LC_ALL)} = ('C') x 3;
+
+# Export TZ=UTC0 so that zone-dependent strings match.
+$ENV{TZ} = 'UTC0';
+
+# NOTE: for all tests, we print only the parts that are parsed,
+#       as the other parts get the values from 'now' and will change
+#       every time.
+my @Tests =
+    (
+     ##
+     ## Date related formats specifiers (%Y, %b, %d, %M)
+     ##
+     ['d1', "--date-format '%Y' --date '2003' +%Y", {OUT=>"2003"}],
+     ['d2', "--date-format '%d %b %Y' --date '17 Feb 1979' +%F",
+      {OUT=>"1979-02-17"}],
+     ['d3', "--date-format '%b %Y,,, %d' --date 'Mar 1981,,, 13' +%F",
+      {OUT=>"1981-03-13"}],
+     ['d4', "--date-format '%y %d %m' --date '87 7 2' +%F",
+      {OUT=>"1987-02-07"}],
+     ['d5', "--date-format '%y %m %d' --date '87 02 07' +%F",
+      {OUT=>"1987-02-07"}],
+
+     # Common case that frustrates users with the default parsing heuristics,
+     # see: 
https://www.gnu.org/software/coreutils/manual/html_node/Pure-numbers-in-date-strings.html
+     ['d6',  "--date-format '%y%m%d' --date '010203' +%F", 
{OUT=>"2001-02-03"}],
+     ['d7',  "--date-format '%y%d%m' --date '010203' +%F", 
{OUT=>"2001-03-02"}],
+     ['d8',  "--date-format '%d%m%y' --date '010203' +%F", 
{OUT=>"2003-02-01"}],
+     ['d9',  "--date-format '%d%y%m' --date '010203' +%F", 
{OUT=>"2002-03-01"}],
+     ['d10', "--date-format '%m%d%y' --date '010203' +%F", 
{OUT=>"2003-01-02"}],
+     ['d11', "--date-format '%m%y%d' --date '010203' +%F", 
{OUT=>"2002-01-03"}],
+     ['d12', "--date-format '%Y%m%d' --date '01020304' +%F",
+      {OUT=>"0102-03-04"}],
+     ['d13', "--date-format '%m%Y%d' --date '01020304' +%F",
+      {OUT=>"0203-01-04"}],
+
+     # Tuesday (%A) of the 10th week (%W) in 2018 (%Y)
+     ['d20', "--date-format '%Y %W %A' --date '2018 10 Tue' +%F",
+      {OUT=>"2018-03-06"}],
+     # Same, with day-of-week (1..7), Monday=1 (%u)
+     ['d21', "--date-format '%Y %W %u' --date '2018 10 2' +%F",
+      {OUT=>"2018-03-06"}],
+     # Same, with day-of-week (0..6), Sunday=1 (%U)
+     ['d22', "--date-format '%Y %W %w' --date '2018 10 2' +%F",
+      {OUT=>"2018-03-06"}],
+
+     # The 100th day of 2019
+     ['d23', "--date-format '%Y %j' --date '2019 100' +%F",
+      {OUT=>"2019-04-10"}],
+     # Same, with funky separator
+     ['d24', "--date-format '%Y->%j' --date '2019->100' +%F",
+      {OUT=>"2019-04-10"}],
+     # Same, without separator
+     ['d25', "--date-format '%Y%j' --date '2019100' +%F",
+      {OUT=>"2019-04-10"}],
+
+
+     ##
+     ## Time related formats specifiers (%Y, %b, %d, %M)
+     ##
+     ['t1', "--date-format '%H^%M^%S' --date '13^14^15' +%T",
+      {OUT=>"13:14:15"}],
+
+     # AM/PM
+     ['t2', "--date-format '%I:%M:%S%p' --date '03:09:00pm' +%T",
+      {OUT=>"15:09:00"}],
+
+     # %T vs %H:%M:%S
+     ['t3', "--date-format '%T' --date '03:09:00' +%H:%M:%S",
+      {OUT=>"03:09:00"}],
+     ['t4', "--date-format '%H:%M:%S' --date '03:09:00' +%T",
+      {OUT=>"03:09:00"}],
+
+
+    );
+
+# Append "\n" to each OUT=> RHS if the expected exit value is either
+# zero or not specified (defaults to zero).
+foreach my $t (@Tests)
+  {
+    my $exit_val;
+    foreach my $e (@$t)
+      {
+        ref $e && ref $e eq 'HASH' && defined $e->{EXIT}
+          and $exit_val = $e->{EXIT};
+      }
+    foreach my $e (@$t)
+      {
+        ref $e && ref $e eq 'HASH' && defined $e->{OUT} && ! $exit_val
+          and $e->{OUT} .= "\n";
+      }
+  }
+
+# Repeat all tests with --debug option, ensure it does not cause any regression
+my @debug_tests;
+foreach my $t (@Tests)
+  {
+    # Skip tests with EXIT!=0 or ERR_SUBST part
+    # (as '--debug' requires its own ERR_SUBST).
+    my $exit_val;
+    my $have_err_subst;
+    foreach my $e (@$t)
+      {
+        next unless ref $e && ref $e eq 'HASH';
+        $exit_val = $e->{EXIT} if defined $e->{EXIT};
+        $have_err_subst = 1 if defined $e->{ERR_SUBST};
+      }
+    next if $exit_val || $have_err_subst;
+
+    # Duplicate the test, add '--debug' argument
+    my @newt = @$t;
+    $newt[0] = 'dbg_' . $newt[0];
+    $newt[1] = '--debug ' . $newt[1];
+
+    # Discard all debug printouts before comparing output
+    push @newt, {ERR_SUBST => q!s/^date: .*\n//m!};
+
+    push @debug_tests, \@newt;
+  }
+push @Tests, @debug_tests;
+
+
+my $save_temps = $ENV{DEBUG};
+my $verbose = $ENV{VERBOSE};
+
+my $prog = 'date';
+my $fail = run_tests ($ME, $prog, \@Tests, $save_temps, $verbose);
+exit $fail;
-- 
2.11.0

>From f347d3b3790fb548c7f1cad0275077f8ed45dd9d Mon Sep 17 00:00:00 2001
From: Assaf Gordon <assafgor...@gmail.com>
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

Reply via email to