How to Validating Date Formats using shell script

One of the most challenging validation tasks, but one that's crucial for shell scripts that work with dates, is to ensure that a specific date is actually possible. If we ignore leap years, this task is not too bad, because the calendar is well behaved and consistent each year. All we need in that case is a table with the days of each month against which to compare a specified date. To take leap years into account, you have to add some additional logic to the script. One set of rules for calculating a leap year is as follows:

Years not divisible by 4 are not leap years.

Years divisible by 4 and by 400 are leap years.

Years divisible by 4, not divisible by 400, and divisible by 100, are not leap years.

All other years divisible by 4 are leap years.

Notice how this script utilizes normdate (Script #3) to ensure a consistent date format before proceeding.

The Code
#!/bin/sh
# valid-date -- Validates a date, taking into account leap year rules.

exceedsDaysInMonth()
{
# Given a month name, return 0 if the specified day value is
# less than or equal to the max days in the month; 1 otherwise

case $(echo $1|tr '[:upper:]' '[:lower:]') in
jan* ) days=31 ;; feb* ) days=28 ;;
mar* ) days=31 ;; apr* ) days=30 ;;
may* ) days=31 ;; jun* ) days=30 ;;
jul* ) days=31 ;; aug* ) days=31 ;;
sep* ) days=30 ;; oct* ) days=31 ;;
nov* ) days=30 ;; dec* ) days=31 ;;
* ) echo "$0: Unknown month name $1" >&2; exit 1
esac

if [ $2 -lt 1 -o $2 -gt $days ] ; then
return 1
else
return 0 # the day number is valid
fi
}

isLeapYear()
{
# This function returns 0 if a leap year; 1 otherwise.
# The formula for checking whether a year is a leap year is:
# 1. Years not divisible by 4 are not leap years.
# 2. Years divisible by 4 and by 400 are leap years.
# 3. Years divisible by 4, not divisible by 400, and divisible by 100,
# are not leap years.
# 4. All other years divisible by 4 are leap years.

year=$1
if [ "$((year % 4))" -ne 0 ] ; then
return 1 # nope, not a leap year
elif [ "$((year % 400))" -eq 0 ] ; then
return 0 # yes, it's a leap year
elif [ "$((year % 100))" -eq 0 ] ; then
return 1
else
return 0
fi
}

## Begin main script

if [ $# -ne 3 ] ; then
echo "Usage: $0 month day year" >&2
echo "Typical input formats are August 3 1962 and 8 3 2002" >&2
exit 1
fi

# Normalize date and split back out returned values

newdate="$(normdate "$@")"

if [ $? -eq 1 ] ; then
exit 1 # error condition already reported by normdate
fi

month="$(echo $newdate | cut -d\ -f1)"
day="$(echo $newdate | cut -d\ -f2)"
year="$(echo $newdate | cut -d\ -f3)"

# Now that we have a normalized date, let's check to see if the
# day value is logical

if ! exceedsDaysInMonth $month "$2" ; then
if [ "$month" = "Feb" -a "$2" -eq "29" ] ; then
if ! isLeapYear $3 ; then
echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2
exit 1
fi
else
echo "$0: bad day value: $month doesn't have $2 days" >&2
exit 1
fi
fi

echo "Valid date: $newdate"

exit 0

Running the Script
To run the script, simply specify a date on the command line, in "month day year" format. The month can be a three-letter abbreviation, a full word, or a numeric value; the year must be four digits.

The Results
$ valid-date august 3 1960
Valid date: Aug 3 1960
$ valid-date 9 31 2001
valid-date: bad day value: Sep doesn't have 31 days
$ valid-date feb 29 2004
Valid date: Feb 29 2004
$ valid-date feb 29 2006
valid-date: 2006 is not a leap year, so Feb doesn't have 29 days

Hacking the Script
A roughly similar approach to this script could validate time specifications, either using a 24-hour clock or with an ante meridiem/post meridiem (am/pm) suffix. Split the value at the colon, ensure that the minutes and seconds (if specified) are between 0 and 60, and then check that the first value is between 0 and 12 if allowing am/pm, or between 0 and 24 if you prefer a 24-hour clock. (Fortunately, while there are leap seconds and other tiny variations in time to help keep the calendar balanced, we can safely ignore them on a day-to-day basis.)