Date arithmetic, Part 3

The last few tools you need to calculate yesterday's date

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

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
fmtdate 19981206 %m/%d/%yyyy
fmtdate 19980904 "%month %d, %yyyy"
September 4, 1998
fmtdate 19980203 %yyyy-%mm-%dd

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.

 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
 8	# create a temporary version of argument 2 that replaces spaces
 9	# with x
10	tmpfmt=`echo $2|tr "_" x`
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
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`
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`
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/"`
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/"`
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/"

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
 7	# create a temporary version of argument 2 that replaces spaces
 8	# with x
 9	tmpfmt=`echo $2|tr " " x`
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
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`
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`
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/"`
49	# Create a three-character abbreviation of the month by cutting the
50	# first three characters out on $month
52	mon=`echo $month|cut -c1-3`
54	# Use tr to create uppercase versions of $mon and $month
56	MON=`echo $mon|tr [a-z] [A-Z]`
57	MONTH=`echo $month|tr [a-z] [A-Z]`
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/
67	s/%MON/$MON/
68	s/%yy/$yy/
69	s/%mm/$mm/
70	s/%m/$m/
71	s/%dd/$dd/
72	s/%d/$d/"

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






    • GEN-CODE
    • COMPILERS   



Search Now:
In Association with

Copyright©2001 King Computer Services Inc. All rights reserved.