Date arithmetic in Unix shell scripts

I need to do date arithmetic in Unix shell scripts that I use to control the execution of third party programs.

I'm using a function to increment a day and another to decrement:

IncrementaDia(){
echo $1 | awk '
BEGIN {
        diasDelMes[1] = 31
        diasDelMes[2] = 28
        diasDelMes[3] = 31
        diasDelMes[4] = 30
        diasDelMes[5] = 31
        diasDelMes[6] = 30
        diasDelMes[7] = 31
        diasDelMes[8] = 31
        diasDelMes[9] = 30
        diasDelMes[10] = 31
        diasDelMes[11] = 30
        diasDelMes[12] = 31
}
{
        anio=substr($1,1,4)
        mes=substr($1,5,2)
        dia=substr($1,7,2)

        if((anio % 4 == 0 && anio % 100 != 0) || anio % 400 == 0)
        {
                diasDelMes[2] = 29;
        }

        if( dia == diasDelMes[int(mes)] ) {
                if( int(mes) == 12 ) {
                        anio = anio + 1
                        mes = 1
                        dia = 1
                } else {
                        mes = mes + 1
                        dia = 1
                }
        } else {
                dia = dia + 1
        }
}
END {
        printf("%04d%02d%02d", anio, mes, dia)
}
'
}

