|
![]() Subtracting datesCalculating the difference between two dates |
This month, Mo answers his readers' requests for the tools needed to calculate the number of days between two separate dates. This follows his three-part series on adding and subtracting days from a given date, which concluded last month. (2200 Words)
![]() Mail this article to a friend |
Below, I have reproduced three scripts (with slight improvements) that were developed as part of the series. For the theory and explanations behind these, I refer you to Part 1 and Part 2 of the series on date math.
Listing 1, yeardays, is a script that takes a single argument of a 4-digit year and outputs the number of days in the year by using leap year logic. Note that the scripts are numbered for the sake of explanation, though the numbers are not part of the script.
1 #!/bin/ksh 2 # yeardays 3 # return the number of days in a year 4 # usage yeardays yyyy 5 6 "# if there is no argument on the command line, then assume that a" 7 # yyyy is being piped in 8 9 if [ $# = 0 ] 10 then 11 read y 12 else 13 y=$1 14 fi 15 16 # a year is a leap year if it is even divisible by 4 17 # but not evenly divisible by 100 18 # unless it is evenly divisible by 400 19 20 # if it is evenly divisible by 400 it must be a leap year 21 a=`expr $y % 400` 22 if [ $a = 0 ] 23 then 24 echo 366 25 exit 26 fi 27 28 #if it is evenly divisible by 100 it must not be a leap year 29 a=`expr $y % 100` 30 if [ $a = 0 ] 31 then 32 echo 365 33 exit 34 fi 35 36 # if it is evenly divisible by 4 it must be a leap year 37 a=`expr $y % 4` 38 if [ $a = 0 ] 39 then 40 echo 366 41 exit 42 fi 43 44 # otherwise it is not a leap year 45 echo 365
Listing 2, monthdays, can be invoked with one or two arguments. If a single argument is provided, it should be a date in YYYYMMDD format. If two arguments are provided, they are assumed to be a 4-digit year and a 1- or 2-digit month.
In either case, the script calculates the number of days in the month that has been provided as part of a date or as a separate argument on the command line, and then outputs that number. This uses the 30-days-hath- September rule and leap year logic (both yeardays and monthdays appeared in Part 1 of the series):
1 # monthdays 2 3 # calculates the number of days in a month 4 # usage monthdays yyyy mm 5 # or monthdays yyyymmdd 6 7 # if there are no command line arguments then assume that a yyyymmdd is being 8 # piped in and read the value. 9 # if there is only one argument assume it is a yyyymmdd on the command line 10 # other wise it is a yyyy and mm on the command line 11 12 if [ $# = 0 ] 13 then 14 read ymd 15 elif [ $# = 1 ] 16 then 17 ymd=$1 18 else 19 ymd=`expr \( $1 \* 10000 \) + \( $2 \* 100 \) + 1` 20 fi 21 22 # extract the year and the month 23 24 y=`expr $ymd / 10000` ; 25 m=`expr \( $ymd % 10000 \) / 100` ; 26 27 # 30 days hath september etc. 28 case $m in 29 1|3|5|7|8|10|12) echo 31 ; exit ;; 30 4|6|9|11) echo 30 ; exit ;; 31 *) ;; 32 esac 33 34 # except for month 2 which depends on whether the year is a leap year 35 # Use yeardays to get the number of days in the year and return a value 36 # accordingly. 37 diy=`yeardays $y` 38 39 case $diy in 40 365) echo 28 ; exit ;; 41 366) echo 29 ; exit ;; 42 esac
The third listing, ymd2yd, converts a date in YYYYMMDD (Gregorian format) to YYYYDDD (Julian format). Please see Part 2 for details on this listing, explanation of these two formats, and some information on why they are called Gregorian and Julian formats even though they both represent dates in the Gregorian Calendar.
1 #!/bin/ksh 2 # ymd2yd converts yyyymmdd to yyyyddd 3 # usage ymd2yd 19980429 4 5 "# if there is no command line argument, then assume that the date" 6 # is coming in on a pipe and use read to collect it 7 8 if [ $# = 0 ] 9 then 10 read dt 11 else 12 dt=$1 13 fi 14 15 "# break the yyyymmdd into separate parts for year, month and day" 16 17 y=`expr $dt / 10000` 18 m=`expr \( $dt % 10000 \) / 100` 19 d=`expr $dt % 100` 20 21 # add the days in each month up to (but not including the month itself) 22 # into the days. For example if the date is 19980203 then extract the 23 "# number of days in January and add it to 03. If the date is June 14, 1998" 24 "# then extract the number of days in January, February, March, April and May" 25 # and add them to 14. 26 27 x=1 28 while [ `expr $x \< $m` = 1 ] 29 do 30 md=`monthdays $y $x` 31 d=`expr $d + $md` 32 x=`expr $x \+ 1` 33 done 34 35 # combine the year and day back together again and you have the julian date. 36 37 jul=`expr \( $y \* 1000 \) + $d` 38 echo $jul
|
|
|
|
The Julian way
The Julian format is much more convenient for date math. The Julian format uses a year and a 3-digit ordinal day of the year; January 1, 1998 becomes 1998001
and February 3rd, 1998 becomes 1998034. The conversion involves adding up the
number of days in all the months within the date except the month of the date
itself, plus the number of days in the month. For example to calculate the
Julian day for February 2, 1998, add up all the days in January plus the two
days in February: 31 + 2 = 33. Similarly, the Julian day for April 7, 1997
would be the sum of all the days in January, February and March, plus the 7
days in April: 31 + 28 + 31 + 7 = 97.
This format happens to be well suited to date math, and day difference is no exception. Using the scripts created in the previous series, it is possible to create quickly a script that calculates the day difference in two dates.
At lines 8 through 15 a function is defined that is used to display the usage information on the shell script on the screen. Note that these lines define the function but do not execute it. The script is supposed to be invoked with two Julian formatted date arguments as in:
juldif 1998001 1997023
At line 17, the argument count is tested to ensure that there are two arguments. If there are not, the usage function is called to display usage information and the script exits. At lines 26 through 43, the two arguments are extracted into the variables $jul1 and $jul2 with one little twist. The logic of the script is designed to calculate the difference by subtracting the second date argument from the first, assuming that the second argument is the earlier date. If this is not the case, the dates are loaded in reverse. Argument 1 is placed in jul2 and argument2 in jul1. This reversal is rectified later in the script.
We now have the larger date in $jul1 and the smaller date in $jul2. These are both broken into the 4-digit year and 3-digit day parts of the dates at lines 35 through 39 to create the variables $yyyy1, $yyyy2, $ddd2 and $ddd2.
The $ddd2 value is subtracted from $ddd1 and the result is stored in $res at line 42. From lines 44 through 50, a loop checks if $yyyy2 is less than $yyyy1. If it is, the yeardays script is called to extract the number of days in $yyyy2 and those days are added to $res. The $yyyy2 variable is incremented by 1 and the loop continues until $yyyy2 catches up to $yyyy1. The result of this loop plus the original calculation of $ddd1 - $ddd2, which was stored in $res as a first step, is the answer.
There is one final adjustment needed at lines 55 through 58, which rechecks the command line arguments. And if $1 was less than $2, the sign of $res is reversed by multiplying it by -1.
1 #!/bin/ksh 2 # juldif 3 # calculates the days difference between two dates and reports 4 # the number days as jul1 - jul2 5 # usage juldif jul1 jul2 6 # where julian date is in the form yyyyddd 7 8 usage () { 9 echo "Usage:" 10 echo " juldif jul1 jul2" 11 echo "" 12 echo " Calculates the day difference between" 13 echo " two julian dates (jul1 -jul2)" 14 echo " where a julian date is in the form of yyyyddd." 15 } 16 17 if [ $# != 2 ] 18 then 19 usage 20 exit 21 fi 22 23 # This process subtracts arg2 from arg1. If arg2 is larger 24 # then reverse the arguments. The calculations are done, and 25 # then the sign is reversed 26 if [ `expr $1 \< $2` = 1 ] 27 then 28 jul1=$2 29 jul2=$1 30 else 31 jul1=$1 32 ul2=$2 33 fi 34 35 # Break the dates in to year and day portions 36 yyyy1=`expr $jul1 / 1000` 37 yyyy2=`expr $jul2 / 1000` 38 ddd1=`expr $jul1 % 1000` 39 ddd2=`expr $jul2 % 1000` 40 41 # Subtract days 42 res=`expr $ddd1 - $ddd2` 43 44 # Then add days in year until year2 matches year1 45 while [ `expr $yyyy2 \< $yyyy1` = 1 ] 46 do 47 diy=`yeardays $yyyy2` 48 res=`expr $res + $diy` 49 yyyy2=`expr $yyyy2 + 1` 50 one 51 52 # if argument 2 was larger than argument 1 then 53 # the arguments were reversed before calculating 54 # adjust by reversing the sign 55 if [ `expr $1 \< $2` = 1 ] 56 then 57 res=`expr $res \* -1` 58 fi 59 60 # and output the results 61 echo $res
Listing 5 is really a wrapper around juldif that will accept two Gregorian formatted date arguments. This script uses ymd2yd to translate both of the arguments to Julian format dates and then calls juldif for the results.
1 #!/bin/ksh 2 # grgdif 3 # calculates the days difference between two dates and reports 4 # the number days as grg1 - grg2 5 # usage grgdif grg1 grg2 6 # where gregorian date is in the form yyyymmdd 7 8 usage () { 9 echo "Usage:" 10 echo " grgdif grg1 grg2" 11 echo "" 12 echo " Calculate day difference between" 13 echo " two gregorian dates (grg1 - grg2)" 14 echo " where a gregorian date is in the form of yyyymmdd." 15 } 16 17 if [ $# != 2 ] 18 then 19 usage 20 exit 21 fi 22 # convert each date to julian 23 grg1=$1 24 grg2=$2 25 jul1=`ymd2yd $grg1` 26 jul2=`ymd2yd $grg2` 27 28 # calculate the answer using juldif 29 res=`juldif $jul1 $jul2` 30 31 # and output the results 32 echo $res
You can test this set of scripts by using grgdif to calculate day differences, particularly working in and around leap years. Also, check reversed dates and the same date.
grgdif 19960101 19950101 365 grgdif 19950101 19960101 -365 grgdif 20010101 19960101 1827 grgdif 19980101 19980101 0
With this and the previous series, you should be able to handle just about any math problem with dates.
Once again, I repeat my caveat from my previous columns on this subject. The methods developed in the shell scripts in this article and the previous series are not suited to calculating large day differences -- they are too slow. Limit their use to only a few years.
|
Resources
About the author
Mo Budlong, president of King Computer Services Inc.,
specializes in Unix and client/server consulting and training and
currently publishes the COBOL Just In Time Course, a crash
course for the year 2000 problem as well as COBOL Dates and the
Year 2000, which offers date solutions.
Reach Mo at mo.budlong@sunworld.com.
If you have technical problems with this magazine, contact webmaster@sunworld.com
URL: http://www.sunworld.com/swol-02-1999/swol-02-unix101.html
Last modified: Wednesday, February 03, 1999