12

If I subtract a time amount from the current date, GNU date works intuitively:

date '+%F %R'; date '+%F %R' --date='- 1 hour'
2021-04-19 15:35
2021-04-19 14:35

However, when I use a date as operand, the result is unexpected:

$ date '+%F %R' --date='2000/1/2 03:04:05 - 1 hour'
2000-01-02 06:04

$ date '+%F %R' --date='2000/1/2 03:04:05 + 1 hour ago'
2000-01-02 02:04

How is date intepreting the $date - 1 hour expression?

2
  • 1
    Does this answer your question? How to add time to a date variable in linux? – muru Apr 19 at 12:40
  • 2
    @muru I had a look, however, this question, and the answers, are more specific. Of the answers provided to that question, the third is wrong, the second doesn't apply to this question, and the first is more complex (and less informative) than the current answers to this question. – Marcus Apr 19 at 20:27
19

In short: The date you give with --date is taken in local time, unless you specify a time zone, and something like +/- NNN is taken as one. Only anything after that, even if it's just hour is taken as the relative modifier. So - 1 hour doesn't mean to subtract one hour from the given time, but to specify that the time is in the time zone UTC-01, and then to add one hour to it.


What I think should work for what you're trying, would be to either explicitly give the timezone before the offset, or put the offset first so it can't be confused with a timezone.

Here, using the Central European Summer Time timezone (CEST), and today's date, with %Z added to the output to show the timezone. (You could also use %z to output the numeric timezone, or +0200 here.)

$ date +'%F %T %Z' -d '2021-04-19 12:00:00 CEST + 5 hours'
2021-04-19 17:00:00 CEST
$ date +'%F %T %Z' -d '+ 5 hours 2021-04-19 12:00:00'
2021-04-19 17:00:00 CEST

Though of course for a January date like in the question, a summer-time time zone like CEST would not be a valid one. But rearranging the two still works, the time you give is just taken as the local time at that time.

$ date +'%F %T %Z' -d '+ 5 hours 2021-01-01 12:00:00'
2021-01-01 17:00:00 CET

(And for 2021-10-31 02:30:00 I get CET, even though that time also exists in CEST...)

(See older revisions of this answer for more examples on how it interprets various inputs.)


As per @muru's answer on another question, we can also use the --debug option to have the program actually tell us what it did. Note the second and third lines:

$ date --debug +'%F %T %Z' -d '2021-04-19 12:00:00 - 1 hour'
date: parsed date part: (Y-M-D) 2021-04-19
date: parsed time part: 12:00:00 TZ=-01:00
date: parsed relative part: +1 hour(s)
date: input timezone: -01:00 (set from parsed date/time string)
date: using specified time as starting value: '12:00:00'
date: starting date/time: '(Y-M-D) 2021-04-19 12:00:00 TZ=-01:00'
date: '(Y-M-D) 2021-04-19 12:00:00 TZ=-01:00' = 1618837200 epoch-seconds
date: after time adjustment (+1 hours, +0 minutes, +0 seconds, +0 ns),
date:     new time = 1618840800 epoch-seconds
date: output timezone: +01:00 (set from TZ="Europe/Berlin" environment value)
date: final: 1618840800.000000000 (epoch-seconds)
date: final: (Y-M-D) 2021-04-19 14:00:00 (UTC0)
date: final: (Y-M-D) 2021-04-19 16:00:00 (output timezone TZ=+01:00)
2021-04-19 16:00:00 CEST

The man page says:

The date string format is more complex than is easily documented here [...]

Which indeed seems quite apt. The more comprehensive documentation is in the info pages, or online: https://www.gnu.org/software/coreutils/manual/html_node/Date-input-formats.html

3
  • @Isaac, yes, that second one says to take 22:00 in UTC+05, which is 17:00 in UTC. – ilkkachu Apr 19 at 12:55
  • Have you noticed that the date specified by the user is in January (2000-1-2)? And that in January there is no DST in effect? The timezone should be CET, at that time. – Isaac Apr 19 at 20:43
  • 1
    @Isaac, right, thanks. (I hate DST.) – ilkkachu Apr 19 at 20:54
7

-d

Yes, when a -d value is simple, its effect could be easily understood.
Yes, a -d '- 1 hour' will offset the time 1 hour back.

$ date ; date -d '- 1 hour'
Mon 19 Apr 2021 07:58:52 AM EDT
Mon 19 Apr 2021 06:58:52 AM EDT