if [ $# -eq 1 ]; then
        tomorrow=$1
else
        today=$(date +"%Y%m%d")
        tomorrow=$(IncrementaDia $hoy)
fi

but now I need to do more complex arithmetic.

What it's the best and more compatible way to do this?


Solution 1:

Assuming you have GNU date, like so:

date --date='1 days ago' '+%a'

And similar phrases.

Solution 2:

Here is an easy way for doing date computations in shell scripting.

meetingDate='12/31/2011' # MM/DD/YYYY Format
reminderDate=`date --date=$meetingDate'-1 day' +'%m/%d/%Y'`
echo $reminderDate

Below are more variations of date computation that can be achieved using date utility. http://www.cyberciti.biz/tips/linux-unix-get-yesterdays-tomorrows-date.html http://www.cyberciti.biz/faq/linux-unix-formatting-dates-for-display/

This worked for me on RHEL.

Solution 3:

I have written a bash script for converting dates expressed in English into conventional mm/dd/yyyy dates. It is called ComputeDate.

Here are some examples of its use. For brevity I have placed the output of each invocation on the same line as the invocation, separarted by a colon (:). The quotes shown below are not necessary when running ComputeDate:

$ ComputeDate 'yesterday': 03/19/2010
$ ComputeDate 'yes': 03/19/2010
$ ComputeDate 'today': 03/20/2010
$ ComputeDate 'tod': 03/20/2010
$ ComputeDate 'now': 03/20/2010
$ ComputeDate 'tomorrow': 03/21/2010
$ ComputeDate 'tom': 03/21/2010
$ ComputeDate '10/29/32': 10/29/2032
$ ComputeDate 'October 29': 10/1/2029
$ ComputeDate 'October 29, 2010': 10/29/2010
$ ComputeDate 'this monday': 'this monday' has passed.  Did you mean 'next monday?'
$ ComputeDate 'a week after today': 03/27/2010
$ ComputeDate 'this satu': 03/20/2010
$ ComputeDate 'next monday': 03/22/2010
$ ComputeDate 'next thur': 03/25/2010
$ ComputeDate 'mon in 2 weeks': 03/28/2010
$ ComputeDate 'the last day of the month': 03/31/2010
$ ComputeDate 'the last day of feb': 2/28/2010
$ ComputeDate 'the last day of feb 2000': 2/29/2000
$ ComputeDate '1 week from yesterday': 03/26/2010
$ ComputeDate '1 week from today': 03/27/2010
$ ComputeDate '1 week from tomorrow': 03/28/2010
$ ComputeDate '2 weeks from yesterday': 4/2/2010
$ ComputeDate '2 weeks from today': 4/3/2010
$ ComputeDate '2 weeks from tomorrow': 4/4/2010
$ ComputeDate '1 week after the last day of march': 4/7/2010
$ ComputeDate '1 week after next Thursday': 4/1/2010
$ ComputeDate '2 weeks after the last day of march': 4/14/2010
$ ComputeDate '2 weeks after 1 day after the last day of march': 4/15/2010
$ ComputeDate '1 day after the last day of march': 4/1/2010
$ ComputeDate '1 day after 1 day after 1 day after 1 day after today': 03/24/2010

I have included this script as an answer to this problem because it illustrates how to do date arithmetic via a set of bash functions and these functions may prove useful for others. It handles leap years and leap centuries correctly:

#! /bin/bash
#  ConvertDate -- convert a human-readable date to a MM/DD/YY date
#
#  Date ::= Month/Day/Year
#        |  Month/Day
#        |  DayOfWeek
#        |  [this|next] DayOfWeek
#        |  DayofWeek [of|in] [Number|next] weeks[s]
#        |  Number [day|week][s] from Date
#        |  the last day of the month
#        |  the last day of Month
#
#  Month ::= January | February | March | April | May | ...  | December
#  January  ::= jan | january | 1
#  February  ::= feb | january | 2
#  ...
#  December ::=  dec | december | 12
#  Day   ::= 1 | 2 | ... | 31
#  DayOfWeek ::= today | Sunday | Monday | Tuesday | ...  | Saturday
#  Sunday    ::= sun*
#  ...
#  Saturday  ::= sat*
#
#  Number ::= Day | a
#
#  Author: Larry Morell

if [ $# = 0 ]; then
   printdirections $0
   exit
fi



# Request the value of a variable
GetVar () {
   Var=$1
   echo -n "$Var= [${!Var}]: "
   local X
   read X
   if [ ! -z $X ]; then
      eval $Var="$X"
   fi
}

IsLeapYear () {
   local Year=$1
   if [ $[20$Year % 4]  -eq  0 ]; then
      echo yes
   else
      echo no
   fi
}

# AddToDate -- compute another date within the same year

DayNames=(mon tue wed thu fri sat sun )  # To correspond with 'date' output

Day2Int () {
   ErrorFlag=
   case $1 in
      -e )
         ErrorFlag=-e; shift
         ;;
   esac
   local dow=$1
   n=0
   while  [ $n -lt 7 -a $dow != "${DayNames[n]}" ]; do
      let n++
   done
   if [ -z "$ErrorFlag" -a $n -eq 7 ]; then
      echo Cannot convert $dow to a numeric day of wee
      exit
   fi
   echo $[n+1]

}

Months=(31 28 31 30 31 30 31 31 30 31 30 31)
MonthNames=(jan feb mar apr may jun jul aug sep oct nov dec)
# Returns the month (1-12) from a date, or a month name
Month2Int () {
   ErrorFlag=
   case $1 in
      -e )
         ErrorFlag=-e; shift
         ;;
   esac
   M=$1
   Month=${M%%/*}  # Remove /...
   case $Month in
      [a-z]* )
         Month=${Month:0:3}
         M=0
         while [ $M -lt 12 -a ${MonthNames[M]} != $Month ]; do
            let M++
         done
         let M++
   esac
   if [  -z "$ErrorFlag" -a $M -gt 12 ]; then
      echo "'$Month' Is not a valid month."
      exit
   fi
   echo $M
}

# Retrieve month,day,year from a legal date
GetMonth() {
   echo ${1%%/*}
}

GetDay() {
   echo $1 | col / 2
}

GetYear() {
   echo ${1##*/}
}


AddToDate() {

   local Date=$1
   local days=$2
   local Month=`GetMonth $Date`
   local Day=`echo $Date | col / 2`   # Day of Date
   local Year=`echo $Date | col / 3`  # Year of Date
   local LeapYear=`IsLeapYear $Year`

   if [ $LeapYear = "yes" ]; then
      let Months[1]++
   fi
   Day=$[Day+days]
   while [ $Day -gt ${Months[$Month-1]} ]; do
       Day=$[Day -  ${Months[$Month-1]}]
       let Month++
   done
   echo "$Month/$Day/$Year"
}

