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.

 

MENU:

 
SOFTWARE DEVELOPMENT:
    • EXPERIENCE
PRODUCTS:
UNIX: 

   • UNIX TUTORIALS

LEGACY SYSTEMS:

    • LEARN COBOL
    • PRODUCTS
    • GEN-CODE
    • COMPILERS   

INTERNET:
    • CYBERSUITE   
WINDOWS:

    • PRODUCTS


Search Now:
 
In Association with Amazon.com

Copyright©2001 King Computer Services Inc. All rights reserved.