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)をチェックするため、ループ条件が決して偽にならず、ループが無限に繰り返されることです。

次のループの1つを見てみましょう。

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

> 0 の場合、$quarter制御フローはループに入り、増減$qNum$amountますが、$quarter同じままであるため、別のループ反復を実行します。


コードの修正はリファクタリングによって最適です。

  • amountグローバル変数が関数の副作用に設定されることに依存するのではなく、引数を受け入れ、その結果をstdout(可能であれば)出力するように関数を再構築してください。

    • 結果stdout:関数は、使用可能かどうかにかかわらず、後でスクリプトで処理getamt()できます(変更されていません)。何を呼び出しても、この出力はを使用して変数としてキャプチャできます。残念ながら、関数が複数の値を返す必要がある場合は機能しません。この場合、関数は戻り値を改行文字で区切って印刷することができ、値に表示されない文字を表示できます。このような出力形式を選択することもできますecho $amountamountgetamtamount=$(getamt)

      quarter=3  
      dime=1  
      nickel=4  
      

      そして、その出力を評価し、関数の戻り値を使用してローカル変数を設定します。$(yourfunction); echo $quarter

    • パラメータ:関数は、グローバル変数から読み取るのではなく、パラメータchange()(つまり呼び出す)で計算する必要がある変更を取ることができます。最初の引数、2番目の引数などのamount 2.50インデックスを使用して、関数(またはコンテキストに従ってスクリプト)に指定された引数にアクセスできます。$1$2

  • bc小数点以下の桁数を削除すると、複数の呼び出しを避けることができます一度次に、bash算術評価を使用します。現在の交換アイテム${quarter%???}も削除されます。どの最後の3文字は、ユーザーが小数点の2桁より大きいまたは小さい値を入力することを決定した場合、望ましくない結果を生成します。同様の方法で${quarter%%.*}最初の項目以降のすべての項目を削除します.

  • コメントを使用する(aで始まり#行末まで続く):
    たとえば、amount=${amount%%.*} # remove decimal places
    ほとんどのコードは今は明確に見えるかもしれませんが、他の人にははっきりしないかもしれません。数ヶ月後にもう一度見直す必要があり、もう必要ありません。

  • 正直なところ、あなたのスクリプトが現在返すコインの数をどのように計算するのかは完全にわかりません。変化量を計算する最も一般的な方法は、利用可能な最高のコイン値から始まり、その値の変化量と「一致する」可能な限り多くのコインを配布するためのグリディアルゴリズムです。1、変更金額からこれらのコインの合計値を差し引いた後、変更金額がゼロに達するまで次の(より小さい)コイン値に進みます(つまり、全体の変更金額を構成するのに十分なコインが配布されました)。
    1確認できるコイン数を計算するにはモジュロ算術または、変更量がコイン値よりも小さくなるまでループを実行して、変更量から現在のコイン値を減算します(つまり、現在の値の他のコインを除算すると、余りにも多くの残高が返されます)。

答え2

次の2つのシェル関数では、実際の数学的計算はここで行われます。

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シェル制御ステートメントが機能する方法なので、これには何もする必要はありません。できるだけ早い一致を選択するだけです。したがって、必要なのは、コインを最大から最小の順に並べることであり、反復回数は3を超えない。

上記の唯一の難しい部分は、08と09の場合、結果を8進数として誤って解釈しないように移植可能なシェル数学を保護することです。これは、ループが実行されるたびに前のゼロを削除することによって処理されます。

実際、以下の機能の多くは入力検証とエラー報告に焦点を当てています。指定された目標は、対話型ユーザーから入力を受け取り、出力を提供することです。これもいくつかの重要なことです。特に、シェル数学が関係する場合にはさらにそうです。シェル数学は本質的に2つの部分からなる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}シェルが印刷されます拡張型stderrに移動し、エラー状態で突然終了します。この動作は通常、直接処理したいテストの種類にはうまく機能しませんが、そのunsetパラメータを完全に拡張するために特定の条件を満たす必要があることがわかっている場合、これは確かにプロセスを意味する条件です。しなければならないもしそうなら、これは非常に便利な方法です。これは、シェルが独自の標準的な方法ですべての出力フォーマットを指定するためです。(対話式シェルユーザーはすでにこれに慣れている可能性があります)、終了コードをすぐに処理してください。

たとえば、

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異なる。配列はコードを単純化するのに役立ちます。untilfor

 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' 

関連情報