我使用 while 循环错误吗?

我使用 while 循环错误吗?

我找到了一份要做的项目清单,其中一个是会产生大量变化的项目。我做了这个代码:

getamt() {
echo "Enter amount of money."
read amount
echo "OK."
}
change() {
amount=$(echo "$amount*100" | bc)
quarter=$(echo "($amount-25)" | bc)
dime=$(echo "($amount-10)" | bc)
nickel=$(echo "($amount-5)" | bc)
penny=$(echo "($amount-1)" | bc )
quarter=${quarter%???}
dime=${dime%???}
nickel=${nickel%???}
penny=${penny%???}
amount=${amount%???}
qNum=0
dNum=0
nNum=0
pNum=0
}

getchange() {
while [ $quarter -ge 0 ]
do
qNum=$(( qNum+1 ))
amount=$(( $amount-25 ))
done
while [ $dime -ge 0 ]
do
dNum=$(( dNum+1 ))
amount=$(( $amount-10 ))
done
while [ $nickel -ge 0 ]
do
nNum=$(( nNum+1 ))
amount=$(( $amount-5 ))
done
while [ $penny -ge 0 ]
do
pNum=$(( nNum+1 ))
amount=$(( $amount-1 ))
done
}

display() {
echo "Your change is:"
echo "$qNum quarters"
echo "$dNum dimes"
echo "$nNum nickels"
echo "$pNum pennies"
}

getamt
change
getchange
display

我知道这可能是做我需要做的事情的一个糟糕方法,但它被卡住了。我想我可能while错误地使用了循环,但我不知道。我使用while循环的目标是检查是否可以在其中添加另一种类型的硬币,因此它检查该值是否大于零。

答案1

您的代码最明显的问题是所有while循环都检查循环内从未更改的变量(例如$quarter),因此循环条件永远不会变为假,并且循环会无限重复。

让我们看一下其中一个循环:

while [ $quarter -ge 0 ]
do
qNum=$(( qNum+1 ))
amount=$(( $amount-25 ))
done

如果$quarter> 0,则控制流进入循环,$qNum递增并$amount递减,但$quarter保持不变,因此您将进行另一个循环迭代。


