15

All the other questions on the SE network deal with scenarios where either the date is assumed to be now (Q) or where only a date is specified (Q).

What I want to do is supply a date and time, and then subtract a time from that.
Here is what I tried first:

date -d "2018-12-10 00:00:00 - 5 hours - 20 minutes - 5 seconds"

This results in 2018-12-10 06:39:55 - It added 7 hours. Then subtracted 20:05 minutes.

After reading the man and info page of date, I thought I have it fixed with this:

date -d "2018-12-10T00:00:00 - 5 hours - 20 minutes - 5 seconds"

But, same result. Where does it even get the 7 hours from?

I tried other dates as well because I thought maybe we had 7200 leap seconds on that day, who knows lol. But same results.

A few more examples:

$ date -d "2018-12-16T00:00:00 - 24 hours" +%Y-%m-%d_%H:%M:%S
2018-12-17_02:00:00

$ date -d "2019-01-19T05:00:00 - 2 hours - 5 minutes" +%Y-%m-%d_%H:%M:%S
2019-01-19_08:55:00

But here it becomes interesting. If I omit the time on input, it works fine:

$ date -d "2018-12-16 - 24 hours" +%Y-%m-%d_%H:%M:%S
2018-12-15_00:00:00

$ date -d "2019-01-19 - 2 hours - 5 minutes" +%Y-%m-%d_%H:%M:%S
2019-01-18_21:55:00

$ date --version
date (GNU coreutils) 8.30

What am I missing?

Update: I've added a Z at the end, and it changed the behaviour:

$ date -d "2019-01-19T05:00:00Z - 2 hours" +%Y-%m-%d_%H:%M:%S
2019-01-19_04:00:00

I'm still confused though. There is not much about this in the GNU info page about date.

I'm guessing this is a timezone issue, but quoting The Calendar Wiki on ISO 8601:

If no UTC relation information is given with a time representation, the time is assumed to be in local time.

Which is what I want. My local time is set correctly too. I'm not sure why date would mess with the timezone at all in this simple case of me supplying a datetime and wanting to subtract something off of it. Shouldn't it subtract the hours from the date string first? Even if it does convert it to a date first and then does the subtraction, if I leave out any subtractions I get exactly what I want:

$ date -d "2019-01-19T05:00:00" +%Y-%m-%d_%H:%M:%S
2019-01-19_05:00:00

So IF this truly is a timezone issue, where does that madness come from?

19

That last example should have clarified things for you: timezones.

$ TZ=UTC date -d "2019-01-19T05:00:00Z - 2 hours" +%Y-%m-%d_%H:%M:%S
2019-01-19_03:00:00
$ TZ=Asia/Colombo date -d "2019-01-19T05:00:00Z - 2 hours" +%Y-%m-%d_%H:%M:%S 
2019-01-19_08:30:00

As the output clearly varies by the timezone, I'd suspect some non-obvious default taken for a time string without a timezone specified. Testing a couple of values, it seems to be UTC-05:00, though I'm not sure what that is.

$ TZ=UTC date -d "2019-01-19T05:00:00 - 2 hours" +%Y-%m-%d_%H:%M:%S%Z
2019-01-19_08:00:00UTC
$ TZ=UTC date -d "2019-01-19T05:00:00Z - 2 hours" +%Y-%m-%d_%H:%M:%S%Z
2019-01-19_03:00:00UTC
$ TZ=UTC date -d "2019-01-19T05:00:00" +%Y-%m-%d_%H:%M:%S%Z           
2019-01-19_05:00:00UTC

It's only used when performing date arithmetic.


It seems the issue here is that - 2 hours is not taken as arithmetic, but as a timezone specifier:

