7

I have to perform some date calculations and conversion in a shell script. For example computing the difference in day between a date formatted as Nov 28 20:27:19 2012 GMT and today.

There are several possibilities (GNU date, gawk, perl, etc.) but I would like to be able to run it on systems without GNU tools (e.g., BSDs, Solaris, etc.)

At the moment the most portable solution I have is Perl but it could require the installation of additional modules for the date conversion.

What should I choose?

Edit: looking at the comments I realized that I need to clarify that the tool I am writing will be publicly distributed. I am not looking on a way to perform computations on my machine or on how to install GNU tools.

I want to use tools that are likely to be found on the majority of the machines.

Matteo
  • 9,796
  • 4
  • 51
  • 66
  • 1
    Use what ever you are most comfortable with - i personally like the python datetime module though it is a little bit rough re. timezones - http://docs.python.org/library/datetime.html – Ulrich Dangel Jul 05 '12 at 06:14
  • Python would not be a problem but I don't think I can just assume it is always installed. I'll check if it is present on standard Solaris and Mac OS X installations. – Matteo Jul 05 '12 at 06:33
  • jftr perl is afaik not installed on minimal centos 6 installations - but if you want to go the perl route have a look at carton which is basically bundler for perl and will installed the specified modules in your local directory. – Ulrich Dangel Jul 05 '12 at 06:38
  • Ok, so the standard way is to use a programming language like perl/ruby/python and just state the requirements. These languages have package manager as well as most systems have one too. – Ulrich Dangel Jul 05 '12 at 09:28
  • 1
    Non-embedded unices without perl are really rare. I'd go for perl with no additional module, it's good enough to parse a date that follows a well-known format. – Gilles 'SO- stop being evil' Jul 05 '12 at 22:37

4 Answers4

4

My solution is short, and I have previously written versions in C, Lua, Python, Perl, Awk, Ksh, and Csh (the version below is ksh.) Some specific questions you can answer with ansi_dn, nd_isna, and dow: Given a date YYYY MM DD, what is the date 100 days before or after? Given a date, what is the day of the week? Given two dates, how many days are in between?

#==========================================================================
# Calendar operations: Valid for dates in [1601-01-01, 3112-12-30].
# Adapted from a paper by B.E. Rodden, Mathematics Magazine, January, 1985.
#==========================================================================
# ANSI day number, given y=$1, m=$2, d=$3, starting from day number 1 ==
# (1601,1,1).  This is similar to COBOL's INTEGER-OF-DATE function.
function ansi_dn {
  integer y=$1 ys=$(($1-1601)) m=$2 d=$3 s dn1 isleap h
  ((isleap = y%4==0 && (y%100!=0 || y%400==0) ))
  ((dn1 = 365*ys + ys/4 - ys/100 + ys/400)) # first day number of given year.
  ((s = (214*(m-1) + 3)/7 + d - 1)) # standardized day number within year.
  ((h = -2*(!isleap && s>58) - (isleap && s>59) )) # correction to actual dn.
  print -- $((1 + dn1 + s + h))
}
# Inverse of ansi_dn, for adn=$1 in [1, 552245], with returned dates in
# [1601-01-01, 3112-12-30].  Similar to COBOL's DATE-OF-INTEGER function.
function nd_isna {
  integer y a s ys d0=$(($1-1)) dn1 isleap h
  typeset -Z2 m d
  ((ys = d0/365))
  ((365*ys + ys/4 - ys/100 + ys/400 >= $1)) && ((ys -= 1))
  ((dn1 = 365*ys + ys/4 - ys/100 + ys/400))
  ((y = 1601 + ys))
  ((a = d0 - dn1))
  ((isleap = y%4==0 && (y%100!=0 || y%400==0) ))
  ((h = -2*(!isleap && a>58) - (isleap && a>59) ))
  ((s = a - h))
  ((m = (7*s + 3)/214 + 1))
  ((d = s - (214*(m-1) + 3)/7 + 1))
  print -- $y $m $d
}
# Day of the week, given $1=ansi_dn.  0==Sunday, 6==Saturday.
function dow { print -- $(($1%7)); }
3

