Date arithmetic in Unix shell scripts Date arithmetic in Unix shell scripts unix unix

Date arithmetic in Unix shell scripts


Assuming you have GNU date, like so:

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

And similar phrases.


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

meetingDate='12/31/2011' # MM/DD/YYYY FormatreminderDate=`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.htmlhttp://www.cyberciti.biz/faq/linux-unix-formatting-dates-for-display/

This worked for me on RHEL.


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

Here are some examples of its use. For brevity I have placed the output of each invocationon 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 howto do date arithmetic via a set of bash functions and these functions may prove usefulfor 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 Morellif [ $# = 0 ]; then   printdirections $0   exitfi# Request the value of a variableGetVar () {   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 yearDayNames=(mon tue wed thu fri sat sun )  # To correspond with 'date' outputDay2Int () {   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 nameMonth2Int () {   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 dateGetMonth() {   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 formNormalizeDate () {   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 monComputeExtension () {   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 leftPop () {   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 ofcol 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 lineSeparator=" "Reverse=falsecase "$1" in -r )  Reverse=true; shift; ;; [0-9]* ) ;; * )Separator="$1"; shift; ;;esaccase "$1" in -r )  Reverse=true; shift; ;; [0-9]* ) ;; * )Separator="$1"; shift; ;;esac#  Replace each col-i with $iCols=""for  f in $*do  if [ $Reverse = true ]; then     Cols="$Cols \$(NF-$f+1),"  else     Cols="$Cols \$$f,"  fidoneCols=`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   exitfi#  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 linesnumlines=`wc -l /tmp/printdirections.$$ | awk '{print $1}'`#  Remove the last   linenumlines=`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.