2

For cigs.sh - the complete script can be found here - I wrote the following ugly (but perfectly working) logic to print and format the script's output, partly just to figure out all the edge cases but also because I didn't see any other alternative.

...

if [[ $W -le 0 && $D -le 0 && $H -eq 1 ]]; then string="$H hour" elif [[ $W -le 0 && $D -le 0 && $H -gt 1 ]]; then string="$H hours" elif [[ $W -le 0 && $D -eq 1 && $H -le 0 ]]; then string="$D day" elif [[ $W -le 0 && $D -eq 1 && $H -eq 1 ]]; then string="$D day and $H hour" elif [[ $W -le 0 && $D -eq 1 && $H -gt 1 ]]; then string="$D day and $H hours"
elif [[ $W -le 0 && $D -gt 1 && $H -le 0 ]]; then string="$D days" elif [[ $W -le 0 && $D -gt 1 && $H -eq 1 ]]; then string="$D days and $H hour" elif [[ $W -le 0 && $D -gt 1 && $H -gt 1 ]]; then string="$D days and $H hours" elif [[ $W -eq 1 && $D -le 0 && $H -le 0 ]]; then string="$W week" elif [[ $W -eq 1 && $D -le 0 && $H -eq 1 ]]; then string="$W week and $H hour" elif [[ $W -eq 1 && $D -le 0 && $H -gt 1 ]]; then string="$W week and $H hours" elif [[ $W -eq 1 && $D -eq 1 && $H -le 0 ]]; then string="$W week and $D day" elif [[ $W -eq 1 && $D -gt 1 && $H -le 0 ]]; then string="$W week and $D days" elif [[ $W -eq 1 && $D -eq 1 && $H -eq 1 ]]; then string="$W week, $D day and $H hour" elif [[ $W -eq 1 && $D -eq 1 && $H -gt 1 ]]; then string="$W week, $D day and $H hours" elif [[ $W -eq 1 && $D -gt 1 && $H -eq 1 ]]; then string="$W week, $D days and $H hour" elif [[ $W -eq 1 && $D -gt 1 && $H -gt 1 ]]; then string="$W week, $D days and $H hours" elif [[ $W -gt 1 && $D -le 0 && $H -le 0 ]]; then string="$W weeks" elif [[ $W -gt 1 && $D -le 0 && $H -eq 1 ]]; then string="$W weeks and $H hour" elif [[ $W -gt 1 && $D -le 0 && $H -gt 1 ]]; then string="$W weeks and $H hours" elif [[ $W -gt 1 && $D -eq 1 && $H -le 0 ]]; then string="$W weeks and $D day" elif [[ $W -gt 1 && $D -gt 1 && $H -le 0 ]]; then string="$W weeks and $D days" elif [[ $W -gt 1 && $D -eq 1 && $H -eq 1 ]]; then string="$W weeks, $D day and $H hour" elif [[ $W -gt 1 && $D -eq 1 && $H -gt 1 ]]; then string="$W weeks, $D day and $H hours" elif [[ $W -gt 1 && $D -gt 1 && $H -eq 1 ]]; then string="$W weeks, $D days and $H hour" elif [[ $W -gt 1 && $D -gt 1 && $H -gt 1 ]]; then string="$W weeks, $D days and $H hours" fi

colour1='\033[0;31m' colour2='\033[0;32m' if (($elapsed < threshold)) then echo -e "${colour1}It's been $string since you last bought a $item." else echo -e "${colour2}It's been $string since you last bought a $item." fi

Maybe I'm just being dumb, but embarrassing as the above code is, I can't see a better way it could be rewritten. Does one exist, and if so, what is it?

Hashim Aziz
  • 13,835

4 Answers4

3

An alternative approach is to start with the long verbose version and trim it down using matching patterns to remove plurals etc. I don't know that it is more radable though, so might be harder to maintain.