I had to do this in the past with brute force parsing and calculation in shell script.

Doing it manually in shell script is highly error-prone and slow. You need to account for days per month, leap years, time zones. It will fail with different locales and different languages.

I fixed up one of my old scripts, this would probably need to be modified for different shell environments, but there shouldn't be anything that can't be changed to work in any shell.

#!/bin/sh 

datestring="Nov 28 20:27:19 2012 GMT"
today=`date`

function date2epoch {

month=$1
day=$2
time=$3
year=$4
zone=$5

# assume 365 day years
epochtime=$(( (year-1970) * 365 * 24 * 60 * 60 ))

# adjust for leap days
i=1970
while [[ $i -lt $4 ]]
do
    if [[ 0 -eq $(( i % 400 )) ]]
    then
        #echo $i is a leap year
        # divisible by 400 is a leap year
        epochtime=$((epochtime+24*60*60))
    elif [[ 0 -eq $(( i % 100 )) ]]
    then
        #echo $i is not a leap year
        epochtime=$epochtime
    elif [[ 0 -eq $(( i % 4 )) ]]
    then
        #echo $i is a leap year
        # divisible by 4 is a leap year
        epochtime=$((epochtime+24*60*60))
    #   epoch=`expr $epoch + 24 * 60 * 60`
    fi

    i=$((i+1))
done    

dayofyear=0
for imonth in Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
do
    if [[ $month == $imonth ]]
    then
        break
    fi

    case $imonth in
    'Feb')
        if [[ 0 -eq $(( year % 400 )) ]]
        then
            days=29
        elif [[ 0 -eq $(( year % 100 )) ]]
        then
            days=28
        elif [[ 0 -eq $(( year % 4 )) ]]
        then
            days=29
        fi
    ;;

    Jan|Mar|May|Jul|Aug|Oct|Dec) days=31 ;;
    *) days=30 ;;
    esac

    #echo $imonth has $days days
    dayofyear=$((dayofyear + days))
done
## add the day of the month 
dayofyear=$((dayofyear+day))
#echo $dayofyear

########## Add the day fo year (-1) to the epochtime
#(-1, since eg. Jan 1 is not 24 hours into Jan1 )

epochtime=$((epochtime + (dayofyear -1) * 24*60*60))
#echo $epochtime
################## hours, minutes, seconds
OFS=$IFS
IFS=":"
set -- $time
hours=$1
minutes=$2
seconds=$3
epochtime=$((epochtime + (hours * 60 * 60) + (minutes * 60) + seconds))
IFS=$OFS

################## Time zone

case $zone in
'GMT') zonenumber=0
break;;
'EST') zonenumber=-5
break;;
'EDT') zonenumber=-4
break;;
esac

epochtime=$((epochtime + zonenumber * 60 * 60 ))

echo $epochtime
}

result=`date2epoch $datestring`

echo $result

I probably made a mistake somewhere and there may be a better way. Let me know if you find a bug, or a better way.

Once you have the epoch time, you can do some useful calculations... although converting that back into a date without the gnu utils... requires doing the above in reverse...

mgjk
  • 666
  • a case .... should always contain the default case, such as for example *) output_error ; exit 1 ;; . Here, any timezone different than one of your 3 cases will be silently ignored. – Olivier Dulac Mar 13 '15 at 16:12
2

You could use my dateutils. They're portable but it's rather safe to assume they're not installed anywhere. However, just ship the source code (plain old C) or binaries.

hroptatyr
  • 1,291
-3

Most logical thing to me would be to use the shells date&time capabilities? e.g for bash : http://ss64.com/bash/date.html

  • 2
    I didn't downvote you but the problem is that GNU date will behave differently than BSD date and they don't have, as far as i know, the same feature set. – Ulrich Dangel Jul 05 '12 at 09:29
  • Yes the problem is that the needed "-d" is a GNU date option – Matteo Jul 05 '12 at 14:36
  • I also didn't down vote you, but that resource is awkward. Only by visiting the higher level page http://ss64.com/bash/ does the author of ss64.com identify which are bash built-ins. The web side directory structure is misleading. – Lloyd Dewolf Apr 26 '15 at 00:26