Days Between Dates: the Counting
In my last article, we began an exploration of date math by validating a given date specified by the user, then explored how GNU date offers some slick math capabilities, but has some inherent limitations, the most notable of which is that it isn't on 100% of all Linux and UNIX systems.
So here, let's continue the development by adding the date math itself. For this script, the goal is to answer the question of how many days have transpired between the specified date and the current date. The necessary algorithm has obvious applications in the broader "date A to date B" puzzle, but let's defer that for another time.
Remember also that we're talking about dates, not time, so it's not counting 24hour increments but simply the number of days. So running the completed script at 11:59pm will offer a different result from at 12:01am, just a few seconds later.
To start, let's grab the current month, day and year. There's a neat
eval
trick we can use to do this:
eval $(date "+thismon=%m;thisday=%d;thisyear=%Y;dayofyear=%j")
This instantiates thismon
to the current month,
thisday
to the current day of the month, thisyear
to the current year and
dayofyear
to the numeric day number into the year of the current
day, all in a single line—neat.
The userspecified starting date already is broken down into day of month, month of year and year, so once the current month, day and year are identified, there are four parts to the equation:

How many years have transpired * 365.

How many days were left in the starting year.

How many days have transpired since the beginning of this year.

Compensate for leap years in the interim period.
The first test demonstrates the nuances in this calculation because if we're calculating the number of days since, say, Nov 15, 1996, and today, Jun 3, 2014, we don't want to count 365*(2014–1996) because both the start and end years will be wrong. In fact, better math is to use this formula:
365 * (thisyear – starting year – 2)
But, that's not right either. What happens if the starting date is Jun 1, 2014, and the end date is Jun 3, 2014? That should then produce a zero value, as it also would if the start date was Mar 1, 2013, even though 2014–2013=1. Here's my first stab at this chunk of code:
if [ $(( $thisyear  $startyear )) gt 2 ] ; then
elapsed=$(( $thisyear  $startyear  2 ))
basedays=$(( elapsed * 365 ))
else
basedays=0
fi
echo "$basedays days transpired between end of $startyear \
and beginning of this year"
Now that isn't taking into account leap years, is it? So instead,
let's try doing it differently to tap into the isleap
function
too:
if [ $(( $thisyear  $startyear )) gt 2 ] ; then
# loop from year to year, counting years and adding +1
# for leaps as needed
theyear=$startyear
while [ $theyear ne $thisyear ] ; do
isleap $theyear
if [ n "$leapyear" ] ; then
elapsed=$(( $elapsed + 1 ))
echo "(adding 1 day to account for $theyear being a leap)"
fi
elapsed=$(( $elapsed + 365 ))
theyear=$(( $theyear + 1 ))
done
fi
Fast and easy. When I run this block against 1993 as a starting year, it informs me:
(adding 1 day to account for 1996 being a leap year)
(adding 1 day to account for 2000 being a leap year)
(adding 1 day to account for 2004 being a leap year)
(adding 1 day to account for 2008 being a leap year)
(adding 1 day to account for 2012 being a leap year)
7670 days transpired between end of 1993 and beginning of this year
For the second step in the algorithm, calculating the number of days from the specified starting date to the end of that particular year, well, that too has an edge case of it being the current year. If not, it's a calculation that can be done by summing up the days of each of the previous months plus the number of days into the month of the starting date, then subtracting that from the total days in that particular year (since we have to factor in leap years, which means we have to consider whether the date occurs before or after Feb 29), or we can do the same basic equation, but sum up the days after the specified date. In the latter case, we can be smart about the leap day by tracking whether February is included.
The basic code assumes we have an array of dayspermonth for each of the 12 months or some other way to calculate that value. In fact, the original script included this snippet:
case $mon in
135781012 ) dim=31 ;; # most common value
46911 ) dim=30 ;;
2 ) dim=29 ;; # is it a leap year?
* ) dim=1 ;; # unknown month
esac
A logical approach would be to turn this into a short function that can do
double duty. That's easily done by wrapping it in function
daysInMonth {
and }
.
With that, it's a matter of stepping through the remaining months, although be alert for the leap year calculation we need to do if month = 2 (Feb). The program has February always having 29 days, so if it isn't a leap year, we need to subtract one day to compensate:
if [ $thisyear ne $startyear ] ; then
monthsleft=$(( $startmon + 1 ))
daysleftinyear=0
while [ $monthsleft le 12 ] ; do
if [ $monthsleft eq 2 ] ; then # February. leapyear?
isleap $startyear
if [ n "$leapyear" ] ; then
daysleftinyear=$(( $daysleftinyear + 1 )) # feb 29!
fi
fi
daysInMonth $monthsleft
daysleftinyear=$(( $daysleftinyear + $dim ))
monthsleft=$(( $monthsleft + 1 ))
done
else
daysleftinyear=0 # same year so no calculation needed
fi
The last part is to calculate how many days are left in the starting date's month, again worrying about those pesky leap years. This is only necessary if the start year is different from the current year and the start month is different from the current month. In the case that we're in the same year, as you'll see, we can use "day of year" and calculate things differently.
Here's the block of code:
if [ $startyear ne $thisyear a $startmon ne $thismon ] ; then
daysInMonth $startmon
if [ $startmon eq 2 ] ; then # edge case: February
isleap $startyear
if [ z "$leapyear" ] ; then
dim=$(( $dim  1 )) # dim = days in month
fi
fi
daysleftinmon=$(( $dim  $startday ))
echo "calculated $daysleftinmon days left in the startmon"
fi
We have a few useful variables that now need to be added to the
"elapsed" variable:
daysleftinyear
is how many days were
left in the start year, and dayofyear
is the current day number in
the current year (June 3, for example, is day 154).
For clarity, I add it like this:
echo calculated $daysleftinyear days left in the specified year
elapsed=$(( $elapsed + daysleftinyear ))
# and, finally, the number of days into the current year
elapsed=$(( $elapsed + $dayofyear ))
echo "Calculated that $startmon/$startday/$startyear \
was $elapsed days ago."
With that, let's test the script with a few different inputs:
$ sh daysago.sh 8 3 1980
The date you specified  831980  is valid. Continuing...
12419 days transpired between end of 1980 and beginning of this year
calculated 28 days left in the startmon
calculated 122 days left in the specified year
Calculated that 8/3/1980 was 12695 days ago.
$ sh daysago.sh 6 3 2004
The date you specified  632004  is valid. Continuing...
3653 days transpired between end of 2004 and beginning of this year
calculated 184 days left in the specified year
Calculated that 6/3/2004 was 3991 days ago.
Hmm...365*10 = 3650. Add a few days for the leap year, and that seems wrong, doesn't it? Like it's one year too many or something? Worse, look what happens if I go back exactly two years ago:
$ sh daysago.sh 6 3 2012
The date you specified  632012  is valid. Continuing...
0 days transpired between end of 2012 and beginning of this year
calculated 184 days left in the specified year
Calculated that 6/3/2012 was 338 days ago.
Something is definitely wrong. That should be 2*365. But it's not. Bah. Phooey. In my next article, we'll dig in and try to figure out what's wrong!
Dave Taylor has been hacking shell scripts for over thirty 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 www.DaveTaylorOnline.com.
Trending Topics
Enterprise Linux
Linux Journal Ceases Publication  Dec 01, 2017 
So Long, and Thanks for All the Bash  Dec 01, 2017 
Banana Backups  Nov 21, 2017 
Zentera Systems, Inc.'s CoIP Security Enclave  Nov 15, 2017 
Sysadmin 101: Patch Management  Nov 14, 2017 
pfSense: Not Linux, Not Bad  Nov 13, 2017 
 Understanding Firewalld in MultiZone Configurations
 The Weather Outside Is Frightful (Or Is It?)
 Simple Server Hardening
 From vs. to + for Microsoft and Linux
 SNMP
 Bash Shell Script: Building a Better March Madness Bracket
 IGEL Universal Desktop Converter
 Server Technology's HDOT AltPhase Switched POPS PDU
 Linux Journal Ceases Publication
 Teradici's Cloud Access Platform: "Plug & Play" Cloud for the Enterprise