KSH93 Date Manipulation

While bash is the default shell on most, if not all, Linux distributions, there are times when using ksh93 is more efficient and thus makes more sense to use. A classic problem in shell scripting is the manipulation of dates and times. Most shells do not include support for date/time string manipulation and the user is left to roll their own routines as needed. Typically this involves parsing date/time strings and using lookup tables and/or using a version of date with support for formatting date/time strings other than current date.

Since 1999, when version h of ksh93 (the 1993 version of the Korn Shell) was released, ksh93 has included such support via the printf builtin function. However examples on using this this feature are scarce and I have written this short article in an attempt to make more shell script developers aware of this extremely useful and powerful feature in ksh93.

The ksh93 builtin printf (not printf(1)) includes a %T formatting option.

%T               Treat argument as a date/time string and format it accordingly.

%(dateformat)T   T can be preceded by dateformat, where dateformat is any date format supported
                 by the date(1) command.

Here is the relevant output of printf –man:

    %T    Treat string as a date/time string and format it. The T can be
          preceded by (dformat), where dformat is a date format. The accepted
          date formats are as follows:
            %     % character
            a     abbreviated weekday name
            A     full weekday name
            b     abbreviated month name
            B     full month name
            c     ctime(3) style date without the trailing newline
            C     2-digit century
            d     day of month number
            D     date as mm/dd/yy
            e     blank padded day of month number
            f     print a date with the format '%Y.%m.%d-%H:%M:%S'
            F     %ISO 8601:2000 standard date format; equivalent to Y-%m-%d
            g     ls(1) -l recent date with hh:mm
            G     ls(1) -l distant date with yyyy
            h     abbreviated month name
            H     24-hour clock hour, zero-padded
            i     international date(1) date with time zone type name
            I     12-hour clock hour, zero-padded
            j     1-offset Julian date
            J     0-offset Julian date
            k     24-hour clock hour, blank-padded
            K     all numeric date; equivalent to %Y-%m-%d+%H:%M:%S; %_[EO]K
                  for space separator, %OK adds .%N, %EK adds %.N%z, %_EK adds
                  .%N %z
            l     12-hour clock hour, blank-padded
            L     locale default date format
            m     month number
            M     minutes
            n     newline character
            N     nanoseconds 000000000-999999999
            p     meridian (e.g., AM or PM)
            q     quarter of the year
            Q     <del>recent>del>distant<del>: <del> is a unique delimter
                  character; recent format for recent dates, distant format
            r     12-hour time as hh:mm:ss meridian
            R     24-hour time as hh:mm
            s     number of seconds since the epoch; .prec preceding s appends
                  prec nanosecond digits, 9 if prec is omitted
            S     seconds 00-60
            t     tab character
            T     24-hour time as hh:mm:ss
            u     weekday number 1(Monday)-7
            U     week number with Sunday as the first day
            V     ISO week number (i18n is fun)
            w     weekday number 0(Sunday)-6
            W     week number with Monday as the first day
            x     locale date style that includes month, day and year
            X     locale time style that includes hours and minutes
            y     2-digit year (you'll be sorry)
            Y     4-digit year
            z     time zone SHHMM west of GMT offset where S is + or -, use pad
                  _ for SHH:MM
            Z     time zone name
                  set (default or +) or clear (-) flag for the remainder of
                  format, or for the remainder of the process if == is
                  specified. flag may be:
                    l     enable leap second adjustments
                    n     convert %S as %S.%N
                    u     UTC time zone
            #     equivalent to %s
                  use alternate format if a default format override has not
                  been specified, e.g., ls(1) uses "%?%l"; export
                  TM_OPTIONS="format='override'" to override the default

As you can see, there is a rich range if date manipulation functionality available.

Some examples will illustrate the power of this feature.

Output the current date just like the date(1) command.

$ printf "%T\n" now
Sat Mar 22 10:01:35 EST 2008

Output the current hour, minute and second.

$ printf "%(%H:%M:%S)T\n" now

Note that ksh93 does not fork/exec the date(1) command to process this statement. It is built into ksh93. This results in faster shell script execution and less load on the operating system.

Output the number of seconds since the Unix Epoch.

$ printf "%(%s)T\n" now

$ printf "%(%.s)T\n"       # include nanoseconds

$ printf "%(%.3s)T\n"

If you know the number of seconds since the Unix Epoch you can output the corresponding date/time in ctime format.

$ printf "%T\n" #1206199251
Sat Mar 22 10:22:35 EST 2008

The printf builtin also understands date/time strings like “2:00pm yesterday”, “this Wednesday”, “23 days ago”, “next 9:30am”, “in 6 days”, “+ 5 hours 10 minutes” and lots more. Look at the source code for the printf builtin (…/cmd/ksh93/bltins/print.c) in the ksh93 sources for more information on the various date/time strings which are supported.

Output the date/time corresponding to “2:00pm yesterday.”

$ printf "T\n" "2:00pm yesterday"
Fri Mar 21 14:00:00 EST 2008

Output the day of the week corresponding to the last day of February 2008.

$ printf  "%(%a)T\n"  "final day Feb 2008"

Output the date corresponding to the third Wednesday in May 2008.

$ printf  "%(%D)T\n" "3rd wednesday may 2008"

Output what date it was 4 weeks ago.

$ printf  "%(%D)T\n" "4 weeks ago"

You can assign the output of printf “%T” to a variable. Note that “1997-198” represents the 198th day in 1997.

$ datestr=$(printf "%(%D)T" "1997-198")
$ print $datestr

You can also use a ksh93 discipline function to assign the output of printf to a variable:

$ EPOCHTIME.get() { .sh.value=$(printf "%(%s)T"); }
$ echo $EPOCHTIME                                  

$ EPOCHTIME.get() { .sh.value=$(printf "%(%s.%6N)T");    # with nanoseconds
$ echo "$EPOCHTIME"

The printf builtin even understands crontab and at date/time syntax as the following two examples demonstrate.

Output the date/time the command associated with this crontab entry will next execute.

$ printf "%T\n" "0 0 1,15 * 1"
Mon Sep 1 00:00:00 EDT 2008

Output the date/time the command associated with this at date/time string will execute.

$ printf "%T\n" "exactly next hour"
Sun Mar 23 14:07:31 EST 2008

The following example shows how to output the date for the first and last days of last month. Care needs to be taken in the order in which the date string is entered as not all combinations are valid.

$ printf "%(%Y-%m-%d)T\n" "1st last month"
$ printf "%(%Y-%m-%d)T\n" "final last month"

Microseconds are also understood by %T formatting option. Note %N outputs 9 digits by default unless you limit output using a length specifier as in the following example.

$ datestr="2008-11-24 05:17:00.7043"
$ printf "%(%m-%d-%Y %T.%4N)T\n" "$datestr"
11-24-2008 05:17:00.7043

The next example is a short shell script which tackles a common problem associated with backing up files and deleting logs, i.e. calculate the difference between two given dates.

# USAGE: diffdate start-date finish-date
# EXAMPLE: diffdate "Tue, Feb 19, 2008 08:00:02 PM"  "Wed, Feb 20, 2008 02:19:09 AM"
# Note:   Maximum of 100 hours difference 

[[ $# -ne 2 ]]  {
     echo "Usage: diffdate start-date finish-date"
     exit 1

SDATE=$(printf '%(%s)T' "$1")
FDATE=$(printf '%(%s)T' "$2")

SECS=$(($DIFF % 60))
MINS=$(($DIFF % (60 * 60) / 60))
HOURS=$(($DIFF / (60 * 60)))

printf  "%02d:%02d:%02d\n" $HOURS $MINS $SECS

My final example shows how to output a range of dates in a specific format incremented by 1 hour each time.

startdate="2008-05-26 01:00:00"

for ((i=0; i &lt; count; i++))
     printf "%(%m%d%Y%H0000)T\n" "${startdate} + $i hour"

Many users of ksh93 are unaware that printf %T also understands strings containing the 5 crontab date/time fields. Here is a simple example where we wish to output a formatted date for “next Monday 0800hrs”.

$ printf "%T\n"                
Tue Jan  5 16:04:37 GMT 2021
$ printf "%T\n" "0 8 * * 1"
Mon Jan 11 08:00:00 GMT 2021
$ printf "%(%s)T\n" "0 8 * * 1"

Well, that is about all there is to the printf %T feature in ksh93. I hope that you have found this short article on date/time manipulation using this feature to be useful and informative and that you will start using it in your future Korn Shell scripts.

UPDATE JUNE 2020: The version of the Korn Shell called ksh-2020, created by Kurtis Rader and Red Hat’s Siteshwar Vashisht utterly broke the printf %T functionality (and lots more!) as you can see from the following simple examples:

$ printf "%T" now
Thu Jun 18 10:33:55 EDT 2020$ 
$ printf "%T" "one month ago" 
ksh: printf: warning: invalid argument of type T
Thu Jun 18 10:34:37 EDT 2020ksh: printf: I/O error
$ printf  "%(%D)T\n" "4 weeks ago"
$ printf  "%(%D)T\n" "1 month ago"

It in the Fedora 32. I believe that the latest version of RHEL 8 also has this version of the Korn Shell.

By the way, recent versions of bash support a subset of ksh93‘s printf “%T” functionality.

2 comments to Korn Shell 93 Date Manipulation