The problem with relative time calculations in bash date strings

Today I was writing a bash script that needed to reference a time in the past relative to the current UTC date.

For instance, if today is March 31st, 2015 UTC, what month was it exactly one month ago?

Answer: February

However, I found it was less than trivial to actually get the correct answer using a relative time calculation with date.

For instance, you would think that the result of $ date +%B -d "1 month ago" would be February, but the actual result is March, which is still the month we are currently in! Definitely not the result I was looking for.

Buried in the GNU coreutils manual there was an explanation for this behavior.

The fuzz in units can cause problems with relative items. For example, โ€˜2003-07-31 -1 monthโ€™ might evaluate to 2003-07-01, because 2003-06-31 is an invalid date. To determine the previous month more reliably, you can ask for the month before the 15th of the current month.

I understand why there could be a problem if you were starting with an invalid date, but I was surprised to learn that a clever workaround is still required if the result is an invalid date.

Shouldn’t the logic just be NOT to return an invalid result based on the context of the output? If we only want to return the month, not the date, one would think the context of the calculation would change. This is “relative” time after all. In my opinion this is a total bug and should be fixed – not merely passed off as a necessary annoyance.

Anyway, the workaround suggested is to always use the 15th of the current month as a starting point constant before performing the calculation. This way you avoid the date gap problem since '1 month' == '28 days' and there are four different month lengths possible in the Gregorian calendar (28, 29, 30 and 31).

So maintaining our above example, if today is March 31st, 2015 UTC, what month was it exactly one month ago?

$ date +%B -d "$(date +%Y-%m-15) - 1 month" will result in February, which is correct.

Did you find this helpful? Let me know in the comments!

NOTE: This date command was intended for use on Linux and won’t work on OS X or other BSD-based systems.

6 Comments

Add yours →

  1. Oh nice find! It’s a tricky one, as it depends on your intention for “one month ago”.

    If it’s March 31st, “one month ago” would be February 31st, which doesn’t exist. Totally legitimate answer. But if you just meant “closest date last month”, then March 28, 29, 30, and 31 would all return February 28 (3 out of 4 years).

    And in some applications, you’re actually wanting to know the previous month, regardless of the actual date. Which I guess is what you were wanting here.

    I don’t know if I think it’s a bug or not, but it’s definitely something to be conscious of when working with dates in Bash!

    • Yes! I agree with your example, but only if I was outputting a date in the format, like %d or something.

      Since I’m using a return format of %B, which is a month name, one could assume the relative time would be calculated in a monthly context.

      What is “relative” doesn’t take into account the context of the output, which to me seems illogical – but still just an opinion ๐Ÿ˜›

  2. Is it actually equating a month to 30 days? That seems silly. I’d be more suspect if it’s just outputting the previous month name. In the case where the current date is past the end of the month, this results in an invalid date and thus the error.

    This should happen for July as well if you calculated it on July 31st since June only has 30 days. Your original function probably results in “June 31, yyyy” which it then realizes isn’t a date.

    Dates are such a difficult thing to deal with (look at all the date/time related classes in PHP) that programmers often cut corners. I’ve seen many instances of date guessing get into these oddball cases. But that’s what happens when you take a date-first, format-later approach.

    • Hey Ryan – Sorry, I was wrong about that, 1 month actually equates to 28 days.

      If it was July 31st and you said “1 month ago” the output would actually be July 3rd. Here’s an example in March: http://note.io/1xT331c

      The problem is that if you want to do this in the context of only outputting the month, you get the same month name back.

      Working with dates is such a PITA.

  3. “For instance, if today is March 31st, 2015 UTC, what month was it exactly one month ago?”

    Your answer:
    $ date +%B -d “$(date +%Y-%m-15) – 1 month”

    I read this as you are trying to get the previous month; but I might be wrong.

    ..but why use day 15 in the month?
    Yes, I understand day 15 exist in all months; but so does 1 to 28

    I think I would go for something like this:

    $ date
    Wed Mar 9 22:26:15 CET 2016

    # Previous month
    $ date +%B -d “$(date +%Y-%m-01) – 1 month”
    February

    # First date in previous month
    $ date +%Y-%m-%d -d “$(date +%Y-%m-01) – 1 month”
    2016-02-01

    #Last date in previous month
    $ date +%Y-%m-%d -d “$(date +%Y-%m-01) – 0 month – 1 day”
    2016-02-29

Leave a Reply

%d bloggers like this: