Date arithmetic, Part 2

More details on how to calculate yesterday's date

Summary
Last month, Mo introduced the subject of date arithmetic and the problem of adding (or subtracting) days to a given date then calculating the new date. Moving on from his discussion of leap years and leap months, Mo tackles the problem of switching date formats and teaches you some actual date arithmetic. (2,400 words)


With the leap year and leap month problems resolved, we now return to our date-calculation problem. The first step in a calculation is converting an 8-digit Gregorian format date to the erroneously named Julian date format (for a brush up on this topic, see last month's article). The Julian format uses a year and a 3-digit ordinal day of the year. January 1, 1998 becomes 1998001, and February 3, 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.

The ymd2yd script below converts a YYYYMMDD format date to a YYYYDDD format. After the usual argument handling and separating the date into year, month, and day components, the main logic begins at lines 26 to 32. A variable is set to 1 and steps through each month up to but not including the month in question. The monthdays script is then called for each month, passing it the year and month to evaluate the days in the month. Next, the days are added to the $d variable, which already holds the number of days in the current month. Finally, the resulting number of days is recombined with the year to create the Julian date.

 1	# ymd2yd converts yyyymmdd to yyyyddd
 2	# usage ymd2yd 19980429
 3	
 4	# if there is no command line argument, assume the date
 5	# is coming in on a pipe and use read to collect it
 6	
 7	if [ X$1 = X ]
 8	then
 9		read  dt
10	else
11		dt=$1
12	fi
13	
14	# break the yyyymmdd into separate parts for year, month, and day
15	
16	y=`expr $dt / 10000`
17	m=`expr \( $dt % 10000 \) / 100`
18	d=`expr $dt % 100`
19	
20	# add the days in each month, up to but not including the month itself,
21	# into the days. For example, if the date is 19980203, extract the
22	# number of days in January and add it to 03. If the date is June 14, 1998,
23	# extract the number of days in January, February, March, April, and May
24	# and add them to 14.
25	
26	x=1
27	while [ `expr $x \< $m` = 1 ]
28	do
29		md=`monthdays $y $x`
30		d=`expr $d + $md`
31		x=`expr $x \+ 1`
32	done
33	
34	# combine the year and day back together again and you have the julian date.
35	
36	jul=`expr \( $y \* 1000 \) + $d`
37	echo $jul
38	

Of course, if an 8-digit date is to be converted to a Julian date for arithmetic, it will have to be converted back to an 8-digit date after the calculation is done.

The yd2ymd script below reverses the conversions of the ymd2yd script. After the standard logic for receiving input from the command line or a pipe at lines 7 through 12, the resulting date is broken into components of year and days at lines 16 and 17. The key calculation logic is at lines 19 through 34. This logic will subtract the number of days in each month, starting from 1, from the days in the date. When the resulting day goes below 1, we've reached the current month. Finally, add back the number of days in the month to get the correct day of the month. At lines 36 through 38, the results are reassembled into a YYYYMMDD format and echoed to standard output.

 1	# yd2ymd converts yyyyddd to yyyymmdd
 2	# usage yd2ymd 1998213
 3	
 4	# if there is no command line argument, assume one is being 
 5	# piped in and read it
 6	
 7	if [ X$1 = X ]
 8	then
 9	    read dt
10	else
11	    dt=$1
12	fi
13	
14	# break apart the year and the days
15	
16	y=`expr $dt / 1000`
17	d=`expr $dt % 1000`
18	
19	# subtract the number of days in each month starting from 1
20	# from the days in the date. When the day goes below 1, you
21	# have the current month. Add back the number of days in the
22	# month to get the correct day of the month
23	m=1
24	while [ `expr $d \> 0` = 1 ]
25	do
26	    md=`monthdays $y $m`
27	    d=`expr $d \- $md`
28	    m=`expr $m \+ 1`
29	done
30	
31	d=`expr $d \+ $md`
32	
33	# the loop steps one past the correct month, so back up the month
34	m=`expr $m \- 1`
35	
36	# assemble the results into a gregorian date
37	grg=`expr \( $y \* 10000 \) \+ \( $m \* 100 \) \+ $d`
38	echo $grg
39	

Now the arithmetic
We're finally ready to perform some actual date arithmetic. The ydadd script below allows a positive or negative number of days to be added to a date in yyyyddd format. Adding a negative number has the effect of subtracting a number of days from the date. The ydadd script can be called with a 7-digit date and a number of days (positive or negative) to add on the command line, or the 7-digit date can be read from standard input. The logic to sort out the arguments appears at lines 4 through 14. The date is broken into a year portion and a days portion at lines 15 through 17. The correct number of days is added to the days portion of the date at lines 19 through 21. The next step, at lines 23 and 24, is to determine how many days are in the year portion of the date argument.

Once the days in the year are calculated, the new days value will be in one of three states: If the value is greater than the days in the year, the addition of the number of days has thrown the date forward into a later year. Alternatively, if the value is less than 1, a negative value was added and has thrown the date backward into an earlier year. Finally, the days could fall within the year. If the days fall within the year, no further processing is needed and the logic at lines 31 through 36 and at lines 43 through 48 is skipped. These two pieces of logic handle the future year and past year conditions. Lines 31 through 36 are executed as long as the number of calculated days exceeds the number of days in the year. The logic subtracts the number of days in the year, adds 1 to the year, and then again extracts the number of days in the new year. This process is repeated until the number of calculated days is reduced to a value between 1 and 365 (or 366). Because the year has been incremented on a loop, we now have the correct year and day of the year. Lines 43 through 48 perform the reverse process. The year is decremented by 1. The number of days in that year are calculated and added to the calculated day of the year. Once again, the number of calculated days is reduced to a value between 1 and 365 (or 366) and the correct year and day of the year are identified. The use of a loop for both incrementing and decrementing the year allows for the number of days to be added or subtracted to exceed the number of days in one year. Thus, ydadd 1998001 -4000 would calculate correctly to 1987019 (January 19, 1987).

 1	# ydadd adds days to a yyyyddd formatted date
 2	# usage ydadd 1998312 { ,-}14
 3	
 4	# Read from the difference from the command lines
 5	# and the date from the command line, or standard input
 6	if [ X$2 = X ]
 7	then
 8		dif=$1
 9		read yd
10	else
11		yd=$1
12		dif=$2
13	fi
14	
15	# Break it into pieces
16	d=`expr $yd % 1000`
17	y=`expr $yd / 1000`
18	
19	# Add the number of days (if days is negative this results in
20	# a subtraction)
21	d=`expr $d \+ $dif`
22	
23	# Extract the days in the year
24	diy=`yeardays $y`
25	
26	# If the calculated day exceeds the days in the year, 
27	# add one year to the year and subtract the days in the year from the
28	# calculated days. Extract the days in the new year and repeat
29	# test until you end up with a day number that falls within the
30	# days of the year
31	while [ `expr $d \> $diy` = 1 ]
32	do
33		d=`expr $d - $diy`
34		y=`expr $y \+ 1`
35		diy=`yeardays $y`
36	done
37	
38	# This is the reverse process. If the calculated number of days
39	# is less than 1, move back one year. Extract
40	# the days in this year and add the days in the year
41	# loop on this test until you end up with a number that
42	# falls within the days of the year
43	while [ `expr $d \< 1` = 1 ]
44	do
45		y=`expr $y - 1`
46		diy=`yeardays $y`
47		d=`expr $d \+ $diy`
48	done
49	
50	# put the year and day back together and echo the result
51	
52	yd=`expr \( $y \* 1000 \) + $d`
53	
54	echo $yd
55	

If you have any doubts about the validity of the math, here are a couple of examples that illustrate how this works. The first adds three years and one day to January 1, 1998. Because the year 2000 is a leap year, the number of days being added is 365 for 1998 and 1999 and 366 for 2000 plus the one day, for 1097 days. The listing starts with the relevant portion of the code so that you can follow the steps more easily. The result we're looking for is Jan 1, 1997 plus three years and a day, or Jan 2, 2001.


24	diy=`yeardays $y`
.
.
.
31	while [ `expr $d \> $diy` = 1 ]
32	do
33		d$=`expr $d - $diy`
34		y=`expr $y \+ 1`
35		diy=`yeardays $y`
36	done

Action Result
Starting date Jan 1, 1998
Convert to Julian 1998001
Separate days 1
Separate year 1998
Add 1097 to day 1 1098
Days in 1998 365
1098 > 365 True
1098 - 365 733
Year + 1 1999
Days in 1999 365
733 > 365 True
733 - 365 368
Year + 1 2000
Days in 2000 366
368 > 366 True
368 - 366 2
Year + 1 2001
Days in 2001 365
2 > 362 False
Combine year and days 2001002
Convert to Gregorian Jan 2, 2001

Let's try the reverse example and subtract three years and one day from January 1, 1998. Once again we're crossing a leap year, so the number of days will be 365 for 1997 and 1995, and 366 for 1996, plus one day for -1097. The result we're looking for is January 1, 1998 minus three years and a day, or December 31, 1994.


24	diy=`yeardays $y`
.
.
.
43	while [ `expr $d \< 1` = 1 ]
44	do
45		y=`expr $y - 1`
46		diy=`yeardays $y`
47		d=`expr $d \+ $diy`
48	done

Action Result
Starting date Jan 1, 1998
Convert to Julian 1998001
Separate days 1
Separate year 1998
Add -1097 to day 1 -1096
-1096 < 1 True
Year - 1 1997
Days in 1997 365
-1096 + 365 -731
-731 < 1 True
Year - 1 1996
Days in 1996 366
-731 + 366 -365
-365 < 1 True
Year - 1 1995
Days in 1995 365
-365 + 365 0
0 < 1 True
Year - 1 1994
Days in 1994 365
0 + 365 365
365 < 1 False
Combine year and days 1994365
Convert to Gregorian Dec 31, 1994

All the hard work is done. In order to add or subtract days to or from an 8-digit date, it's simply necessary to convert the date to Julian format, perform the addition, and convert the result back to 8-digit format.

Next month we'll tackle this and date formatting problems with some interesting insights on shell programming and the sed utility.

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.