|
![]() Date arithmetic, Part 3The 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)
![]() Mail this article to a friend |
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.
|
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-01-1999/swol-01-unix101.html
Last modified: Tuesday, January 12, 1999