Date arithmetic, Part 1

Calculating yesterday's date

Summary
This will be the first in a three-part series in which Mo will address the topic of date arithmetic, beginning with a method for calculating the date one day before any given "today." This process can be a bit tricky for certain dates, so watch out for leap years. (2,300 words)

 


 

An interesting letter from a Unix Insider reader posed the problem of how to calculate a date one day earlier than the present day's date. The problem is that if today is the first day of the month, or even the first day of January, finding the date one day earlier is no easy task, and date arithmetic is sadly neglected in most Unix utilities.

Rather than address this as a one-day-backwards problem, I decided to look at the general case of adding (or subtracting) days from a date and calculating the value of the new date.

The scripts in this article rely heavily on the expr command, which can perform integer arithmetic on shell variables. In the example below, the variable $a is set to 6 and then divided by 2 using the expr utility. The output of this command is 3, the result of the division.

 

  
$ a=6; expr $a / 2
3

In this second example of expr, the backquotes are used to assign the result of the expression $a / 2 to the variable $b, then the variable $b is echoed to the screen.

 

  
$ a=6; b=`expr $a / 2`; echo $b
3

This method of assigning a value by using expr in back quotes is used throughout these scripts. Make sure you understand what the commands are doing.

The expr expression evaluator uses standard arithmetic operators and parentheses for grouping parts of an equation together. The operators are listed below. Note the spaces between values and the operator characters. In any of the examples, a number can be replaced with a variable containing a numeric value.

 

Operation
output

Operator

Example

Addition 
7

 

+
expr 5 + 2
Subtraction 
3

 

-
expr 5 - 2
Multiplication
10

 

*
expr 5 * 2
Division
2

 

/
expr 5 / 2
Remainder division
1

 

%
expr 5 % 2
Grouping
14

 

()
expr ( 5 + 2 ) * 2
Less than
1

 

<
expr 3 < 5
Greater than
0

 

>
expr 3 > 5
Less than or equal
1

 

<=
expr 3 <= 5
Greater than or equal
0

 

>=
expr 3 >= 5
Equal
0

 

=
expr 3 = 5

There is one hitch to the expr command. Operators interpreted as special characters by the shell must be preceded by a backslash if the shell is to leave them alone. For the Korn and Bourne shells, these operators include parentheses for grouping ("()"), the asterisk for multiplication ("*"), and greater than and less than (">" and "<"). The table of operators with the escape characters included becomes:

 

Operation
output

Operator

Example

Addition 
7

 

+
expr 5 + 2
Subtraction 
3

 

-
expr 5 - 2
Multiplication
10

 

\*
expr 5 \* 2
Division
2

 

/
expr 5 / 2
Remainder division
1

 

%
expr 5 % 2
Grouping
14

 

\(\)
expr \( 5 + 2\ ) \* 2
Less than
1

 

\<
expr 3 \< 5
Greater than
0

 

\>
expr 3 \> 5
Less than or equal
1

 

\<=
expr 3 \<= 5
Greater than or equal
0

 

\>=
expr 3 \>= 5
Equal
0

 

=
expr 3 = 5

Using expr on the command line or in a shell script, you would include the escape characters wherever needed.

 

  
$ a=5; b=2; c=3; expr \( $a + $b \) \* $c
21

When looking at an expr formula, removing the backslashes will give you a clearer picture of what it's doing.

With that background on expr, we 're ready to look at date arithmetic.

Date arithmetic is simpler to perform in the so-called Julian date format. This is not a date in the Julian calendar; rather it's a date expressed as a year followed by a 3-digit number representative of the ordinal number of the day in the year. January 1, 1998, for example, becomes 1998001, and February 1, 1997 becomes 1997032. This format for dates has been incorrectly called Julian for so long that the name has stuck, but it's more appropriately called a year-and-ordinal-day format. On the other hand, you can see why the easier term, Julian, has stuck. I've also heard it called a military date, as it's a popular format in military software. Conversely, a date containing a year, a 2-digit month and a 2-digit year is sometimes called Gregorian, even though both dates are in the Gregorian calendar.

 

Leap years
The first problem to tackle is how to convert a YYYYMMDD formatted date into a YYYYDDD formatted date.

This problem would be fairly straightforward if it weren't for leap years. Since leap years will play a regular part in all of the following data calculations, I've created a single script that is passed a 4-digit year on the command line or on standard input and outputs the number of days in the year. The yeardays script is shown in the following listing. You may use yeardays by typing

 

  
$ yeardays 1998
365

or by piping a value in to the script as in

 

  
$ echo 2000|yeardays
366

The rules for a leap year are as follows:

 

  1. A year is a leap year if it is evenly divisible by 4
  2. ...but not if it's evenly divisible by 100
  3. ...unless it's also evenly divisible by 400

Testing these rules is simplified by turning them upside down and testing the year's divisibility by 400 then by 100 then by 4, and taking the result as soon as you have a match.

