Date
Arithmetic with the Shell
Kyle Douglass and Ed Schaefer
Sometimes you need to measure the interval between two events
or otherwise manipulate dates. In this article, we survey the different
tools for acquiring the number of seconds from the epoch and present
several techniques for performing date arithmetic and date comparison.
We provide:
- A Korn shell function to determine Epoch seconds. We verify
the shell function using C and Perl time calls.
- A Korn shell date arithmetic example using the Astro-Julian
date algorithm. (See the Julian Date Definition sidebar.) We provide
functions to change between the Astro-Julian date and a Gregorian
date.
- A C date arithmetic example.
- A Perl date arithmetic example.
- A Korn shell solution for determining the day of the week from
a Gregorian date.
- Date range examples using UNIX tools.
Determine Epoch Time with the Shell
The easiest way to measure an interval is to manipulate the days,
hours, minutes, or seconds from a known point in time. In UNIX,
this point is midnight, Jan. 1, 1970 UTC, the Epoch.
Modern tools such as Tcl, Perl, GNU awk, and GNU date, all provide
the Epoch time, but what if you need a shell solution? The Korn
shell function GetEpochTime (Listing 1) counts the number of seconds
since the Epoch. The following is the Program Design Language (PDL)
for function GetEpochTime:
- Calculate the number of elapsed seconds from 1970 to yesterday
at midnight. GetEpochTime uses Scaliger's Julian Date (JD).
The JD, explained in the Date Arithmetic in the Shell section,
is the number of days from 1 January 4713 B.C.
- Calculate the number of elapsed seconds from midnight last
night to the current system time.
- Add the two calculations to obtain the current Epoch time.
The GetEpochTime test script (Listing 2) verifies the accuracy
of the shell function against C, GNU date, and Perl time calls.
The C portion includes creating a temporary C program and checking
for the proper C compiler before compiling.
Date Arithmetic with the Shell
You don't have to call external tools to perform date arithmetic.
With a little external algorithm help, the Korn shell alone can
perform date arithmetic.
In addition to the UNIX Epoch, there's another "stake
in the ground" -- Scaliger's Julian Date. The JD,
as mentioned in the sidebar is defined for purposes of this article
as the Astro-Julian date. We implement, in Korn shell, algorithms
based on the Astro-Julian Date. The get_astro_JD() function
(Listing 3) requires day, month, and year arguments, and returns
an integer Astro-Julian date.
The companion function get_greg_from_JD() (Listing 4) returns
a Gregorian date string (in format MONTH DAY YEAR) from an Astro-Julian
date. This example script finds yesterday's date:
#!/bin/ksh
TY=$(date '+%Y') # Year for today - MUST be YYYY
TM=$(date '+%m') # Month for today
TD=$(date '+%d') # Day for today
JD=$(get_astro_JD TD TM TY) # today's Astro-Julian date
# yesterday's date
yesterdays_date_str=$(get_greg_from_JD $((JD-1)) )
# parse yesterdays date string
set - $(echo $yesterdays_date_str)
echo $1 # month
echo $2 # day
echo $3 # year
# end script
Date Arithmetic with C
Before the advent of modern tools such as Perl, Tcl, and GNU versions
of the UNIX tools, perhaps the easiest method of performing date
arithmetic was to use standard C library calls.
It's possible to capture the output of custom utilities in
shell scripts. The following example script gets the number of seconds
from the Epoch, subtracts the number of seconds in 24 hours, 86400,
and returns yesterday's date:
#!/bin/ksh
# c_test.ss: "C" date arithmetic test
# get the seconds from the Epoch
xsecs=$(./get_epoch_secs)
# what's yesterday's date - 86400 seconds ago
yesterdaysdate=$(./get_date_from_secs $(($xsecs-86400)) )
echo $yesterdaysdate
# end script
c_test.ss: C Arithmetic Test
The get_epoch_secs utility (Listing 5) simply returns the system
date as the number of seconds since the Epoch using the C time()
library function.
In c_test.ss, subtract 86,400 seconds from today's date in
seconds to obtain yesterday's date in seconds. To convert the
number of seconds into a more readable format, C provides the tm
structure defined in the time.h header file:
struct tm {
int tm_sec; /* seconds 0 to 59 */
int tm_min; /* minutes 0 to 59 */
int tm_hour; /* hours 0 to 23 */
int tm_mday; /* days 1 to 31 */
int tm_mon; /* months since january 0 to 11 */
int tm_year; /* years since 1900 */
int tm_wday; /* days since sunday 0 to 6 */
int tm_yday; /* days since january 1, 0 to 365 */
int tm_isdst; /* daylight saving time flag */
};
The get_date_from_secs utility (Listing 6) is a wrapper program for
the C localtime() function. localtime() converts the
number of seconds to a tm object as defined above. The asctime()
function translates the tm structure into a string of form:
Wed Feb 11 10:58:09 2003
If you require output different from the asctime() output,
you can build a custom date and time string by selectively returning
parts of the tm structure. (See the commented-out line near the end
of Listing 6).
c_test.ss uses the system time, but you can obtain the number
of seconds of any date and time by manipulating the tm structure
and using the mktime() function. The mktime utility (Listing
7) requires month, day, and year arguments of the form 2 11 2003,
respectively. Initialize the tm structure with a time() call.
Change the month, day, and year tm columns. The mktime()
call returns the number of seconds. Here's an example call
from a shell script:
feb11seconds=$(./mktime 2 11 2003)
echo $feb11seconds
Date Arithmetic with Perl
While there are add-on modules available for Perl that perform
complex time and date calculations, Perl also has built-in modules
and functions capable of such tasks. The following test script uses
Perl's native "Time::Local" module and the native
"localtime" function to emulate the C Date Arithmetic
script previously mentioned:
#!/bin/ksh
# Example 1
# Prints yesterdays date and time as it relates to (now - (24 hours)).
perl -e '
$xsecs = time();
$yesterdaysdate = localtime(($xsecs - 86400));
print "$yesterdaysdate\n"; '
# Example 2
# Emulates the "DATE ARITHMETIC - 'C'".
# The emulation, however, is in the form of passing arguments
# to a function, rather than passing arguments to an
# executable. Prints epoch time for the date and time specified.
# Local system time is used for hour, minute, second.
# Arguments passed to the mktime function are day, month, year.
# In this case: February 11, 2003
#!/bin/ksh
function mktime
{
echo "$1 $2 $3" | perl -e '
use Time::Local;
for(;<STDIN>;)
{
@SplitArray = split(" ");
die "Must have [D]D [M]M YYYY" if scalar(@{SplitArray}) != 3;
($seconds, $minutes, $hours) = ((localtime)[0,1,2]);
$TheSeconds = timelocal($seconds, $minutes, $hours, \
$SplitArray[0], ($SplitArray[1] - 1), $SplitArray[2]);
print "${TheSeconds}\n";
}
'
}
#Given February 11, 2003 as the target date, call the mktime
#function with an argument of 11 2 2003
echo $(mktime 11 2 2003)
# end script
Find the Day of the Week with the Shell
Standard UNIX tools can easily return the day of the week of a
given date. However, a day-of-the-week algorithm is easily implemented
in the Korn shell. The get_dow() function (Listing 8) returns
an integer with 0 = sunday, 1 = monday, etc. This example script
determines that April 15, 2003 is Tuesday:
#!/bin/ksh
TY=2003
TM=4
TD=25
day_str=$(get_day_string $(get_dow TM TD TY) )
echo $day_str
# end script
Date Range Examples Using UNIX Tools
Often, we don't require complex date arithmetic techniques
to solve simpler date-centric tasks, such as parsing out a range
of dates in the form YYYY-MM-DD. For these tasks, we can use standard
UNIX regex utilities such as awk and sed.
In the following one-liner examples, use awk and sed to parse
out the desired date range. Given data.file:
2003-01-27 23:20:02
2003-01-28 23:25:02
2003-01-29 23:30:01
2003-01-30 00:25:01
2003-01-31 00:25:01
To retrieve date specific data from Jan. 28 to Jan. 30, executing:
awk '$1 ~ /2001-01-28/ , $1 ~ /2003-01-30/' data.file
delivers:
2003-01-28 23:25:02
2003-01-29 23:30:01
2003-01-30 00:25:01
To retrieve the same data using a range pattern, executing:
sed -n "/2003-01-28/,/2003-01-30/p" data.file
delivers the same output as above.
The above awk and sed examples work well as long as both date
parameters exist in data.file. If the first date parameter is not
found, nothing prints to standard output. If the first date parameter
is found, but the second date parameter is not, the entire list
(beginning with the first match) prints to standard output.
Conclusion
We've covered a cornucopia of UNIX date arithmetic processes.
Perl is probably your best choice; Perl has a plethora of date/time
modules in several categories such as Date, Time, and DateTime.
These modules are designed specifically for performing date/time
calculations and are a welcome addition to any systems administrators
toolbox.
Search CPAN, the official Perl module repository, at http://search.cpan.org
for the specific modules. CPAN has a search widget from which you
can search the following phrases (be sure to include the colons):
Time:: Date:: DateTime::
References
Kernighan, Brian W. and Dennis M. Ritchie. The C Programming
Language. Englewood Cliffs, NJ: Prentice-Hall, 1988.
Christiansen, Tom and Nathan Torkington. The Perl Cookbook.
Sebastopol, CA: O'Reilly & Associates, 1998.
The U.S. Naval Observatory Julian Date algorithms are located
at: http://aa.usno.navy.mil/faq/docs/JD_Formula.html
To search for specific Perl modules, see: http://search.cpan.org
See Mike Keith's World of Words & Numbers for the day-of-week
algorithm: http://users.aol.com/s6sj7gt/mikecal.htm
Mo Budlong presents a Bourne shell script for finding the number
of days between two Gregorian dates at: http://www.kingcomputerservices.com/unix_101/subtracting_dates.htm
Kyle Douglass's IT career has included systems administration
and programming on a 150+ node cross-platform server farm. He thanks
all the brilliant IT people whom have had an impact on his career,
from Technical Support to Systems Administration. He can be reached
at: UnixShell2002@yahoo.com.
Ed Schaefer is a frequent contributor to Sys Admin.
He is a software developer and DBA for Intel's Factory Integrated
Information Systems, FIIS, in Aloha, Oregon. Ed also hosts the UnixReview.com
monthly Shell Corner column. He can be reached at: shellcorner@comcast.net.
|