bug#18328: can't say date -d '8pm -0500' though other combos work

Pádraig Brady <[email protected]> Sun, 23 Nov 2025 21:15:44 +0000
Newsgroups gmane.comp.gnu.core-utils.bugs
Message-ID <[email protected]>
On 23/11/2025 05:43, Jeffery Palm wrote:
> Hi Padraig,
> 
> Thanks for the feedback.
> 
> I was able to get back to this and have a look at getting this inconsistency resolved, and I was able to find a change that seems to resolve this inconsistency.
> 
> It is still correctly parsing the timezone with AM/PM:
> 
> $ ./src/date --debug -d '2024-01-01 8:00:00PM -0500'
> date: parsed date part: (Y-M-D) 2024-01-01
> date: parsed time part: 08:00:00pm UTC-05
> date: input timezone: parsed date/time string (-05)
> date: using specified time as starting value: '20:00:00'
> date: starting date/time: '(Y-M-D) 2024-01-01 20:00:00 TZ=-05'
> date: '(Y-M-D) 2024-01-01 20:00:00 TZ=-05' = 1704157200 epoch-seconds
> date: timezone: system default
> date: final: 1704157200.000000000 (epoch-seconds)
> date: final: (Y-M-D) 2024-01-02 01:00:00 (UTC)
> date: final: (Y-M-D) 2024-01-01 17:00:00 (UTC-08)
> date: output format: ‘%a %d %b %Y %T %Z’
> Mon 01 Jan 2024 17:00:00 PST
> 
> And will now correctly prioritize time offset with a ime unit is specified, both with AM/PM and without:
> 
> $ ./src/date --debug -d '2024-01-01 8:00:00 -5 days'
> date: parsed date part: (Y-M-D) 2024-01-01
> date: parsed relative part: -5 day(s)
> date: parsed time part: 08:00:00
> date: input timezone: system default
> date: using specified time as starting value: '08:00:00'
> date: starting date/time: '(Y-M-D) 2024-01-01 08:00:00'
> date: warning: when adding relative days, it is recommended to specify noon
> date: after date adjustment (+0 years, +0 months, -5 days),
> date:     new date/time = '(Y-M-D) 2023-12-27 08:00:00'
> date: '(Y-M-D) 2023-12-27 08:00:00' = 1703692800 epoch-seconds
> date: timezone: system default
> date: final: 1703692800.000000000 (epoch-seconds)
> date: final: (Y-M-D) 2023-12-27 16:00:00 (UTC)
> date: final: (Y-M-D) 2023-12-27 08:00:00 (UTC-08)
> date: output format: ‘%a %d %b %Y %T %Z’
> Wed 27 Dec 2023 08:00:00 PST
> 
> $ ./src/date --debug -d '2024-01-01 8:00:00AM -5 days'
> date: parsed date part: (Y-M-D) 2024-01-01
> date: parsed relative part: -5 day(s)
> date: parsed time part: 08:00:00
> date: input timezone: system default
> date: using specified time as starting value: '08:00:00'
> date: starting date/time: '(Y-M-D) 2024-01-01 08:00:00'
> date: warning: when adding relative days, it is recommended to specify noon
> date: after date adjustment (+0 years, +0 months, -5 days),
> date:     new date/time = '(Y-M-D) 2023-12-27 08:00:00'
> date: '(Y-M-D) 2023-12-27 08:00:00' = 1703692800 epoch-seconds
> date: timezone: system default
> date: final: 1703692800.000000000 (epoch-seconds)
> date: final: (Y-M-D) 2023-12-27 16:00:00 (UTC)
> date: final: (Y-M-D) 2023-12-27 08:00:00 (UTC-08)
> date: output format: ‘%a %d %b %Y %T %Z’
> Wed 27 Dec 2023 08:00:00 PST
> 
> And the coreutils complete testsuite is passing:
> 
> ============================================================================
> Testsuite summary for GNU coreutils 9.9.31-1d58e
> ============================================================================
> # TOTAL: 575
> # PASS:  518
> # SKIP:  57
> # XFAIL: 0
> # FAIL:  0
> # XPASS: 0
> # ERROR: 0
> ============================================================================
> 
> Below is the full patch:
> 
> diff --git a/lib/parse-datetime.y b/lib/parse-datetime.y
> index 6c52cd2c4c..71f8b9ae0e 100644
> --- a/lib/parse-datetime.y
> +++ b/lib/parse-datetime.y
> @@ -573,8 +573,8 @@ debug_print_relative_time (char const *item, parser_control const *pc)
>   %parse-param { parser_control *pc }
>   %lex-param { parser_control *pc }
> 
> -/* This grammar has 31 shift/reduce conflicts.  */
> -%expect 31
> +/* This grammar has 40 shift/reduce conflicts.  */
> +%expect 40
> 
>   %union
>   {
> @@ -681,17 +681,17 @@ iso_8601_datetime:
>     ;
> 
>   time:
> -    tUNUMBER tMERIDIAN
> +    tUNUMBER tMERIDIAN o_zone_offset
>         {
>           set_hhmmss (pc, $1.value, 0, 0, 0);
>           pc->meridian = $2;
>         }
> -  | tUNUMBER ':' tUNUMBER tMERIDIAN
> +  | tUNUMBER ':' tUNUMBER tMERIDIAN o_zone_offset
>         {
>           set_hhmmss (pc, $1.value, $3.value, 0, 0);
>           pc->meridian = $4;
>         }
> -  | tUNUMBER ':' tUNUMBER ':' unsigned_seconds tMERIDIAN
> +  | tUNUMBER ':' tUNUMBER ':' unsigned_seconds tMERIDIAN o_zone_offset
>         {
>           set_hhmmss (pc, $1.value, $3.value, $5.tv_sec, $5.tv_nsec);
>           pc->meridian = $6;
> @@ -720,6 +720,11 @@ iso_8601_time:
>   o_zone_offset:
>     /* empty */
>     | zone_offset
> +  | relunit_snumber
> +      {
> +        if (! apply_relative_time (pc, $1, 1)) YYABORT;
> +        debug_print_relative_time (_("relative"), pc);
> +      }
>     ;
> 
>   zone_offset:
> diff --git a/tests/test-parse-datetime.c b/tests/test-parse-datetime.c
> index 23c8598a6f..8bfb7210d6 100644
> --- a/tests/test-parse-datetime.c
> +++ b/tests/test-parse-datetime.c
> @@ -334,6 +334,31 @@ main (_GL_UNUSED int argc, char **argv)
>     ASSERT (result.tv_sec == result2.tv_sec
>             && result.tv_nsec == result2.tv_nsec);
> 
> +  /* Check that timeone works with AM/PM */
> +  p = "2024-01-01 8PM -08:00";
> +  expected.tv_sec = 1704168000;
> +  expected.tv_nsec = 0;
> +  ASSERT (parse_datetime (&result, p, NULL));
> +  LOG (p, expected, result);
> +  ASSERT (expected.tv_sec == result.tv_sec
> +          && expected.tv_nsec == result.tv_nsec);
> +
> +  /* Check that date offset is preferred to timezone */
> +  p = "2024-01-01 8PM -5 days";
> +  expected.tv_sec = 1703725200;
> +  expected.tv_nsec = 0;
> +  ASSERT (parse_datetime (&result, p, NULL));
> +  LOG (p, expected, result);
> +  ASSERT (expected.tv_sec == result.tv_sec
> +          && expected.tv_nsec == result.tv_nsec);
> +
> +  p = "2024-01-01 20:00 -5 days";
> +  expected.tv_sec = 1703725200;
> +  expected.tv_nsec = 0;
> +  ASSERT (parse_datetime (&result, p, NULL));
> +  LOG (p, expected, result);
> +  ASSERT (expected.tv_sec == result.tv_sec
> +          && expected.tv_nsec == result.tv_nsec);
> 
>     /* TZ out of range should cause parse_datetime failure */
>     now.tv_sec = SOME_TIMEPOINT + 4711;
> 
> Regards.
> 
> Jeff
> 
> On Tue, 29 Jul 2025 at 04:22, Pádraig Brady <[email protected] <mailto:[email protected]>> wrote:
> 
>     On 29/07/2025 06:02, Jeffery Palm wrote:
>      > I took a look at this bug, and believe I have a patch that will resolve it.
>      >
>      > $ ../src/date --debug -d '2024-01-01 8:00:00PM -0500'
>      > date: parsed date part: (Y-M-D) 2024-01-01
>      > date: parsed time part: 08:00:00pm UTC-05
>      > date: input timezone: parsed date/time string (-05)
>      > date: using specified time as starting value: '20:00:00'
>      > date: starting date/time: '(Y-M-D) 2024-01-01 20:00:00 TZ=-05'
>      > date: '(Y-M-D) 2024-01-01 20:00:00 TZ=-05' = 1704157200 epoch-seconds
>      > date: timezone: system default
>      > date: final: 1704157200.000000000 (epoch-seconds)
>      > date: final: (Y-M-D) 2024-01-02 01:00:00 (UTC)
>      > date: final: (Y-M-D) 2024-01-01 17:00:00 (UTC-08)
>      > date: output format: ‘%a %d %b %Y %T %Z’
>      > Mon 01 Jan 2024 17:00:00 PST
>      >
>      >
>      > And I was able to run the coreutils testsuite with no tests failing:
>      >
>      > ============================================================================
>      > Testsuite summary for GNU coreutils 9.7.174-083f8
>      > ============================================================================
>      > # TOTAL: 533
>      > # PASS:  476
>      > # SKIP:  57
>      > # XFAIL: 0
>      > # FAIL:  0
>      > # XPASS: 0
>      > # ERROR: 0
>      > ============================================================================
>      >
>      > Are there any other tests/changes I should consider for this?
>      >
>      >
>      > Below is the patch for the changes I made for this, including a new
>      > testcase for AM/PM with timezone.
>      >
>      >
>      > --- a/lib/parse-datetime.y
>      > +++ b/lib/parse-datetime.y
>      > @@ -592,7 +592,7 @@ debug_print_relative_time (char const *item,
>      > parser_control const *pc)
>      >   %token tYEAR_UNIT tMONTH_UNIT tHOUR_UNIT tMINUTE_UNIT tSEC_UNIT
>      >   %token <intval> tDAY_UNIT tDAY_SHIFT
>      >
>      > -%token <intval> tDAY tDAYZONE tLOCAL_ZONE tMERIDIAN
>      > +%token <intval> tDAY tDAYZONE tLOCAL_ZONE tMERIDIAN tMERIDIAN_WITH_ZONE
>      >   %token <intval> tMONTH tORDINAL tZONE
>      >
>      >   %token <textintval> tSNUMBER tUNUMBER
>      > @@ -698,6 +698,27 @@ time:
>      >           set_hhmmss (pc, $1.value, $3.value, $5.tv_sec, $5.tv_nsec);
>      >           pc->meridian = $6;
>      >         }
>      > +  | tUNUMBER tMERIDIAN_WITH_ZONE tSNUMBER o_colon_minutes
>      > +      {
>      > +        set_hhmmss (pc, $1.value, 0, 0, 0);
>      > +        pc->meridian = $2;
>      > +        pc->zones_seen++;
>      > +        if (! time_zone_hhmm (pc, $3, $4)) YYABORT;
>      > +      }
>      > +  | tUNUMBER ':' tUNUMBER tMERIDIAN_WITH_ZONE tSNUMBER o_colon_minutes
>      > +      {
>      > +        set_hhmmss (pc, $1.value, $3.value, 0, 0);
>      > +        pc->meridian = $4;
>      > +        pc->zones_seen++;
>      > +        if (! time_zone_hhmm (pc, $5, $6)) YYABORT;
>      > +      }
>      > +  | tUNUMBER ':' tUNUMBER ':' unsigned_seconds tMERIDIAN_WITH_ZONE
>      > tSNUMBER o_colon_minutes
>      > +      {
>      > +        set_hhmmss (pc, $1.value, $3.value, $5.tv_sec, $5.tv_nsec);
>      > +        pc->meridian = $6;
>      > +        pc->zones_seen++;
>      > +        if (! time_zone_hhmm (pc, $7, $8)) YYABORT;
>      > +      }
>      >     | iso_8601_time
>      >     ;
>      >
>      > @@ -1527,14 +1548,19 @@ yylex (union YYSTYPE *lvalp, parser_control *pc)
>      >
>      >             *p = '\0';
>      >             tp = lookup_word (pc, buff);
>      > -          if (! tp)
>      > +          if (tp)
>      >               {
>      > -              if (debugging (pc))
>      > -                dbg_printf (_("error: unknown word '%s'\n"), buff);
>      > -              return '?';
>      > +              lvalp->intval = tp->value;
>      > +              if (tp->type == tMERIDIAN)
>      > +                {
>      > +                  char const *p = pc->input;
> 
>     Better to use a non shadowing name here ^
> 
>      > +                  while (*p && c_isspace (*p))
>      > +                    p++;
>      > +                  if (*p == '-' || *p == '+')
>      > +                    return tMERIDIAN_WITH_ZONE;
>      > +                }
>      > +              return tp->type;
>      >               }
>      > -          lvalp->intval = tp->value;
>      > -          return tp->type;
>      >           }
>      >
>      >         if (c != '(')
>      > diff --git a/tests/test-parse-datetime.c b/tests/test-parse-datetime.c
>      > index 546b383c55..9766ed7a13 100644
>      > --- a/tests/test-parse-datetime.c
>      > +++ b/tests/test-parse-datetime.c
>      > @@ -335,6 +335,15 @@ main (_GL_UNUSED int argc, char **argv)
>      >     ASSERT (result.tv_sec == result2.tv_sec
>      >             && result.tv_nsec == result2.tv_nsec);
>      >
>      > +  /* Check that timeone works with AM/PM */
>      > +  p = "2024-01-01 8PM -08:00";
>      > +  expected.tv_sec = 1704168000;
>      > +  expected.tv_nsec = 0;
>      > +  ASSERT (parse_datetime (&result, p, NULL));
>      > +  LOG (p, expected, result);
>      > +  ASSERT (expected.tv_sec == result.tv_sec
>      > +          && expected.tv_nsec == result.tv_nsec);
>      > +
>      >
>      >     /* TZ out of range should cause parse_datetime failure */
>      >     now.tv_sec = SOME_TIMEPOINT + 4711;
>     Thanks for looking at this.
>     This changes relative handling unfortunately:
> 
>         $ src/date --debug -d '2024-01-01 8:00:00PM -5 days'
>         date: parsed date part: (Y-M-D) 2024-01-01
>         date: parsed time part: 08:00:00pm UTC-05
>         date: parsed relative part: +1 day(s)
>         ...
>         Wed 03 Jan 2024 01:00:00 GMT
> 
>         $ date --debug -d '2024-01-01 8:00:00PM -5 days'
>         date: parsed date part: (Y-M-D) 2024-01-01
>         date: parsed time part: 08:00:00pm
>         date: parsed relative part: -5 day(s)
>         ...
>         Wed 27 Dec 2023 20:00:00 GMT
> 
>     Now there is an existing ambiguity here,
>     where the AM/PM induces the relative interpretation:
> 
>         $ date --debug -d '2024-01-01 8:00:00PM -5 days'
>         date: parsed date part: (Y-M-D) 2024-01-01
>         date: parsed time part: 08:00:00pm
>         date: parsed relative part: -5 day(s)
> 
>         $ date --debug -d '2024-01-01 8:00:00 -5 days'
>         date: parsed date part: (Y-M-D) 2024-01-01
>         date: parsed time part: 08:00:00 UTC-05
>         date: parsed relative part: +1 day(s)
> 
>     BTW https://bugs.gnu.org/79078 <https://bugs.gnu.org/79078> was a recent bug report
>     along the same lines of the relative part being unexpectedly
>     considered as a timezone offset
> 
>     Now I agree we're already inconsistent in this regard, but I'm sure
>     folks are relying on the AM/PM inducing a relative interpretation.
> 
>     If we were trying to make all this more consistent, IMHO
>     we should change things so that we always interpret +|-<int> <days|minutes|...>
>     as a relative adjustment, whereas your change does the opposite for the AM/PM case.