# TZ=UTC date -d "2019-01-19T05:00:00 - 2 hours" +%Y-%m-%d_%H:%M:%S%Z --debug
date: parsed datetime part: (Y-M-D) 2019-01-19 05:00:00 UTC-02
date: parsed relative part: +1 hour(s)
date: input timezone: parsed date/time string (-02)
date: using specified time as starting value: '05:00:00'
date: starting date/time: '(Y-M-D) 2019-01-19 05:00:00 TZ=-02'
date: '(Y-M-D) 2019-01-19 05:00:00 TZ=-02' = 1547881200 epoch-seconds
date: after time adjustment (+1 hours, +0 minutes, +0 seconds, +0 ns),
date:     new time = 1547884800 epoch-seconds
date: timezone: TZ="UTC" environment value
date: final: 1547884800.000000000 (epoch-seconds)
date: final: (Y-M-D) 2019-01-19 08:00:00 (UTC)
date: final: (Y-M-D) 2019-01-19 08:00:00 (UTC+00)
2019-01-19_08:00:00UTC

So, not only is no arithmetic being done, there seems to be a daylight savings 1 hour adjustment on the time, leading to a somewhat nonsensical time for us.

This also holds for addition:

# TZ=UTC date -d "2019-01-19T05:00:00 + 5:30 hours" +%Y-%m-%d_%H:%M:%S%Z --debug
date: parsed datetime part: (Y-M-D) 2019-01-19 05:00:00 UTC+05:30
date: parsed relative part: +1 hour(s)
date: input timezone: parsed date/time string (+05:30)
date: using specified time as starting value: '05:00:00'
date: starting date/time: '(Y-M-D) 2019-01-19 05:00:00 TZ=+05:30'
date: '(Y-M-D) 2019-01-19 05:00:00 TZ=+05:30' = 1547854200 epoch-seconds
date: after time adjustment (+1 hours, +0 minutes, +0 seconds, +0 ns),
date:     new time = 1547857800 epoch-seconds
date: timezone: TZ="UTC" environment value
date: final: 1547857800.000000000 (epoch-seconds)
date: final: (Y-M-D) 2019-01-19 00:30:00 (UTC)
date: final: (Y-M-D) 2019-01-19 00:30:00 (UTC+00)
2019-01-19_00:30:00UTC

Debugging a bit more, the parsing seems to be: 2019-01-19T05:00:00 - 2 (-2 being the timezone), and hours (= 1 hour), with an implied addition. It becomes easier to see if you use minutes instead:

# TZ=UTC date -d "2019-01-19T05:00:00 - 2 minutes" +%Y-%m-%d_%H:%M:%S%Z --debug
date: parsed datetime part: (Y-M-D) 2019-01-19 05:00:00 UTC-02
date: parsed relative part: +1 minutes
date: input timezone: parsed date/time string (-02)
date: using specified time as starting value: '05:00:00'
date: starting date/time: '(Y-M-D) 2019-01-19 05:00:00 TZ=-02'
date: '(Y-M-D) 2019-01-19 05:00:00 TZ=-02' = 1547881200 epoch-seconds
date: after time adjustment (+0 hours, +1 minutes, +0 seconds, +0 ns),
date:     new time = 1547881260 epoch-seconds
date: timezone: TZ="UTC" environment value
date: final: 1547881260.000000000 (epoch-seconds)
date: final: (Y-M-D) 2019-01-19 07:01:00 (UTC)
date: final: (Y-M-D) 2019-01-19 07:01:00 (UTC+00)
2019-01-19_07:01:00UTC

So, well, date arithmetic is being done, just not the one that we asked for. ¯\(ツ)/¯

  • 1
    @confetti it does indeed; I think this default timezone is used only when adding/subtracting (comparing TZ=UTC date -d "2019-01-19T05:00:00Z - 2 hours" +%Y-%m-%d_%H:%M:%S vs TZ=UTC date -d "2019-01-19T05:00:00 - 2 hours" +%Y-%m-%d_%H:%M:%S) – Olorin Jan 30 at 8:54
  • 2
    Could be a possible bug in date, as per the ISO 8601 standard, if no timezone is supplied, local time(zone) should be assumed, which in case of an arithmetic operation, it is not. Very strange issue to me though. – confetti Jan 30 at 8:57
  • 1
    @confetti found the problem: - 2 hours is taken as the timezone specifier here. – Olorin Jan 30 at 9:17
  • 2
    Wow. Now that somehow makes sense. Well, at least it explains it. Thank you a lot for figuring that out. To me it sounds like their parser needs an update though, as clearly with a format and whitespace like this you do not want to specify the timezone by the ` - 2 hours` and it's surely causing confusion. If they don't want to update the parser, at least the manual should get a note about this. – confetti Jan 30 at 9:19
  • 1
    Today I learned about date's --debug option! Great explanation. – Jeff Schaller Jan 30 at 17:26