# Convert a date to normal form
NormalizeDate () {
   Date=`echo "$*" | sed 'sX  *X/Xg'`
   local Day=`date +%d`
   local Month=`date +%m`
   local Year=`date +%Y`
   #echo Normalizing Date=$Date > /dev/tty
   case $Date in
      */*/* )
         Month=`echo $Date | col / 1 `
         Month=`Month2Int $Month`
         Day=`echo $Date | col / 2`
         Year=`echo $Date | col / 3`
         ;;
      */* )
         Month=`echo $Date | col / 1 `
         Month=`Month2Int $Month`
         Day=1
         Year=`echo $Date | col / 2 `
         ;;
      [a-z]* ) # Better be a month or day of week
         Exp=${Date:0:3}
         case $Exp in
            jan|feb|mar|apr|may|june|jul|aug|sep|oct|nov|dec )
               Month=$Exp
               Month=`Month2Int $Month`
               Day=1
               #Year stays the same
               ;;
            mon|tue|wed|thu|fri|sat|sun )
               # Compute the next such day
               local DayOfWeek=`date +%u`
               D=`Day2Int $Exp`
               if [ $DayOfWeek -le $D ]; then
                  Date=`AddToDate $Month/$Day/$Year $[D-DayOfWeek]`
               else
                  Date=`AddToDate $Month/$Day/$Year $[7+D-DayOfWeek]`
               fi

               # Reset Month/Day/Year
               Month=`echo $Date | col / 1 `
               Day=`echo $Date | col / 2`
               Year=`echo $Date | col / 3`
               ;;
            * ) echo "$Exp is not a valid month or day"
                exit
               ;;
            esac
         ;;
      * ) echo "$Date is not a valid date"
          exit
         ;;
   esac
   case $Day in
      [0-9]* );;  # Day must be numeric
      * ) echo "$Date is not a valid date"
          exit
         ;;
   esac
      [0-9][0-9][0-9][0-9] );;  # Year must be 4 digits
      [0-9][0-9] )
          Year=20$Year
      ;;
   esac
   Date=$Month/$Day/$Year
   echo $Date
}
# NormalizeDate jan
# NormalizeDate january
# NormalizeDate jan 2009
# NormalizeDate jan 22 1983
# NormalizeDate 1/22
# NormalizeDate 1 22
# NormalizeDate sat
# NormalizeDate sun
# NormalizeDate mon

ComputeExtension () {

   local Date=$1; shift
   local Month=`GetMonth $Date`
   local Day=`echo $Date | col / 2`
   local Year=`echo $Date | col / 3`
   local ExtensionExp="$*"
   case $ExtensionExp in
      *w*d* )  # like 5 weeks 3 days or even 5w2d
            ExtensionExp=`echo $ExtensionExp | sed 's/[a-z]/ /g'`
            weeks=`echo $ExtensionExp | col  1`
            days=`echo $ExtensionExp | col 2`
            days=$[7*weeks+days]
            Due=`AddToDate $Month/$Day/$Year $days`
      ;;
      *d )    # Like 5 days or 5d
            ExtensionExp=`echo $ExtensionExp | sed 's/[a-z]/ /g'`
            days=$ExtensionExp
            Due=`AddToDate $Month/$Day/$Year $days`
      ;;
      * )
            Due=$ExtensionExp
      ;;
   esac
   echo $Due

}


# Pop -- remove the first element from an array and shift left
Pop () {
   Var=$1
   eval "unset $Var[0]"
   eval "$Var=(\${$Var[*]})"
}