string=" $W weeks, $D days and $H hours" # leading space to simplify matching
string=${string/ 1 weeks/1 week}
string=${string/ 1 days/ 1 day}
string=${string/ 1 hours/ 1 hour}
string=${string# 0 weeks,}
string=${string/ 0 days/}
string=${string%and 0 hours}
string=${string/, and/ and}
string=${string# }
string=${string% }
string=${string%,}
string=${string#and }
[[ $string =~ and ]] || string=${string/,/ and}

This uses bash's parameter expansion syntax, where ${parameter/pattern/replacement} tries to match the glob pattern, and if found replaces it. This is used to "fix" the "errors" like "1 weeks" to make it "1 week". To avoid also matching 21 weeks, the initial string has an extra space at the start, so the space can be in the pattern to ensure only " 1 " is matched.

The syntax ${parameter#pattern} deletes the match if found at the beginning of the parameter. This is used to delete " 0 weeks,".

The syntax ${parameter%pattern} deletes the match if found at the end of the parameter. This is used to delete "and 0 hours". The other fixes delete a space if it gets left at the start or the end, or a comma if it gets left at the end, or an "and" and space if it gets left at the beginning.

The final line replaces comma by " and" if there is no "and" in the string. This corresponds to, say, changing "2 weeks, 3 days" to "2 weeks and 3 days", where we have already removed the "and 0 hours".

meuh
  • 6,624
3

If you're running the script under bash, how about this (based on Jeff Zeitlin's comment):

# Start with an empty list of units
units=()

Add the non-zero units to the list

if (( W == 1 )); then units+=("1 week") elif (( W > 1 )); then units+=("$W weeks") fi

if (( D == 1 )); then units+=("1 day") elif (( D > 1 )); then units+=("$D days") fi

if (( H == 1 )); then units+=("1 hour") elif (( H > 1 )); then units+=("$H hours") fi

Based on the number of non-zero units, add separators appropriately

case ${#units[@]} in 3) string="${units[0]}, ${units[1]} and ${units[2]}" ;; 2) string="${units[0]} and ${units[1]}" ;; 1) string="${units[0]}" ;; 0) string="less than an hour" ;; esac

Warning: this pretty much requires bash. zsh also has arrays, but it numbers the entries differently (starting at 1 rather than 0), so the final section would fail weirdly under zsh.

3

In the days before bash had [[ ]] expressions, case statements were used a lot for pattern matching. The list of if's can be converted into a single case statement (the + sign is just an arbitrary separator):

case $W+$D+$H in
0+0+0 ) string="" ;;
0+0+1 ) string="$H hour" ;;
0+1+0 ) string="$D day"  ;;
0+1+1 ) string="$D day and $H hour" ;;
1+0+0 ) string="$W week" ;;
1+0+1 ) string="$W week and $H hour" ;;
1+1+0 ) string="$W week and $D day" ;;
1+1+1 ) string="$W week, $D day and $H hour" ;;

0+0+* ) string="$H hours" ;; 0++0 ) string="$D days" ;; +0+0 ) string="$W weeks" ;; 0+1+* ) string="$D day and $H hours" ;; 0++1 ) string="$D days and $H hour" ;; +0+1 ) string="$W weeks and $H hour" ;; +1+0 ) string="$W weeks and $D day" ;; 1+0+ ) string="$W week and $H hours" ;; 1++0 ) string="$W week and $D days" ;; 1+1+ ) string="$W week, $D day and $H hours" ;; 1++1 ) string="$W week, $D days and $H hour" ;; +1+1 ) string="$W weeks, $D day and $H hour" ;;

0++ ) string="$D days and $H hours" ;; 1++ ) string="$W week, $D days and $H hours" ;; +0+ ) string="$W weeks and $H hours" ;; ++0 ) string="$W weeks and $D days" ;; +1+ ) string="$W weeks, $D day and $H hours" ;; ++1 ) string="$W weeks, $D days and $H hour" ;; ++* ) string="$W weeks, $D days and $H hours" ;; esac

This is still hard to digest, but if we separate out the plural words with variables holding an s or not, the case statement is a lot simpler:

ws=s; [ $W = 1 ] && ws=
ds=s; [ $D = 1 ] && ds=
hs=s; [ $H = 1 ] && hs=
case $W+$D+$H in
0+0+0 ) string="" ;;
0+0+* ) string="$H hour$hs" ;;
0+*+0 ) string="$D day$ds"  ;;
0+*+* ) string="$D day$ds and $H hour$hs" ;;
*+0+0 ) string="$W week$ws" ;;
*+0+* ) string="$W week$ws and $H hour$hs" ;;
*+*+0 ) string="$W week$ws and $D day$ds" ;;
*+*+* ) string="$W week$ws, $D day$ds and $H hour$hs" ;;
esac
meuh
  • 6,624
0

Bash supports functions, so maybe try a function. I sketched out (in pseudo code) a possible tack to take.

The "s" is easy to handle because it should only be singular if the value is one. The comma and the "and" are count-based decisions, so one function returns a string either empty or "properly pluralized", and the next function is used to increment a counter based on the empty-nature of the string.

The counter is then used to manipulate two separator strings and then the whole thing just get blindly assembled.

Pseudo code:

function foo ($int, $singular, $suffix, $ignoreZero) {
    $rval = "";
    if ( $ignoreZero && $int == 0 )  { return $rval; }
    if ( $int == 1 ) { $rval = $int + " " + $singular; }
    else { $rval = $int + " " + $singular + $suffix; }
    return $rval;
}
function bar($int, $str) {
   $rval = $int;
   if ( $str !="" ) { $rval = $int + 1; }
   return $rval;
}

$count = 0

$strW = foo($W, "week", "s", true); $count = bar($count, $strW);

$strD = foo($D, "day", "s", true); $count = bar($count, $strD);

$strH = foo($H, "hour", "s", true); $count = bar($count, $strH);

$sep1 = ""; $sep2 = " and ";

if ($count == 3) { $sep1=", ";} if ($count < 2) { $sep2=""; }

$emit = $strW + $sep1 + $strD + $sep2 + $strH

Yorik
  • 4,988