Thanks for looking at this again.
I've attached the diff in case others want to more easily test.
I see that the unadorned number is still taken as a timezone which is good
for compat (and would be good to add to tests I think):

   $ src/date --debug -d '2024-01-01 20:00 -5'
   date: parsed date part: (Y-M-D) 2024-01-01
   date: parsed time part: 20:00:00 UTC-05

thanks,
Padraig
parse-datetime-relative.diff (text/x-patch, 1.3 KB)
diff --git a/lib/parse-datetime.y b/lib/parse-datetime.y
index 6c52cd2c4c..71f8b9ae0e 100644
--- a/lib/parse-datetime.y
+++ b/lib/parse-datetime.y
@@ -573,8 +573,8 @@ debug_print_relative_time (char const *item, parser_control const *pc)
 %parse-param { parser_control *pc }
 %lex-param { parser_control *pc }

-/* This grammar has 31 shift/reduce conflicts.  */
-%expect 31
+/* This grammar has 40 shift/reduce conflicts.  */
+%expect 40

 %union
 {
@@ -681,17 +681,17 @@ iso_8601_datetime:
   ;

 time:
-    tUNUMBER tMERIDIAN
+    tUNUMBER tMERIDIAN o_zone_offset
       {
         set_hhmmss (pc, $1.value, 0, 0, 0);
         pc->meridian = $2;
       }
-  | tUNUMBER ':' tUNUMBER tMERIDIAN
+  | tUNUMBER ':' tUNUMBER tMERIDIAN o_zone_offset
       {
         set_hhmmss (pc, $1.value, $3.value, 0, 0);
         pc->meridian = $4;
       }
-  | tUNUMBER ':' tUNUMBER ':' unsigned_seconds tMERIDIAN
+  | tUNUMBER ':' tUNUMBER ':' unsigned_seconds tMERIDIAN o_zone_offset
       {
         set_hhmmss (pc, $1.value, $3.value, $5.tv_sec, $5.tv_nsec);
         pc->meridian = $6;
@@ -720,6 +720,11 @@ iso_8601_time:
 o_zone_offset:
   /* empty */
   | zone_offset
+  | relunit_snumber
+      {
+        if (! apply_relative_time (pc, $1, 1)) YYABORT;
+        debug_print_relative_time (_("relative"), pc);
+      }
   ;

 zone_offset: