echo 20171231 | xargs -i date -d "{} +1 day" | xargs -i date -d "{} -1 month"
**Fri Dec  1 00:00:00 PST 2017**

In this case, when date formatting commands are pipelined I am get Dec 1.

echo 20171231 | xargs -i date -d "{}  +1day -1 month"
Sat Dec  2 00:00:00 PST 2017 

Whereas when the date formatting is included in a single data command getting result as Dec 2.

In the above command looks like -1 month is getting precedence over +1 day.

Can someone help me in understanding how this is working ?

  • I remember seeing some advice, probably here, to use day 15 when doing month manipulation. The links through the duplicate are good reading. – Jeff Schaller Feb 8 '18 at 21:28

No, there is no order of precedence.

Ironically, this just came up on the Debian Users' mailing list this month, where it was pointed out that to a human being reading the supposed human-readable natural language date manipulation commands the behaviour of the GNU date tool seems quite maddeningly inconsistent. Vincent Lefèvre gave these examples:

jdebp % date +%Y-%m-%d -d '2003-02-01 - 1 month'
jdebp % date +%Y-%m-%d -d '2003-02-01 - 31 days'
jdebp % date +%Y-%m-%d -d '2003-02-01 - 31 days + 1 month'
jdebp % date +%Y-%m-%d -d '2003-02-01 - 1 month + 1 month'
jdebp % date +%Y-%m-%d -d '2003-09-01 1 day ago + 1 month'
jdebp % date +%Y-%m-%d -d '2003-09-01 1 day ago'
jdebp % date +%Y-%m-%d -d '2003-08-31 + 1 month'
jdebp %

What's actually happening internally in date is that during the calculation it is constructing intermediate invalid dates with negative values in places, such as 2003-03-(-30) for example. Then it renormalizes these invalid dates after everything is done, using a function from the C language's standard library.

What it is not doing is renormalizing at each step, as a human does. So 2003-02-01 less 31 days to the GNU date program is an invalid date, the negative 30th of February, and not a valid date in January as a human might calculate. Add one month, and that becomes an invalid date in March, still the negative 30th, which finally renormalizes to that date in January because of course adjusting to turn -30 into a number greater than zero skips back over the entire month of February. The unrenormalized invalid dates in the other examples are 2003-10-00, 2003-09-00, and 2003-09-31.

Applying this to your example:

  • 2017-12-31 + 1 day is 2017-12-32, which renormalizes to 2018-01-01 in the output of the program.
  • 2018-01-01 - 1 month is 2018-00-01, which renormalizes to 2017-12-01 in the output of the program.
  • 2017-12-31 + 1 day - 1 month is 2017-11-32, which renormalizes to 2017-12-02 in the output of the program.

As you can see when you renormalize at each step you don't get the same result as applying all of the changes in one go, because GNU date applying multiple changes in one go does not renormalize at each step.

Further reading

  • +1 for good examples but also for unrenormalized – Jeff Schaller Feb 9 '18 at 16:55
  • Awesome, Thanks for the clear, crisp example, If you ever write a book I would buy one. Thanks again! – Murali Rao Feb 12 '18 at 18:23

It seems to me, that date first goes back a month, and then adds the day, in this order.

If you change the order of the operations in the pipeline, you get the same result as with the other one.

$ echo 20171231 | xargs -i date -d "{} -1 month" | xargs -i date -d "{} +1 day"
Sat Dec  2 00:00:00 EET 2017

The thing is that going back a month from December 31st is problematic. The same date in the previous month would be November 31st, but November only has 30 days. In a sense, Nov 31 is the same as Dec 01 so giving the latter has some logic to it.

Of course another option would be to go from Dec 31 to Nov 30, but that's not entirely without issues either. Should Nov 30 - 1 month then be Oct 30 or Oct 31?

You may need to implement the required logic manually.


The reason is that this happens:

$ a=20171231; b=$(date -d "$a  -1 month"); echo "<$b>"
<Fri Dec  1 00:00:00 PST 2017>

The command date goes to the start of the month in december.
When you add 1 day, it goes to the day 2.

Instead, when the day is 1 of january it goes back a full month

$ a=20180101; b=$(date -d "$a -1 month"); echo "<$b>"
<Fri Dec  1 00:00:00 PST 2017>

That is an internal characteristic of date.

date goes back to the previous month with the same day number as given:

$ a=20171110; b=$(date -d "$a -1 month"); echo "<$b>"
<Tue Oct 10 00:00:00 PST 2017>

That is the given date in november goes back to october. But if the day is 31 it gets into trouble. For example from 31 of october there is no 31 of september, so it goes back to october 1:

$ a=20171031; b=$(date -d "$a -1 month"); echo "<$b>"
<Sun Oct  1 00:00:00 AST 2017>

Month shifts are exact only when the day is 01, February is a particularly odd month:

$ a=20170331; b=$(date -d "$a -1 month"); echo "<$b>"
<Fri Mar  3 00:00:00 AST 2017>

Because there is no 31 of February.

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