Date arithmetic, Part 3
The last few tools you need to calculate yesterday's date
Summary
In this final installment on date arithmetic, Mo combines the tools created
in the last two episodes to perform date arithmetic on an 8-digit date, and
he also takes a look at some date formatting tricks. (2,100 words)
All the hard work is done. In order to add (or subtract) days to an
8-digit date, it is simply necessary to convert a date to Julian format,
perform the addition on the Julian date, and convert the result back to 8-digit
format. We have all the tools to do this. The following listing for ymdadd
puts all the pieces together. A standard input similar to ydadd (see last
month) is used at lines 6 through 14. Once the year and days-to-add have
been identified, the date is piped through ymd2yd to convert it, piped on
through ydadd to do the arithmetic, and then piped once more through yd2ymd to
convert back to an 8-digit date. This long pipe is at line 19.
1 # ymdadd adds days to yyyymmdd
2 # usage ymdadd 19980801 { ,-}4
3 # will add negative number of days to a date
4
5
6 # if only one argument exists, it assumes that the date is being piped in
7 if [ X$2 = X ]
8 then
9 dif=$1
10 read ymd
11 else
12 ymd=$1
13 dif=$2
14 fi
15
16 # echo the date through pipes to convert it to a Julian format
17 # complete the addition of the days and convert it back to
18 # 8-digit format
19 echo $ymd|ymd2yd|ydadd $dif|yd2ymd
20
You might be thinking we've done enough on this subject, but as you may
recall, I started this project two issues back based on a letter from a
reader. He wanted to know how to calculate a date one day earlier than the
current date, and also wanted the result formatted as YYYYMMDD, as in
1998-10-24. Rather than leave a stone unturned, we'll tackle the problem
of formatting in this issue.
Once again, I'm going to attempt to come up with a more generic approach
to formatting than simply outputting one specific format. The routine I
designed, fmtdate, will accept a formatting string that is similar to the
formatting characters of the date command, in that its format fields start
with a percent sign (%). The format fields are listed below with an example
of the results of each format.
Format string |
Represents |
Applied to 19980206 |
%yyyy |
4-digit year |
1998 |
%yy |
2-digit year |
98 |
%mm |
2-digit month with leading zeroes |
02 |
%m |
2-digit month with no leading zeroes |
2 |
%dd |
2-digit day with leading zeroes |
06 |
%d |
2-digit day with no leading zeroes |
6 |
%mon |
Three-character month abbreviation |
Feb |
%month |
Full month name |
February |
The script, fmtdate, and will accept an 8-digit date and a format string.
Using the above table of format strings, output examples of the script
are shown below.
fmtdate 19980206 %mm/%dd/%yy
02/06/98
fmtdate 19981206 %m/%d/%yyyy
12/6/1998
fmtdate 19980904 "%month %d, %yyyy"
September 4, 1998
fmtdate 19980203 %yyyy-%mm-%dd
1998-02-03
The fmtdate script presents a number of interesting shell programming
problems. I decided to imitate the logic used in all the preceding scripts,
allowing either two arguments on the command line, representative of the date and
format string, or allowing the date to be piped in and using only one
argument on the command line, representative of the format string.
It would seem at first glance that the simple way to test for two arguments
is to test whether the second argument exists, as in earlier scripts.
A test such as: if [ X$2 = X ] should do the job, but it doesn't.
If the format string is the second argument, it is allowed to contain spaces:
fmtdate 19980904 "%month %d, %yyyy"
September 4, 1998
If you use a simple test command on argument $2 in the above example, the
test: if [ X$2 = X ] is translated into: if [ X%month %d, %yyyy = X ] and
fails. There are too many spaces in the test, so its logic looks
at the second field, %d, tries to use it as a test operator, and fails.
In order to make the test work, the second argument has to be converted
into a solid string with no spaces. This is handled at line 10 of the fmtdate
script. The tr utility is used to replace all spaces in $2 with underscores (_)
and assign the result to $tmpfmt, used for the test at line 13. Using the
"solidified" version of argument 2, the test becomes: if [
X%month_%d,_%yyyy = X ], which tests correctly.
The $tmpfmt variable is only needed to test whether $2 is empty or not
and is used no further. If $2 is empty, $1 is assumed to be the format string
and the date is read from standard input. Otherwise $1 becomes the date and
$2 becomes the format string. See lines 13 through 20. At lines 22 through 27,
cut is used to cut characters out of the date and assign them to variables $yyyy,
$yy, $mm, and $dd. These are used to create the 4-digit year, 2-digit year, 2-digit month
with leading zeroes and 2-digit day with leading zeroes, respectively.
At lines 29 through 32 expr is used to create "numeric" versions of
$d and $m which will have their leading zeroes suppressed because
they were generated as a mathematical result of the expr command.
Three-character month display
The three-character month is trickier to generate. The logic for this is at
lines 34 through 48. The 2-digit month, $mm, is echoed through a sed script
that does a search and replace of 01 with Jan, 02 with Feb, and so on through
December. Since only one value, $mm, is echoed through sed, sed will only
return one value, the three-character month. The result is assigned to the
variable $mon. Similar logic is used at lines 50 through 64 to generate the
full month name and assign it to the variable $month.
At this point the 8-digit date has been broken down into various variable
values for each of its parts.
At lines 66 through 77, the format string is echoed through a sed script
that searches for the percent variables, %yyyy, %yy, %mm, %m, %dd, %d, %mon, and
%month. These are replaced by their respective script variables $yyyy, $yy,
$mm, $m, $dd, $d, $mon, and $month. The script variables have been set to
contain values that are the pieces of the date, so the format string fields
are replaced with the date parts and the result is output.
1
2 # fmtdate formats a date
3 # usage fmtdate YYYYMMDD format-string
4 # or echo YYYYMMDD|fmtdate format-string
5 # assumes that if only one argument arrives, it is the format
6 # the date is being piped in
7
8 # create a temporary version of argument 2 that replaces spaces
9 # with x
10 tmpfmt=`echo $2|tr "_" x`
11
12 # test if argument 2 is empty
13 if [ X$tmpfmt = X ]
14 then
15 fmt=$1
16 read ymd
17 else
18 ymd=$1
19 fmt=$2
20 fi
21
22 # cut the 8-digit date into pieces for a 4-digit year
23 # a 2-digit year, a 2-digit month and a 2-digit day
24 yyyy=`echo $ymd|cut -c1-4`
25 yy=`echo $ymd|cut -c3-4`
26 mm=`echo $ymd|cut -c5-6`
27 dd=`echo $ymd|cut -c7-8`
28
29 # Create numeric versions of day and month that will suppress
30 # the leading zeroes
31 m=`expr $mm \* 1`
32 d=`expr $dd \* 1`
33
34 # echo the month through a sed script that will assign a month
35 # abbreviation to the variable $mon for the 2-digit month
36 mon=`echo $mm|sed -e "
37 s/01/Jan/
38 s/02/Feb/
39 s/03/Mar/
40 s/04/Apr/
41 s/05/May/
42 s/06/Jun/
43 s/07/Jul/
44 s/08/Aug/
45 s/09/Sep/
46 s/10/Oct/
47 s/11/Nov/
48 s/12/Dec/"`
49
50 # echo the month through a sed script that will assign a full month
51 # name to the variable $month for the 2-digit month
52 month=`echo $mm|sed -e "
53 s/01/January/
54 s/02/February/
55 s/03/March/
56 s/04/April/
57 s/05/May/
58 s/06/June/
59 s/07/July/
60 s/08/August/
61 s/09/September/
62 s/10/October/
63 s/11/November/
64 s/12/December/"`
65
66 # echo the format string through a sed script that searches for
67 # the key values %yyyy, %yy, %mm, %m, %dd, %d, %mon and %month and replaces
68 # them with appropriate values.
69 echo $fmt|sed -e "
70 s/%yyyy/$yyyy/
71 s/%month/$month/
72 s/%mon/$mon/
73 s/%yy/$yy/
74 s/%mm/$mm/
75 s/%m/$m/
76 s/%dd/$dd/
77 s/%d/$d/"
78
While we're skinning cats, the next listing presents an
alternative approach to generating the $month and $mon variables. Instead
of using sed to generate $mon and then again to generate $month, sed is used
to generate $month and then $mon is created by cutting the first three
characters out of $month at lines 48 and 49. This version of fmtdate also adds two
additional format fields, %MON and %MONTH, which are uppercase versions of
%mon and %month. The variables to populate these fields, $MON and $MONTH, are
created at lines 54 through 57 by using tr to translate lowercase
characters to uppercase.
1 # fmtdate formats a date
2 # usage fmtdate YYYYMMDD format-string
3 # or echo YYYYMMDD|fmtdate format-string
4 # assumes that if only one argument arrives, that it is the format
5 # the date is being piped in
6
7 # create a temporary version of argument 2 that replaces spaces
8 # with x
9 tmpfmt=`echo $2|tr " " x`
10
11 # test if argument 2 is empty
12 if [ X$tmpfmt = X ]
13 then
14 fmt=$1
15 read ymd
16 else
17 ymd=$1
18 fmt=$2
19 fi
20
21 # cut the 8-digit date into pieces for a 4-digit year
22 # a 2-digit year, a 2-digit month and a 2-digit day
23 yyyy=`echo $ymd|cut -c1-4`
24 yy=`echo $ymd|cut -c3-4`
25 mm=`echo $ymd|cut -c5-6`
26 dd=`echo $ymd|cut -c7-8`
27
28 # Create numeric versions of day and month that will suppress
29 # the leading zeroes
30 m=`expr $mm \* 1`
31 d=`expr $dd \* 1`
32
33 # echo the month through a sed script that will assign a full month
34 # name to the variable $month for the 2-digit month
35 month=`echo $mm|sed -e "
36 s/01/January/
37 s/02/February/
38 s/03/March/
39 s/04/April/
40 s/05/May/
41 s/06/June/
42 s/07/July/
43 s/08/August/
44 s/09/September/
45 s/10/October/
46 s/11/November/
47 s/12/December/"`
48
49 # Create a three-character abbreviation of the month by cutting the
50 # first three characters out on $month
51
52 mon=`echo $month|cut -c1-3`
53
54 # Use tr to create uppercase versions of $mon and $month
55
56 MON=`echo $mon|tr [a-z] [A-Z]`
57 MONTH=`echo $month|tr [a-z] [A-Z]`
58
59 # echo the format string through a sed script that searches for
60 # the key values %yyyy, %yy, %mm, %m, %dd, %d, %mon, and %month and replaces
61 # them with appropriate values.
62 echo $fmt|sed -e "
63 s/%yyyy/$yyyy/
64 s/%month/$month/
65 s/%mon/$mon/
66 s/%MONTH/$MONTH/
67 s/%MON/$MON/
68 s/%yy/$yy/
69 s/%mm/$mm/
70 s/%m/$m/
71 s/%dd/$dd/
72 s/%d/$d/"
73
And at last we come to the solution requested by my reader:
extract today's date, subtract 1, and output the result in
YYYYMMDD format. All you have to do is run the yestrday script
shown below.
1 # yestrday -- extracts the system date, subtracts 1 from it, and formats
2 # the result as YYYYMMDD
3
4 echo `date +%Y%m%d`|ymdadd -1|fmtdate %yyyy-%mm-%dd
It only took three installments. Now wasn't that simple?
A final note is in order on these date-handling routines. The methods
developed in the shell scripts in this and the previous two articles are
not suited to calculating large day differences, they're too slow. Limit their
use to only a few years.
Contact
us for a free consultation. |