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