Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>I have written a bash script for converting dates expressed in English into conventional mm/dd/yyyy dates. It is called <strong>ComputeDate</strong>.</p> <p>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 <em>not</em> necessary when running <strong>ComputeDate</strong>:</p> <pre><code>$ 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 </code></pre> <p>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:</p> <pre><code>#! /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 &gt; /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[*]}" &gt; /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[*]} &gt; /dev/tty Date=`ComputeDate $Date ${Exp[*]}` #Recursive call #echo "New date is " $Date &gt; /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 &gt; /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[*]} &gt; /dev/tty Date=`ComputeDate $Date ${Exp[*]}` #Recursive call #echo "New date is " $Date &gt; /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 $* </code></pre> <p>This script makes extensive use of another script I wrote (called <strong>col</strong> ... many apologies to those who use the standard <strong>col</strong> supplied with Linux). This version of <strong>col</strong> simplifies extracting columns from the stdin. Thus,</p> <pre><code>$ echo a b c d e | col 5 3 2 </code></pre> <p>prints</p> <pre><code>e c b </code></pre> <p>Here it the <strong>col</strong> script:</p> <pre><code>#!/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}" </code></pre> <p>It also uses <strong>printdirections</strong> for printing out directions when the script is invoked improperly:</p> <pre><code>#!/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 &gt; /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.$$ </code></pre> <p>To use this place the three scripts in the files <strong>ComputeDate</strong>, <strong>col</strong>, and <strong>printdirections</strong>, respectively. Place the file in directory named by your PATH, typically, ~/bin. Then make them executable with:</p> <pre><code>$ chmod a+x ComputeDate col printdirections </code></pre> <p>Problems? Send me some emaiL: morell AT cs.atu.edu Place <strong>ComputeDate</strong> in the subject.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload