LJ Archive

Work the Shell

Days Between Dates: a Smarter Way

Dave Taylor

Issue #245, September 2014

How many days have elapsed? Our intrepid shell script programmer Dave Taylor looks at how to solve date-related calculations and demonstrates a script that calculates elapsed days between any specified day in the past and the current date.

In case you haven't been reading my last few columns or, perhaps, are working for the NSA and scanning this content to identify key phrases, like “back door” and “low-level vulnerability”, we're working on a shell script that calculates the number of days between a specified date in the past and the current date.

When last we scripted, the basic functionality was coded so that the script would calculate days from the beginning date to the end of that particular year, then the number of years until the current year (accounting for leap years), followed by the current number of days into the year. The problem is, it's not working.

Here's the state of things:

$ date
Mon Jul  7 09:14:37 PDT 2014
$ sh daysago.sh 7 4 2012
The date you specified -- 7-4-2012 -- is valid. Continuing...
0 days transpired between end of 2012 and beginning of this year
calculated 153 days left in the specified year
Calculated that 7/4/2012 was 341 days ago.

The script correctly ascertains that the current date, July 7, 2014, is 153 days into the year, but the rest of it's a hopeless muddle. Let's dig in to the code and see what's going on!

Two Versions of date = Not Good

The code in my last article was fairly convoluted in terms of calculating the number of days left in the starting year subsequent to the date specified (July 4, 2012, in the above example), but there's a far easier way, highlighted in this interaction:

$ date -j 0803120010
Tue Aug  3 12:00:00 PDT 2010
$ date -j 0803120010 +%j

In other words, modern date commands let you specify a date (in MON DAY HOUR MIN YEAR format) and then use the %j format notation to get the day-of-the-year for that specific date. August 3, 2010, was the 215th day of the year.

Did you try that, and find that date complained about the -j flag? Good. That means you're likely using GNU date, which is far superior and is actually something we'll need for the full script to work. Test which version you have by using the --version flag:

$ date --version
date (GNU coreutils) 8.4
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Written by David MacKenzie.

How many days were in a given year? That's also easily done with a shortcut, checking the day of the year of December 31st. For example:

$ date -d 12/31/2010 +%j

But, 2012 was a leap year. So:

$ date -d 12/31/2012 +%j

Therefore the days-left-in-year calculation is simply days-in-year – day-of-the-year.

The next calculation is days/year * years between the specified date and the current year.

Days Left in Year

The first step of calculating the days left in the starting year is to create the correct date format string for the date command. Fortunately, with GNU date, it's easily done:

# build date string format for the specified starting date
calculate="$(date -d "12/31/$startyear" +%j) - $(date -d 
 ↪$startdatefmt +%j)"
echo "Calculating $calculate"
daysleftinyear=$(( $calculate ))

When run as part of the script, the debugging echo statement offers useful information to help debug things:

$ sh daysago.sh 2 4 2012
The date you specified -- 2-4-2012 -- is valid. Continuing...
Calculating 366 - 035
There were 337 days left in the starting year
$ sh daysago.sh 2 4 2013
The date you specified -- 2-4-2013 -- is valid. Continuing...
Calculating 365 - 035
There were 336 days left in the starting year

Notice that when we specified Feb 4, 2012, a leap year, there are 337 days left in the year, but when we specify the same date on the following non-leapyear, there are 336 days. Sounds right!

Days in Intervening Years

The next portion of the calculation is to calculate the number of days in each year between the start year and the current year, not counting either of those. Sounds like a while loop to me, so let's do this:

tempyear=$(( $startyear + 1 ))
while [ $tempyear -lt $thisyear ] ; do
  echo "intervening year: $tempyear"
  daysbetweenyears=$(( $daysbetweenyears + $(date -d 
   ↪"12/31/$tempyear" +%j) ))
  tempyear=$(( $tempyear + 1 ))
echo "Intervening days in years between $startyear and 
 ↪$thisyear = $daysbetweenyears"

Again, I'm adding a debugging echo statement to clarify what's going on, but we're getting pretty close:

$ sh daysago.sh 2 4 2010
The date you specified -- 2-4-2010 -- is valid. Continuing...
Calculating 365 - 035
Calculated that there were 336 days left in the starting year
intervening year: 2011
intervening year: 2012
intervening year: 2013
intervening days in years between 2010 and 2014 = 1096

That seems to pass the reasonable test, as there are three years between 2010 and 2014 (namely 2011, 2012 and 2013) and the back-of-envelope calculation of 3*365 = 1095.

If you're using non-GNU date, you already have realized that the string format is different and that you need to use the -j flag instead of the -d flag. The problem is, the older date command also works differently, because 1969 is the beginning of Epoch time. Look:

$ date -j 0204120060  # last two digits are year, so '60
Wed Feb  4 12:00:00 PST 2060

It interprets the two-digit “60” as 2060, not 1960. Boo!

If you're not running GNU date, you're going to have a really big problem for dates prior to 1969, and I'm going to just say “get GNU date, dude” to solve it.

Total Days That Have Passed

We have everything we need to do the math now. We can calculate the number of days left in the start year, the number of days in intervening years, and the day-of-year number of the current date. Let's put it all together:

dayofyear=$(date +%j)              # that's easy!
totaldays=$(( $daysleftinyear + $daysbetweenyears + $dayofyear ))
echo "$totaldays days have elapsed between 
 ↪$startmon/$startday/$startyear and today, 
 ↪day $dayofyear of $thisyear."

That's it. Now, stripping out the debug echo statements, here's what we can ascertain:

$ sh daysago.sh 2 4 1949
23900 days have elapsed between 2/4/1949 and today, 
 ↪day 188 of 2014.
$ sh daysago.sh 2 4 1998
6003 days have elapsed between 2/4/1998 and today, 
 ↪day 188 of 2014.
$ sh daysago.sh 2 4 2013
524 days have elapsed between 2/4/2013 and today, 
 ↪day 188 of 2014.

But look, there's still a lurking problem when it's the same year that we're calculating:

$ sh daysago.sh 2 4 2014
524 days have elapsed between 2/4/2014 and today, 
 ↪day 188 of 2014.

That's a pretty easy edge case to debug, however, so I'm going to wrap things up here and let you enjoy trying to figure out what's not quite right in the resultant script.

Stumped? Send me an e-mail via www.linuxjournal.com/contact.

Dave Taylor has been hacking shell scripts for more than 30 years. Really. He's the author of the popular Wicked Cool Shell Scripts and can be found on Twitter as @DaveTaylor and more generally at his tech site www.AskDaveTaylor.com.

LJ Archive