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 <[email protected]>
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 <[email protected]>
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