|
![]() Date arithmetic, Part 2More details on how to calculate yesterday's date |
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)
![]() Mail this article to a friend |
ith 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.
|
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-12-1998/swol-12-unix101.html
Last modified: Friday, December 04, 1998