通过重构代码来修复代码效果最好:

  • 不要依赖像amount设置为函数副作用的全局变量,而是重写函数以接受参数并将其结果输出到stdout(如果可能)。

    • 结果stdout:您的函数getamt()可以echo $amount不依赖amount可用(且不变)来稍后在脚本中进行处理。无论调用什么,getamt都可以将此输出捕获到带有 的变量中amount=$(getamt)
      不幸的是,当函数需要返回多个值时,这不起作用 - 在这种情况下,您可以让函数打印其返回值,并用换行符或您知道不会出现在值中的字符分隔。你甚至可以选择像这样的输出格式

      quarter=3  
      dime=1  
      nickel=4  
      

      并评估该输出以使用函数的返回值设置局部变量:$(yourfunction); echo $quarter

    • 参数:您的函数change()可以将其应计算的更改量作为参数(即您将调用amount 2.50)而不是从全局变量中读取它。您可以通过索引访问为函数(或脚本,取决于上下文)提供的参数:$1第一个参数、$2第二个参数等。

  • 您可以避免拨打几次电话bc您可以通过删除小数位一次之后只使用 bash 算术评估。您目前的替补${quarter%???}也会删除任何最后三个字符,如果您的用户决定输入多于(或少于)两位小数的值,这将产生不需要的结果。使用类似的方法${quarter%%.*}删除第一个之后(包括)的所有内容.

  • 使用注释(以 a 开头#,一直持续到行尾):
    例如,amount=${amount%%.*} # remove decimal places
    您的大部分代码现在对您来说似乎很明显,但对于其他人来说可能并不明显,而且它也不会很明显当你几个月后不得不再次查看它时,你就不再需要它了。

  • 老实说,我不完全确定您的脚本应该如何计算目前要返回的硬币数量。计算找零的最常见方法是贪婪算法,该算法从最高可用硬币价值开始,分配与找零金额“相符”的尽可能多的该价值的硬币1,从找零金额中减去这些硬币的总价值,然后继续使用下一个(较小的)硬币价值,依此类推,直到找零金额达到0(即已经分配了足够的硬币来弥补总找零金额)。
    1要计算硬币数量,您可以查看模运算或者只是循环地从找零金额中减去当前硬币价值,直到找零金额小于硬币价值(即,如果您分发了当前价值的另一枚硬币,您将返回太多找零)。

答案2

下面的两个 shell 函数中,实际的数学计算在这里完成:

while   set     "${1#0?}" "${1#?}"
        shift   "$((!${#1}))"
        [ "${1:-0}" -gt 0 ]
do      case    $1 in   ([3-9]?|2[5-9])
                set "$(($1%25))" "$((q+=$1/25))";;
        (??)    set "$(($1%10))" "$((d=$1/10))" ;;
        (?)     set "" "$((p=$1-(5*(n=$1>=5))))";;
esac;   done

这就是所有硬币选择代码 - 它经过优化以返回尽可能少的硬币。我不需要做任何事情就可以做到这一点,因为这就是caseshell 控制语句的工作原理 - 通过仅选择最早的可能匹配。因此,所需要做的就是将硬币按从大到小的顺序排列,并且迭代次数永远不会超过 3 次。

上述唯一困难的部分是在 08 和 09 的情况下保护可移植 shell 数学不会将结果误解为八进制。这是通过在每次循环运行时挤掉任何前导零来处理的。

事实上,考虑到您的既定目标是从交互式用户处获取输入并向其提供输出,下面的大多数功能主要集中在输入验证和错误报告上。这也是一些重要的东西 - 特别是在涉及 shell 数学的情况下。因为 shell 数学本质上是一个由两部分组成的eval运算,所以当您将用户输入放入算术语句中时,您可能应该首先确保您知道其中的内容。

case再次,这是我处理此类事情的首选形式。

_err()( unset   parm    msg     IFS     \
                "${1##*[!_[:alnum:]]*}" || exit
        parm=$1 IFS=$2  msg=$3; shift   3
        eval ': "${'"$parm?\"'\$*' can't be right. \$msg"'"}"'
)
_chg()  if      set -- "${1#"${1%%[!0]*}"}.${2%"${2#??}"}${3+.}" "$@" &&
                case    $1      in
                (*.*.*) shift
                        _err    too_many_dots   .       "
                        We're fresh out of microcoins."         "$@"    ;;
                (-*)    shift
                        _err    nice_try_pal    .       "
                        Change isn't magic money, you know."    "$@"    ;;
                (*[!0-9.]*)     shift
                        _err    i_hate_poetry   .       "
                        We only spend numbers around here."     "$@"    ;;
                (.00|.0|.)      shift
                        _err    that_was_easy   .       "
                        Next time try spending something."      0 00    ;;
                esac || return
        then    set     "${1##*.}" "$((q=(${1%%.*}0*4)/10+(d=(n=(p=0)))))"
                while   set     "${1#0?}" "${1#?}"
                        shift   "$((!${#1}))"
                        [ "${1:-0}" -gt 0 ]
                do      case    $1 in   ([3-9]?|2[5-9])
                        set "$(($1%25))" "$((q+=$1/25))";;
                (??)    set "$(($1%10))" "$((d=$1/10))" ;;
                (?)     set "" "$((p=$1-(5*(n=$1>=5))))";;
                esac;   done
                set     quarter q dime d nickel n penny p
                echo    Your change is:
                while   [ "$#" -gt 1 ]
                do      printf "\t$1 coins:\t$(($2))\n"
                        shift   2
        done;   fi

但它实际上没有read任何输入,并且仅将输入作为命令行参数。您可以通过以下方式提取用户输入:

printf '\n$  '; IFS=. read -r dollars cents dot

并直接传递它,就像......

_chg "$dollars" "$cents" ${dot:+""}

...其余的一切都应该是自动的。

_err()函数是我编写的一个可重用函数,您可以在此处或其他地方使用它来报告错误并获得正确的返回值。当您展开时,unset ${var?expansion form}shell 将打印扩展形式到 stderr 并突然退出并显示错误状态。对于您可能想要自己处理的类型的测试来说,这种行为通常效果不佳,但是如果您知道必须满足某些条件才能使该unset参数完全扩展,这绝对是一个条件,这意味着您的过程应该死了,那么这可能是一个非常方便的方法。这是因为 shell 以自己的标准方式格式化所有输出(您的交互式 shell 用户可能已经习惯了),并立即处理您的退出代码。

例如:

bash -c '. ~/coins.sh
         _err parameter_name \
              -splitter      \
              "Some custom message that is also thrown in." \
              and my entire input arg array
'

...在命令行运行时返回 1 并打印到 stderr...

/home/mikeserv/coins.sh: line 5: parameter_name: 'and-my-entire-input-arg-array' can't be right. Some custom message that is also thrown in.

因此,整个上半部分_chg()都致力于验证输入是否正确,或者在不正确时返回错误条件和错误输出。

当一切顺利时,最后一个季度专门用于格式化标准输出,例如:

sh -c '. ~/coins.sh; _chg 10 97'
Your change is:
    quarter coins:  43
    dime coins:     2
    nickel coins:   0
    penny coins:    2

答案3

另一个答案已经解决了您的具体问题。经过一段时间的尝试后,我犹豫了。因此,这是您可以考虑的另一种方法 - 使用一个while又一个until又一个for循环。数组有助于简化代码。

 echo "Enter amount of money: $.c or just $"
 read amount
 echo

 a=(${amount/./ })  # change '.' to ' ' and make an array: a[0], a[1]
 da=${a[0]}         # dollar-amount 
 pa=$((10#${a[1]})) # penny-amount
 cv=(25 10 5 1)     # array of coin-values  cv[0] ... cv[3] - q d n p
 cc=(\  \  \  \ )   # array of coin-counts  cc[0] ... cc[3] - q d n p
 cn=( quarters dimes nickels pennies ) # array of coin-names
 while (( pa > 0 )); do
     for (( i=0; i<${#cv[@]}; i++ )); do  # process coin-types from hi-val to lo-val
         (( (pa-cv[i]) < 0 )) && continue # look-ahead: don't give too much change
         (( (pa-=cv[i]) ))                # decrement penny-amount
         (( cc[i]+=1 ))                   # increment coin-type counters
     done
 done
 # 'paste' arrrays side by side, and tabulate via 'column' 
 echo "Your coins change is:"  # and show only relevant coins via 'sed'  
 column -t <(paste  <(printf '%s\n' "${cn[@]}") \
                    <(printf '%s\n' "${cc[@]}")) | sed -n '/[0-9]/p' 

相关内容