The yeardays listing has line numbers for this explanation. At lines 8 through 13, argument $1 is tested by concatenating it with an X. If it contains nothing ( [ X$1 = X ] ), command read is used to load the variable $y, otherwise $y is set to $1. There is no error checking for valid arguments. At lines 19 through 25, $a is set to the remainder of dividing $y by 400. If this remainder is 0, $y is evenly divisible by 400 and the year is a leap year. The number 366 is output and the script ends. At lines 27 through 33, a similar test is made to determine whether or not $y is evenly divisible by 100. If it is, $y cannot be a leap year, 365 is output, and the script ends. At lines 35 through 41, the classic evenly-divisible-by-4 test is done on the year. If it is divisible by 4, 366 is output and the script ends. Otherwise, 365 is output. This approach, of diminishing tests that cause the script to exit, makes it possible to do the leap year tests without a lot of if-else logic.

Please note the spacing used in expr and in the if tests.

 

  
 1   # yeardays
 2   # return the number of days in a year
 3   # usage yeardays yyyy
 4   
 5   # if there is no argument on the command line, assume that a
 6   # yyyy is being piped in
 7   
 8   if [ X$1 = X ]
 9   then
10      read y
11   else
12      y=$1
13   fi
14   
15   # a year is a leap year if it is even divisible by 4
16   # but not evenly divisible by 100
17   # unless it is evenly divisible by 400
18   
19   # if it is evenly divisible by 400 it must be a leap year
20   a=`expr $y % 400`
21   if  [ $a = 0 ]
22   then
23      echo 366
24      exit
25   fi
26   
27   #if it is evenly divisible by 100 it must not be a leap year
28   a=`expr $y % 100`
29   if [ $a = 0 ]
30   then
31      echo 365
32      exit
33   fi
34   
35   # if it is evenly divisible by 4 it must be a leap year
36   a=`expr $y % 4`
37   if [ $a = 0 ]
38   then
39      echo 366
40      exit
41   fi
42   
43   # otherwise it is not a leap year
44   echo 365
45     

In the year days listing, a test such as:

 

  
a=`expr $y % 100`
if [ $a = 0 ]

can be written more simply as:

 

  
if [`expr $y % 100` = 0 ]

Having illustrated expr in use, let's complicate things. We now have a script (or routine) we can use to determine a leap year. With this, it should be possible to write a similar routine that, when given a year and a month, returns the number of days in the month. The monthdays script shown below is an example of this.

The monthdays script can be invoked in several ways. A date in YYYYMMDD format can be used as an argument or can be piped in from another process. Alternatively, a year in YYYY format and a month appearing as one or two digits can be given as two arguments on the command line.

At lines 11 through 19, the arguments are taken care of. If no argument $1 exists, $ymd is read from standard input. Otherwise, if no argument $2 exists, the script assumes that one argument came in on the command line in YYYYMMDD format. Otherwise, there are two arguments, $1 containing YYYY and $2 containing a month. The expr logic at line 18 combines the year, the month, and a day of 1 into a date in YYYYMMDD format. This logic ensures that before the main part of the script begins, we're always starting with an 8-digit date. Since this routine only cares about the year and month, the day 1 is added whenever it's missing. At line 23, $ymd is divided by 10000 to extract the year into $y. At line 24, remainder division by 10000 and division by 100 are used to extract the month. You can follow the logic for extracting the month below, assuming that we're working with a date of 19981114.

 

  
19981114 % 10000 = 1114
1114 / 100 = 11

At lines 26 to 31, the value of the month ($m) is compared to the list of months containing 30 and 31 days using a case statement. If the month matches anything in the list, the number of days is output and the script exits. If there is no match, we're dealing with the dreaded month 2. At this point yeardays comes into play. At line 36, the script yeardays is called with the year that has been extracted. It will return a 365 or a 366 that is assigned to $diy. The value of $diy is tested, and 28 or 29 is output as the number of days for month 2.

  
 1   # monthdays
 2   # calculates the number of days in a month 
 3   # usage monthdays yyyy mm
 4   # or    monthdays yyyymmdd
 5   
 6   # if there are no command-line arguments, assume that a yyyymmdd is being
 7   # piped in and read the value.
 8   # if there is only one argument, assume it's a yyyymmdd on the command line;
 9   # otherwise it is a yyyy and mm on the command line
10   
11   if  [ X$1 = X ]
12   then
13      read ymd
14   elif [ X$2 = X ] 
15   then
16      ymd=$1
17   else
18      ymd=`expr \( $1 \* 10000 \) + \( $2 \* 100 \) + 1`
19   fi
20   
21   # extract the year and the month
22   
23   y=`expr $ymd / 10000` ;
24   m=`expr \( $ymd % 10000 \) / 100` ;
25   
26   # 30 days hath September etc.
27   case $m in
28      1|3|5|7|8|10|12) echo 31 ; exit ;;
29      4|6|9|11) echo 30 ; exit ;;
30      *) ;;
31   esac
32   
33   # except for month 2, which depends on whether the year is a leap year
34   # Use yeardays to get the number of days in the year and return a value
35   # accordingly.
36   diy=`yeardays $y`
37   
38   case $diy in
39      365) echo 28 ; exit ;;
40      366) echo 29 ; exit ;;
41   esac
42   

With leap years in hand, next month we'll take a look at adding and subtracting days from a date.

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.