我找到了一份要做的项目清单,其中一个是会产生大量变化的项目。我做了这个代码:
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
这就是所有硬币选择代码 - 它经过优化以返回尽可能少的硬币。我不需要做任何事情就可以做到这一点,因为这就是case
shell 控制语句的工作原理 - 通过仅选择最早的可能匹配。因此,所需要做的就是将硬币按从大到小的顺序排列,并且迭代次数永远不会超过 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'