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. |