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.

5
  • 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 - docs.python.org/library/datetime.html Commented Jul 5, 2012 at 6: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
    Commented Jul 5, 2012 at 6: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. Commented Jul 5, 2012 at 6: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. Commented Jul 5, 2012 at 9: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. Commented Jul 5, 2012 at 22:37

4 Answers 4

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...

1
  • 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. Commented Mar 13, 2015 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.

-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

3
  • 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. Commented Jul 5, 2012 at 9:29
  • Yes the problem is that the needed "-d" is a GNU date option
    – Matteo
    Commented Jul 5, 2012 at 14:36
  • I also didn't down vote you, but that resource is awkward. Only by visiting the higher level page ss64.com/bash does the author of ss64.com identify which are bash built-ins. The web side directory structure is misleading. Commented Apr 26, 2015 at 0:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .