Estou usando um loop while errado?

2

Eu encontrei uma lista de projetos para fazer, e um deles foi algo que gera quantidades de mudança. Eu fiz este código:

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

Eu sei que é provavelmente uma maneira ruim de fazer o que eu preciso fazer, mas está ficando preso. Acho que usei o while loop errado, mas não sei. Meu objetivo ao usar os while loops era verificar se é possível adicionar outro tipo dessa moeda lá, então ele verifica se o valor está acima de zero.

    
por 智障的人 27.06.2015 / 23:28

3 respostas

7

A questão mais óbvia do seu código é que todos os while loops verificam uma variável (por exemplo, $quarter ) que nunca é alterada dentro do loop, portanto a condição de loop nunca pode se tornar falsa e o loop se repete indefinidamente. p>

Vamos dar uma olhada em um dos loops:

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

Se $quarter > 0, o fluxo de controle entra no loop, $qNum é incrementado e $amount é decrementado, mas $quarter permanece inalterado, então você está em outra iteração de loop.

Corrigir seu código funciona melhor reestruturando-o:

  • Em vez de confiar em variáveis globais como amount , que são definidas como efeitos colaterais de funções, reescreva suas funções para aceitar parâmetros e exibir seus resultados em stdout (quando possível).

    • Resultados para stdout : Sua função getamt() poderia echo $amount em vez de depender de amount estar disponível (e inalterado) para processamento posterior no script. Quaisquer chamadas getamt podem capturar essa saída em uma variável com amount=$(getamt) .
      Infelizmente, isso não funciona tão bem quando uma função precisa retornar vários valores - nesse caso, você pode fazer com que a função imprima seus valores de retorno separados por novas linhas ou um caractere que você sabe que não aparecerá nos valores. Você pode até ir para um formato de saída como

      quarter=3  
      dime=1  
      nickel=4  
      

      e avalie essa saída para definir variáveis locais com os valores de retorno da função: $(yourfunction); echo $quarter

    • Parâmetros: Sua função change() poderia receber a quantidade de alterações que deveria calcular como um parâmetro (ou seja, você chamaria amount 2.50 ) em vez de lê-la a partir de uma variável global. Você pode acessar parâmetros dados à sua função (ou ao seu script, dependendo do contexto) através de seus índices: $1 para o primeiro parâmetro, $2 para o segundo, etc.

  • Você pode evitar algumas chamadas para bc apenas cortando as casas decimais uma vez e usando apenas a avaliação aritmética bash depois disso. Sua substituição atual ${quarter%???} também remove qualquer últimos três caracteres, o que produzirá resultados indesejados se os usuários decidirem inserir um valor com mais (ou menos) do que duas casas decimais. Use algo como ${quarter%%.*} para remover tudo após (e incluindo) o primeiro . .

  • Use comentários (iniciado com # e continuado até o final da linha):
    por exemplo. amount=${amount%%.*} # remove decimal places
    A maior parte do seu código parecerá óbvia para você agora, mas pode não ser óbvia para qualquer outra pessoa que esteja olhando para ele, e também não será mais óbvio para você quando você precisar vê-lo novamente em alguns meses .

  • Para ser honesto, não sei ao certo como o seu script deve calcular o número de moedas a serem devolvidas no momento. A abordagem mais comum para calcular a mudança seria um algoritmo guloso que começa no valor de moeda mais alto disponível, dispensa tantas moedas desse valor como "adequado" à quantia de alteração 1 , subtrai o valor total dessas moedas do valor da alteração, depois continua com o valor da moeda seguinte (menor) e assim por diante, até que o valor da alteração atinja 0 (ou seja, moedas suficientes foram distribuídas para compensar o valor total da alteração)
    1 Para calcular esse número de moedas, você pode consultar modulo operations ou apenas subtrai o valor atual da moeda do valor da mudança em um loop até que a quantia da alteração seja menor que o valor da moeda (ou seja, você retornaria muita alteração se dispensasse outra moeda da corrente valor).

por 28.06.2015 / 01:20
3

Das duas funções do shell abaixo, a matemática real é feita aqui:

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

Esse é todo o código de seleção de moedas - é otimizado para retornar o menor número de moedas possível. E eu não precisei fazer nada para fazer isso porque é assim que a instrução de controle case shell funciona - selecionando apenas a correspondência mais antiga possível. Então, tudo o que é necessário é colocar as moedas em ordem, do maior para o menor, e você nunca excederá 3 iterações.

A única parte difícil sobre o acima é proteger a matemática shell portátil de interpretar erroneamente os resultados como um octal no caso de 08 e 09. Isso é feito espremendo qualquer zeros à esquerda, uma vez que cada loop é executado.

Na verdade, considerando que o objetivo declarado é fornecer dados e fornecer saída a um usuário interativo, a maioria das funções abaixo está focada principalmente na validação de entrada e no relatório de erros. Isso é algo importante também - especialmente quando se trata de matemática de shell. Como a matemática do shell é essencialmente uma operação de eval de 2 partes, quando você insere a entrada do usuário em uma instrução aritmética, você provavelmente deve primeiro garantir que você saiba o que tem lá.

case é, mais uma vez, meu formato goto para essas coisas.

_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

Na verdade, ele não aceita read de nenhuma entrada e aceita apenas entradas como argumentos de linha de comando. Você pode puxar a entrada do usuário com algo como:

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

E passe isso diretamente como ...

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

... e todo o resto deve ser automático.

A função _err() é uma função reutilizável que você pode usar aqui ou em outro lugar para gerar relatórios de erro com devida devolução. Quando você expande um unset ${var?expansion form} , o shell irá imprimir formulário de expansão para stderr e sair abruptamente com o status de erro. Este não é um comportamento que normalmente funciona bem para testes do tipo que você gostaria de manipular sozinho, mas se você sabe que alguma condição que deve ser atendida para que o parâmetro unset seja expandido é definitivamente uma condição o que significa que seu processo deve morrer, então pode ser uma maneira muito conveniente de ir. Isso ocorre porque o shell formata toda a saída em sua própria forma padrão (para a qual seu usuário do shell interativo provavelmente já está acostumado) , e lida com seu código de saída de uma só vez.

Por exemplo:

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

... quando executado na linha de comando retorna 1 e imprime para 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.

E assim toda a metade superior de _chg() é dedicada a verificar se a entrada é o que deveria ser ou se retorna uma condição de erro e uma saída de erro quando não é.

O último trimestre do mesmo é dedicado à formatação do stdout quando tudo vai bem como:

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

Outra resposta abordou seu problema específico. Eu parei depois de um tempo tentando resolvê-lo. Então, aqui está uma outra abordagem a ser considerada - com um loop while e um until e um for . Arrays ajudam a simplificar o código.

 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' 
    
por 28.06.2015 / 01:53