Days Between Dates?
Alert readers will know that I'm working on a major revision to my popular Wicked Cool Shell Scripts book to come out later this year. Although most of the scripts in this now ten-year-old book still are current and valuable, a few definitely are obsolete or have been supplanted by new technology or utilities. No worries—that's why I'm doing the update.
One script I'll be adding is a complicated one that I'm going to develop here in my Linux Journal column: daysago. The script will take a specified date in the past and tell you how many days have elapsed between that date and the current day and time.
You might be thinking that's fairly complicated, and it is, but not in the way you might be thinking. The actual calculation is really easy because of how Linux systems store and manipulate dates. The challenge is in parsing the input.
The first part of the book includes a library of useful scripting utilities, however, and one just so happens to be what we want—no coincidence that!
Valid Date?The easiest way to deal with something as complicated as a date is to force the work onto the user. There are a couple different strategies for that, but let's be lazy for now and prompt the user for the month, then day, then year, requiring numeric values. Then, we'll need to check whether it's valid.
Validating a user-specified date is pretty straightforward until we get to the issue of leap years. We're used to thinking that every four years is a leap year, but the formula is quite a bit more complicated than that, and it can be summarized with this set of rules:
-
Years divisible by four are leap years, unless...
-
The year also is divisible by 100, except if...
-
The year is divisible by 400, in which case it is.
Is that complicated enough? Of course, if we're just looking at leap years in the last few decades, it's not a very big deal, but it's inevitable that someone will try something like Feb 29, 1776, in which case we need to know whether it's valid.
Or, we can be lazy.
Since I like the lazy solution to things (remember, I'm not writing
production code here, I'm demonstrating concepts), let's cheat by
using the Linux cal
command. Because it lets us specify
month/year, we
can hand off the question of whether there's a February 29 in the year 1776
by just asking for a display of 2/1776:
$ cal 2 1776
February 1776
Su Mo Tu We Th Fr Sa
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29
It looks like 1776 was indeed a leap year. No wonder they had time to draft the Constitution before summer came along and made Philly too darn hot for anyone to work!
To turn this command into a script, a simple grep
and test for
nonzero results does the trick:
mon=$1; day=$2; year=$3
if [ $mon -eq 2 -a $day -eq 29 ] ; then
echo checking for feb 29 : was $3 a leap year?
leapyear=$(cal 2 $year | grep '29')
if [ ! -z "$leapyear" ] ; then
echo "Yes, $year was a leapyear, so February 29, $year \
is a valid date."
else
echo "Oops, $year wasn't a leapyear, so February only \
had 28 days."
fi
fi
Let's run a few quick tests to see what happens:
$ sh valid-date.sh 2 29 1777
checking for feb 29 : was 1777 a leap year?
Oops, 1777 wasn't a leapyear, so February only had 28 days.
$ sh valid-date.sh 2 29 1776
checking for feb 29 : was 1776 a leap year?
Yes, 1776 was a leapyear, so February 29, 1776 is a valid date.
That makes sense, and it's sure easy to use cal
for this
particular test.
We still need to encapsulate the "30 days have September, April, June and November" information too, and that's easily done with a rather compact case statement:
case $mon in
1|3|5|7|8|10|12 ) dim=31 ;; # most common value
4|6|9|11 ) dim=30 ;;
2 ) dim=29 ;; # possible leap year?
* ) dim=-1 ;; # unknown month
esac
In this case, the variable we're setting is "days in month" or
dim
(not a reference to A Clockwork
Orange, my cineophile readers).
This makes it easy to check all but Feb 29 as a possible date, as demonstrated
in this simple conditional:
if [ $day -lt 0 -o $day -gt $dim ] ; then
echo "Invalid date: Month #$mon has $dim days, so day \
$day is impossible."
fi
There are a bunch of different ways to do this, of course, but because most months have 31 days, again, I'm looking for the shortcut!
Mixed together and slightly tweaking the output, we now can test the validity of any date specified in the correct month, day, year format:
$ sh valid-date.sh 2 29 2013
The date you specified -- 2-29-2013 -- is valid. Continuing...
$ sh valid-date.sh 1 33 2013
Invalid date: Month #1 has 31 days, so day 33 is impossible.
$ sh valid-date.sh 2 29 2013
2013 wasn't a leapyear, so February only had 28 days.
Ahh, that all works just fine.
We started out by deciding that all the date formatting issues were going to be pushed to the user, but we still need to do some rudimentary tests, at least this one:
if [ $# -ne 3 ] ; then
echo "Usage: $(basename $0) mon day year"
echo " with just numerical values (ex: 7 7 1776)"
exit 1
fi
Yes, this month the script isn't glamorous—such is the life of a scripter.
With a valid date, there's a tendency to use something like GNU
date
to do the math (see the GNU
date
sidebar), but that has some inherent
limitations, not the least of which is that it won't work with any dates
prior to 1970.
I'll stop here for this month, but next month, we'll take the date we've validated and see if there's a formula to count the number of days quickly from then to now!
GNUdate
If you have GNU date on your system (try date --version
), the latter
part of this scripting project becomes crazy easy, because you easily
can calculate the number of seconds between Jan 1, 1970 and the specified date
subsequent. For example:
date '+%s' -d 2011-11-04
It's easy to subtract one date from another and divide by 86400 to convert seconds to days—for dates after Jan 1, 1970.