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

From: Pádraig Brady <[email-masked]>
Date: Tue, 29 Jul 2025 12:22:32 +0100

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 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.

cheers,
Padraig