6

It works correctly when you convert the input date to ISO 8601 first:

$ date -d "$(date -Iseconds -d "2018-12-10 00:00:00") - 5 hours - 20 minutes - 5 seconds"
So 9. Dez 18:39:55 CET 2018
  • Thank you, that does work indeed, but is there any explanation for this? I've added some more info about the ISO 8601 thing to my question, and I really don't see where date would mess this up as without any subtractions supplied, the timezone is left untouched and everything is as expected too without having to supply any timezone information or conversion. – confetti Jan 30 at 8:49
  • I cannot tell, sorry. If someone else could answer this I'd be happy because I was wondering too. – RoVo Jan 30 at 8:50
  • I'd be glad too but I'll accept this for now because it does fix my issue! – confetti Jan 30 at 8:54
  • 3
    This works because date -I also outputs the timezone specifier (e.g. 2018-12-10T00:00:00+02:00) which fixes the issue described in Olorin's answer – ilkkachu Jan 30 at 19:22
6

TLDR: This is not a bug. You've just found one of the subtle but documented behaviours of date. When performing time arithmetic with date, use a timezone-independent format (like Unix time) or read very carefully the documentation to know how to properly use this command.


GNU date uses your system settings (the TZ environment variable or, if unset, the system defaults) to determine the timezone of both the date feeded with the -d/--date option and the date reported by the +format argument. The --date option also let you override the timezone for its own option-argument but it doesn't override the timezone of +format. That's the root of the confusion, IMHO.

Considering that my timezone is UTC-6, compare the following commands:

$ date -d '1970-01-01 00:00:00' '+Normal: %F %T %:z%nUnix: %s'
Normal: 1970-01-01 00:00:00 -06:00
Unix: 21600
$ date -d '1970-01-01 00:00:00 UTC' '+Normal: %F %T %:z%nUnix: %s'
Normal: 1969-12-31 18:00:00 -06:00
Unix: 0
$ TZ='UTC0' date -d '1970-01-01 00:00:00 UTC' '+Normal: %F %T %:z%nUnix: %s'
Normal: 1970-01-01 00:00:00 +00:00
Unix: 0

The first one uses my timezone for both -d and +format. The second one uses UTC for -d but my timezone for +format. The third one uses UTC for both.

Now, compare the following simple operations:

$ date -d '1970-01-01 00:00:00 UTC +1 day' '+Normal: %F %T %:z%nUnix: %s'
Normal: 1970-01-01 18:00:00 -06:00
Unix: 86400
$ TZ='UTC0' date -d '1970-01-01 00:00:00 UTC +1 day' '+Normal: %F %T %:z%nUnix: %s'
Normal: 1970-01-02 00:00:00 +00:00
Unix: 86400

Even if the Unix time is telling me the same thing, the "Normal" time differs because of my own timezone.

If I wanted to do the same operation but using exclusively my timezone:

$ TZ='CST+6' date -d '1970-01-01 00:00:00 -06:00 +1 day' '+Normal: %F %T %:z%nUnix: %s'
Normal: 1970-01-02 00:00:00 -06:00
Unix: 108000
  • 3
    Upvoted for mentioning epoch/Unix time, which imo is the only sane way to do time arithmetic – Rui F Ribeiro Jan 30 at 9:58
  • 1
    TL;DR If you just know your local time offset from Zulu time (e.g. west coast USA is -8 hours or -08:00), simply append that to input date-time...nothing else to fiddle with. Example: date -d '2019-02-28 14:05:36-08:00 +2 days 4 hours 3 seconds' +'local: %F %T' gives local: 2019-03-02 18:05:36 – B Layer Feb 28 at 22:52
  • @BLayer You're right, it works as expected in that scenario. But imagine the situation where a given script using that approach is executed in a place with a different timezone. The output, while technically correct, may look wrong depending on the information provided by the +format argument. For instance, on my system, the same command outputs: local: 2019-03-02 20:05:39 (if I add %:z to +format, it becomes obvious that the info is correct and the discrepancy is due to the timezones). – nxnev Mar 1 at 1:45
  • @BLayer IMHO, a better general approach would be to use the same timezone for both -d and +format or Unix time, as I said in the answer, to avoid unexpected results. – nxnev Mar 1 at 1:45
  • 1
    @nxnev Agreed, if you're writing a script to be published/shared. The opening words "if you know your own timezone", besides having a literal meaning, are supposed to imply casual/personal use. Someone publishing a script relying on something as flimsy as knowing just their own system's tz probably shouldn't be in the script sharing game. :) On the other hand I do/would use my own command on all my personal environments. – B Layer Mar 1 at 2:17
