Cover V12, I07

Article
Listing 1
Listing 2
Listing 3
Listing 4
Listing 5
Listing 6
Listing 7
Listing 8
Sidebar 1

jul2003.tar

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.