Tuesday, July 2, 2013

unixtime.bash

The Solaris version of /bin/date does not support the '+%s' argument whereas GNU's date does.

According to (GNU) man date, it provides the following:

       %s     seconds since 1970-01-01 00:00:00 UTC

Of course, this is also known as UNIXTIME or Epoch Time and is very useful for determining the difference between two dates. 

Necessity being the mother of invention, I developed the following bash script to provide a common mechanism for both Solaris (10+) and Linux (RHEL 5+) to calculate an epoch time based on the input given.  It is also compatible with anything else that can run the bash shell and has 'cut' somewhere in the path (e.g. CentOS, Debian, Ubuntu, BSD, Cygwin, etc.).

So now it's possible to write a single script that works on multiple UNIX variants without installing additional binaries.

Here it is ...

--cut--
#!/bin/bash -r

# Script to calculate an accurate UNIX/Epoch time with the only external dependency being 'cut'.

# 20130702, Joseph Tingiris (joseph.tingiris@gmail.com)


# This function uses strict input definitions and doesn't do much in the way of validating them.  The function will fail if
# the inputs are not integers (except for INPUT_TIMEZONE).  It will *not* fail if the integers are out of range.
#
# i.e. unixtime 1970 01 01 00 00 00 -0000
# or   unixtime 2013 07 02 13 35 33 -0400
function unixtime() {
    if [ "$1" == "" ]
    then
        # no arguments returns current unixtime (i.e. now)
        INPUT_YEAR=`date +%Y`
        INPUT_MONTH=`date +%m`
        INPUT_DAY=`date +%d`
        INPUT_HOUR=`date +%H`
        INPUT_MINUTE=`date +%M`
        INPUT_SECOND=`date +%S`
        INPUT_TIMEZONE=`date +%z`
    else
        INPUT_YEAR=$1
        INPUT_MONTH=$2
        INPUT_DAY=$3
        INPUT_HOUR=$4
        INPUT_MINUTE=$5
        INPUT_SECOND=$6
        INPUT_TIMEZONE=$7
    fi

    INPUT_YEAR=$((10#$INPUT_YEAR)) # force decimal (base 10)
    INPUT_MONTH=$((10#$INPUT_MONTH)) # force decimal (base 10)
    INPUT_DAY=$((10#$INPUT_DAY)) # force decimal (base 10)
    INPUT_HOUR=$((10#$INPUT_HOUR)) # force decimal (base 10)
    INPUT_MINUTE=$((10#$INPUT_MINUTE)) # force decimal (base 10)
    INPUT_SECOND=$((10#$INPUT_SECOND)) # force decimal (base 10)
        # basic input validation
    if [ "$INPUT_YEAR" == "" ]; then return 9; fi
    if [ "$INPUT_MONTH" == "" ] || [ $INPUT_MONTH -lt 1 ] || [ $INPUT_MONTH -gt 12 ]; then return 9; fi
    if [ "$INPUT_DAY" == "" ] || [ $INPUT_DAY -lt 1 ] || [ $INPUT_DAY -gt 31 ]; then return 9; fi
    if [ "$INPUT_HOUR" == "" ] || [ $INPUT_HOUR -gt 23 ] ; then return 9; fi
    if [ "$INPUT_MINUTE" == "" ] || [ $INPUT_MINUTE -gt 60 ]; then return 9; fi
    if [ "$INPUT_SECOND" == "" ] || [ $INPUT_SECOND -gt 60 ]; then return 9; fi
    if [ "$INPUT_TIMEZONE" == "" ]; then INPUT_TIMEZONE="-0000"; fi # If not set, set the default timezone to UTC (-0000)


    # 1 Determine the number of years since 1970 and multiply that by the number of seconds in a standard year (31536000 seconds).
    #
    YEARS_SINCE_1970=$(($INPUT_YEAR-1970))
    SECONDS_SINCE_1970=$(($YEARS_SINCE_1970*31536000))
    TIME_A=$SECONDS_SINCE_1970
    #echo "YEARS_SINCE_1970          : $YEARS_SINCE_1970"
    #echo "SECONDS_SINCE_1970        : $SECONDS_SINCE_1970"
   
   
    # 2 Determine the number of days and seconds that separate the input date and January 1st.
    #
    MONTH=0
    DAYS_SINCE_JAN1=0
    MONTHS_DAYS="31 28 31 30 31 30 31 31 30 31 30 31"
    for MONTH_DAYS in $MONTHS_DAYS
    do
        MONTH=$(($MONTH+1))
        if [ $MONTH -eq $INPUT_MONTH ]
        then
            #echo "MONTH: $MONTH [$MONTH_DAYS] *"
            DAYS_SINCE_JAN1=$(($DAYS_SINCE_JAN1+$INPUT_DAY-1))
            break
        else
            #echo "MONTH: $MONTH [$MONTH_DAYS]"
            DAYS_SINCE_JAN1=$(($DAYS_SINCE_JAN1+$MONTH_DAYS))
        fi
    done
    SECONDS_SINCE_JAN1=$(($DAYS_SINCE_JAN1*86400))
    TIME_B=$SECONDS_SINCE_JAN1
    #echo "DAYS_SINCE_JAN1           : $DAYS_SINCE_JAN1"
    #echo "SECONDS_SINCE_JAN1        : $SECONDS_SINCE_JAN1"

    # 3 Determine the time difference between the input time and midnight (00:00), in seconds.
    #
    SECONDS_SINCE_MIDNIGHT=$((($INPUT_HOUR*3600)+($INPUT_MINUTE*60)+$INPUT_SECOND))
    TIME_C=$SECONDS_SINCE_MIDNIGHT
    #echo "SECONDS_SINCE_MIDNIGHT    : $SECONDS_SINCE_MIDNIGHT"

    # 4 Find the number of leap years that have elapsed since 1970, and multiply that number by the number of seconds in a day (86400).
    #
    LEAP_YEARS_BEFORE_1970=$(((1970/4)-(1970/100)+(1970/400)))
    LEAP_YEARS_BEFORE_INPUT=$((($INPUT_YEAR/4)-($INPUT_YEAR/100)+($INPUT_YEAR/400)))
    LEAP_YEARS_SINCE_1970=$(($LEAP_YEARS_BEFORE_INPUT-$LEAP_YEARS_BEFORE_1970))
    LEAP_SECONDS_SINCE_1970=$(($LEAP_YEARS_SINCE_1970*86400))
    TIME_D=$LEAP_SECONDS_SINCE_1970
    #echo "LEAP_YEARS_BEFORE_1970    : $LEAP_YEARS_BEFORE_1970"
    #echo "LEAP_YEARS_BEFORE_INPUT   : $LEAP_YEARS_BEFORE_INPUT"
    #echo "LEAP_YEARS_SINCE_1970     : $LEAP_YEARS_SINCE_1970"
    #echo "LEAP_SECONDS_SINCE_1970   : $LEAP_SECONDS_SINCE_1970"

    # 5 Calculate the timezone offset, in seconds.
    #
    TZ_PLUS_MINUS=`echo $INPUT_TIMEZONE | cut -c 1-1`
    TZ_HOUR=`echo $INPUT_TIMEZONE | cut -c 2-3`
    TZ_MINUTE=`echo $INPUT_TIMEZONE | cut -c 4-`
    TZ_SECONDS=$((($TZ_HOUR*3600)+($TZ_MINUTE*60)))
    #echo "TZ_PLUS_MINUS             : $TZ_PLUS_MINUS"
    #echo "TZ_HOUR                   : $TZ_HOUR"
    #echo "TZ_MINUTE                 : $TZ_MINUTE"
    #echo "TZ_SECONDS                : $TZ_SECONDS"

    # 6 Determine the sum of times A, B, C and D, and add or subtract the timezone offset.
    #
    if [ "$TZ_PLUS_MINUS" == "-" ]
    then
        UNIXTIME=$((($TIME_A+$TIME_B+$TIME_C+$TIME_D)+$TZ_SECONDS))
    else
        UNIXTIME=$((($TIME_A+$TIME_B+$TIME_C+$TIME_D)-$TZ_SECONDS))
    fi

    echo "$UNIXTIME"

    # These will work to validate the UNIXTIME variable is correct, if 'date' supports the +%s flag (e.g. GNU/Linux but not Solaris).
    #INPUT_DATE="$INPUT_YEAR-$INPUT_MONTH-$INPUT_DAY $INPUT_HOUR:$INPUT_MINUTE:$INPUT_SECOND $INPUT_TIMEZONE"
    #echo "INPUT_DATE                : $INPUT_DATE"
    #echo "UNIXTIME                  : $UNIXTIME (`date -d @$UNIXTIME`)"
    #UNIXTIME_DATE=`date --date="$INPUT_DATE" +%s`
    #echo "UNIXTIME (date)           : $UNIXTIME_DATE (`date -d @$UNIXTIME_DATE`)"

}

unixtime $1 $2 $3 $4 $5 $6 $7
--cut--