Subtracting dates
Calculating the difference between two dates
Summary
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)
In the three-part series that concluded last month I created a series of shell
scripts that could be used to add or subtract days to a date and produce the
new date as a result of the addition. That series immediately met with requests for a method of calculating the number of days between two dates. Fortunately, several of the scripts created in the previous series can be used to help solve this day difference problem.
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.
Contact
us for a free consultation. |