ComputeDate () {
   local Date=`NormalizeDate $1`; shift
   local Expression=`echo $* | sed 's/^ *a /1 /;s/,/ /' | tr A-Z a-z `
   local Exp=(`echo $Expression `)
   local Token=$Exp  # first one
   local Ans=
   #echo "Computing date for ${Exp[*]}" > /dev/tty
   case $Token in
      */* ) # Regular date
         M=`GetMonth $Token`
         D=`GetDay $Token`
         Y=`GetYear $Token`
         if [ -z "$Y" ]; then
            Y=$Year
         elif [ ${#Y} -eq 2 ]; then
            Y=20$Y
         fi
         Ans="$M/$D/$Y"
         ;;
      yes* )
         Ans=`AddToDate $Date -1`
         ;;
      tod*|now )
         Ans=$Date
         ;;
      tom* )
         Ans=`AddToDate $Date 1`
         ;;
      the )
         case $Expression in
            *day*after* )  #the day after Date
               Pop Exp;   # Skip the
               Pop Exp;   # Skip day
               Pop Exp;   # Skip after
               #echo Calling ComputeDate $Date ${Exp[*]} > /dev/tty
               Date=`ComputeDate $Date ${Exp[*]}` #Recursive call
               #echo "New date is " $Date > /dev/tty
               Ans=`AddToDate $Date 1`
               ;;
            *last*day*of*th*month|*end*of*th*month )
               M=`date +%m`
               Day=${Months[M-1]}
               if [ $M -eq 2 -a `IsLeapYear $Year` = yes ]; then
                  let Day++
               fi
               Ans=$Month/$Day/$Year
               ;;
            *last*day*of* )
               D=${Expression##*of }
               D=`NormalizeDate $D`
               M=`GetMonth $D`
               Y=`GetYear $D`
               # echo M is $M > /dev/tty
               Day=${Months[M-1]}
               if [ $M -eq 2 -a `IsLeapYear $Y` = yes ]; then
                  let Day++
               fi
               Ans=$[M]/$Day/$Y
               ;;
            * )
               echo "Unknown expression: " $Expression
               exit
               ;;
         esac
         ;;
      next* ) # next DayOfWeek
         Pop Exp
         dow=`Day2Int $DayOfWeek` # First 3 chars
         tdow=`Day2Int ${Exp:0:3}` # First 3 chars
         n=$[7-dow+tdow]
         Ans=`AddToDate $Date $n`
         ;;
      this* )
         Pop Exp
         dow=`Day2Int $DayOfWeek`
         tdow=`Day2Int ${Exp:0:3}` # First 3 chars
         if [ $dow -gt $tdow ]; then
            echo "'this $Exp' has passed.  Did you mean 'next $Exp?'"
            exit
         fi
         n=$[tdow-dow]
         Ans=`AddToDate $Date $n`
         ;;
      [a-z]* ) # DayOfWeek ...

         M=${Exp:0:3}
         case $M in
            jan|feb|mar|apr|may|june|jul|aug|sep|oct|nov|dec )
               ND=`NormalizeDate ${Exp[*]}`
               Ans=$ND
               ;;
            mon|tue|wed|thu|fri|sat|sun )
               dow=`Day2Int $DayOfWeek`
               Ans=`NormalizeDate $Exp`

               if [ ${#Exp[*]} -gt 1 ]; then # Just a DayOfWeek
                  #tdow=`GetDay $Exp` # First 3 chars
                  #if [ $dow -gt $tdow ]; then
                     #echo "'this $Exp' has passed.  Did you mean 'next $Exp'?"
                     #exit
                  #fi
                  #n=$[tdow-dow]
               #else  # DayOfWeek in a future week
                  Pop Exp  # toss monday
                  Pop Exp  # toss in/off
                  if [ $Exp = next ]; then
                     Exp=2
                  fi
                  n=$[7*(Exp-1)]   # number of weeks
                  n=$[n+7-dow+tdow]
                  Ans=`AddToDate $Date $n`
               fi
               ;;
         esac
         ;;
      [0-9]* ) # Number  weeks [from|after] Date
         n=$Exp
         Pop Exp;
         case $Exp in
            w* ) let n=7*n;;
         esac

         Pop Exp; Pop Exp
         #echo Calling ComputeDate $Date ${Exp[*]} > /dev/tty
         Date=`ComputeDate $Date ${Exp[*]}` #Recursive call
         #echo "New date is " $Date > /dev/tty
         Ans=`AddToDate $Date $n`
         ;;
   esac
   echo $Ans
}

Year=`date +%Y`
Month=`date +%m`
Day=`date +%d`
DayOfWeek=`date +%a |tr A-Z a-z`

Date="$Month/$Day/$Year"
ComputeDate $Date $*

This script makes extensive use of another script I wrote (called col ... many apologies to those who use the standard col supplied with Linux). This version of col simplifies extracting columns from the stdin. Thus,

$ echo a b c d e | col 5 3 2

prints

e c b

Here it the col script:

#!/bin/sh
# col -- extract columns from a file
# Usage:
#    col [-r] [c] col-1 col-2 ...
#   where [c] if supplied defines the field separator
#   where each col-i represents a column interpreted according to  the presence of -r as follows:
#        -r present : counting starts from the right end of the line
#        -r absent  : counting starts from the left side of the line
Separator=" "
Reverse=false
case "$1" in
 -r )  Reverse=true; shift;
 ;;
 [0-9]* )
 ;;
 * )Separator="$1"; shift;
 ;;
esac

case "$1" in
 -r )  Reverse=true; shift;
 ;;
 [0-9]* )
 ;;
 * )Separator="$1"; shift;
 ;;
esac

#  Replace each col-i with $i
Cols=""
for  f in $*
do
  if [ $Reverse = true ]; then
     Cols="$Cols \$(NF-$f+1),"
  else
     Cols="$Cols \$$f,"
  fi

done

Cols=`echo "$Cols" | sed 's/,$//'`
#echo "Using column specifications of $Cols"
awk -F "$Separator"  "{print $Cols}"

It also uses printdirections for printing out directions when the script is invoked improperly:

#!/bin/sh
#
#  printdirections -- print header lines of a shell script
#
#  Usage:
#      printdirections path
#  where
#      path is a *full* path to the shell script in question
#      beginning with '/'
#
#  To use printdirections, you must include (as comments at the top
#  of your shell script) documentation for running the shell script.

if [ $# -eq 0 -o "$*" = "-h" ]; then
   printdirections $0
   exit
fi
#  Delete the command invocation at the top of the file, if any
#  Delete from the place where printdirections occurs to the end of the file
#  Remove the # comments
#  There is a bizarre oddity here.
   sed '/#!/d;/.*printdirections/,$d;/ *#/!d;s/# //;s/#//' $1 > /tmp/printdirections.$$

#  Count the number of lines
numlines=`wc -l /tmp/printdirections.$$ | awk '{print $1}'`

#  Remove the last   line
numlines=`expr $numlines - 1`


head -n $numlines /tmp/printdirections.$$
rm /tmp/printdirections.$$

To use this place the three scripts in the files ComputeDate, col, and printdirections, respectively. Place the file in directory named by your PATH, typically, ~/bin. Then make them executable with:

$ chmod a+x ComputeDate col printdirections

Problems? Send me some emaiL: morell AT cs.atu.edu Place ComputeDate in the subject.

Solution 4:

For BSD / OS X compatibility, you can also use the date utility with -j and -v to do date math. See the FreeBSD manpage for date. You could combine the previous Linux answers with this answer which might provide you with sufficient compatibility.

On BSD, as Linux, running date will give you the current date:

$ date
Wed 12 Nov 2014 13:36:00 AEDT

Now with BSD's date you can do math with -v, for example listing tomorrow's date (+1d is plus one day):

$ date -v +1d
Thu 13 Nov 2014 13:36:34 AEDT

You can use an existing date as the base, and optionally specify the parse format using strftime, and make sure you use -j so you don't change your system date:

$ date -j -f "%a %b %d %H:%M:%S %Y %z" "Sat Aug 09 13:37:14 2014 +1100"
Sat  9 Aug 2014 12:37:14 AEST

And you can use this as the base of date calculations:

$ date -v +1d -f "%a %b %d %H:%M:%S %Y %z" "Sat Aug 09 13:37:14 2014 +1100"
Sun 10 Aug 2014 12:37:14 AEST

Note that -v implies -j.

Multiple adjustments can be provided sequentially:

$ date -v +1m -v -1w
Fri  5 Dec 2014 13:40:07 AEDT

See the manpage for more details.

Solution 5:

To do arithmetic with dates on UNIX you get the date as the number seconds since the UNIX epoch, do some calculation, then convert back to your printable date format. The date command should be able to both give you the seconds since the epoch and convert from that number back to a printable date. My local date command does this,

% date -n
1219371462
% date 1219371462
Thu Aug 21 22:17:42 EDT 2008
% 

See your local date(1) man page. To increment a day add 86400 seconds.