Timezone

The string that date generates specify a "point in time".
By default that "point in time" is written in local time. But it also could be written in UTC0:

$ date ; date -u
Mon 19 Apr 2021 12:43:04 PM WAT
Mon 19 Apr 2021 11:43:04 AM UTC

Assuming your local time is WAT - West Africa Time (WAT).

Of course, the same "point in time" could happen in other timezones:

$ TZ=Asia/Kolkata date -d "Mon 19 Apr 2021 12:43:04 PM WAT"
Mon 19 Apr 2021 05:13:04 PM IST

Note that what is inside the -d value is exactly what date printed above.

That means that a "point in time" is complete only when the timezone is included.

That's why it is a good idea to print dates with %z or %Z added. The format %F %R is incomplete in this sense, better use +'%F %R %z'.

datestring

When you used the date 2000/1/2 03:04:05 you didn't specify a timezone.
That is why the command date takes the next - 1 to mean a timezone:

$ date -u -d '2000/1/2 03:04:05'; date -u -d '2000/1/2 03:04:05 - 1'
Sun 02 Jan 2000 03:04:05 AM WAT
Sun 02 Jan 2000 05:04:05 AM WAT

That's assuming your local time is WAT (and doesn't change for DST, no summer time). I can not tell as you didn't use %z nor %Z. The timezone change changed the -d value from +1 (WAT) to -1, two hour difference.

Additionally, you wrote hour after that, that is an additional hour:

$ date -d '2000/1/2 03:04:05'; date -d '2000/1/2 03:04:05 - 1 hour'
Sun 02 Jan 2000 03:04:05 AM WAT
Sun 02 Jan 2000 06:04:05 AM WAT

Confirming your results.

And finally, the +1:

$ date -d '2000/1/2 03:04:05 - 1 hour'; date -d '2000/1/2 03:04:05 + 1 hour ago'
Sun 02 Jan 2000 06:04:05 AM WAT
Sun 02 Jan 2000 02:04:05 AM WAT

The +1 doesn't change the time value (as it is in WAT, or +1, already), but the hour ago shifts the time 1 hour back (from 3 to 2).

Solutions

Put the timezone in the -d time value.
Or, put the relative part first - 1 hour

$ date -d '2000/1/2 03:04:05 +0100 - 1 hour'
  Sun 02 Jan 2000 02:04:05 AM WAT


$ date -d '-1 hour 2000/1/2 03:04:05'
  Sun 02 Jan 2000 02:04:05 AM WAT
2
  • 1
    A never ending hope that someday downvoters will state their issue (so we can correct). – Isaac Apr 19 at 20:37
  • Or so that we can correct them. – Peter - Reinstate Monica Apr 20 at 20:27
1

Many other date handling tools do the parsing and the date arithmetic as separate steps. For example with perl:

perl -sE '
    use Time::Piece;
    use Time::Seconds;

    $t = Time::Piece->strptime($timestamp, $format);
    say $t->cdate;

    $s = $t + ONE_HOUR;
    say $s->cdate;

    $s = $t - ONE_HOUR;
    say $s->cdate;
' -- -timestamp='2000/1/2 03:04:05' -format='%Y/%m/%d %T'
Sun Jan  2 03:04:05 2000
Sun Jan  2 04:04:05 2000
Sun Jan  2 02:04:05 2000
6
  • 1
    Whilst this is arguably interesting, you're answering a different question to the one actually asked. – Toby Speight Apr 19 at 19:41
  • 1
    That is true. I remain unapologetic. – glenn jackman Apr 19 at 19:44
  • @Steven, that's a non-sequiteur - POSIX date doesn't specify -d, which is a GNU extension. I agree that this answer may be useful (and perhaps it answers the unstated needs of the asker), but it doesn't give the understanding of the GNU date -d parsing that the asker requested. – Toby Speight Apr 19 at 20:33
  • @TobySpeight sometimes, the best answer to "how do I do that", is "dont do that" meta.stackexchange.com/questions/66377 – Steven Penny Apr 19 at 21:17
  • @Steven, am I reading the question too literally? I read it as a "why?" rather than a "how to?" - though I'm now seeing that one could infer the latter, between the lines, if we squint a little. Anyway, good information in this answer, and I would be withdrawing my downvote if SE would let me do so this late. – Toby Speight Apr 20 at 6:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.