3

GNU date does support simple date arithmetic, though epoch time calculations as shown in @sudodus' answer are sometimes clearer (and more portable).

The use of +/- when there is no timezone specified in the timestamp triggers an attempt to match a timezone next, before anything else is parsed.

Here's one way to do it, use "ago" instead of "-":

$ date -d "2018-12-10 00:00:00 5 hours ago 20 minutes ago 5 seconds ago"
Sun Dec  9 18:39:55 GMT 2018

or

$ date -d "2018-12-10 00:00:00Z -5 hours -20 minutes -5 seconds"
Sun Dec  9 18:39:55 GMT 2018

(Though you cannot arbitrarily use "Z", it works in my zone, but that makes it a UTC/GMT zone timestamp - use your own zone, or %z/%Z by appending ${TZ:-$(date +%z)} to the timestamp instead.)

Adding extra time terms of these forms adjust time:

  • "5 hours ago" subtract 5 hours
  • "4 hours" add (implicit) 4 hours
  • "3 hours hence" add (explicit) 3 hours (not supported in older versions)

Many complex adjustments, in any order, can be used (though relative and variable terms like "14 weeks hence last monday" are asking for trouble ;-)

(There's another small beartrap here too, date will always give a valid date, so date -d "2019-01-31 1 month" gives 2019-03-03, as does "next month")

Given the wide variety of time and date formats that are supported, timezone parsing is necessarily sloppy: it can be a single or multi-letter suffix, an hour or hour:minute offset, a name "America/Denver" (or even a filename in the case of the TZ variable).

Your 2018-12-10T00:00:00 version doesn't work because "T" is just a delimiter, not a timezone, adding "Z" at the end makes that work (subject to correctness of chosen zone) as expected too.

See: https://www.gnu.org/software/tar/manual/html_node/Date-input-formats.html and in particular section 7.7.

  • 1
    Other options include: date -d "2018-12-10 00:00:00 now -5 hours, or today or 0 hour instead of now, anything that tells date that the token after the time is not a timezone offset. – Stéphane Chazelas Jan 31 at 14:03
  • Or, for the sake of thoroughness, leave nothing after the date-time by reordering args: date -d "-5 hours 2018-12-10 00:00:00"` – B Layer Mar 7 at 19:21
2

This solution is easy to understand, but a little more complicated, so I show it as a shellscript.

  • convert to 'seconds since 1970-01-01 00:00:00 UTC'
  • add or subtract the difference
  • convert back to a human readable format with a final date command line

Shellscript:

#!/bin/bash

startdate="2018-12-10 00:00:00"

ddif="0"          # days
diff="-5:-20:-5"  # hours:minutes:seconds

#-----------------------------------------------------------------------------

ss1970in=$(date -d "$startdate" "+%s")  # seconds since 1970-01-01 00:00:00 UTC
printf "%11s\n" "$ss1970in"

h=${diff%%:*}
m=${diff#*:}
m=${m%:*}
s=${diff##*:}
difs=$(( (((ddif*24+h)*60)+m)*60+s ))
printf "%11s\n" "$difs"

ss1970ut=$((ss1970in + difs))  # add/subtract the time difference
printf "%11s\n" "$ss1970ut"

date -d "@$ss1970ut" "+%Y-%m-%d %H:%M:%S"

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.