loop bash com incremento de 0,02

11

Eu quero fazer um loop for no bash com 0,02 como incrementos eu tentei

for ((i=4.00;i<5.42;i+=0.02))
do
commands
done

mas não funcionou.

Mehrshad
fonte
9
Bash não faz matemática de ponto flutuante.
Jordanm #
1
pode ser feito um incremento bc, mas parar no 4.52 pode ser complicado. usar @roaima sugestão, temos var auxiliar com o passo de 2, e a utilizaçãoi=$(echo $tmp_var / 100 | bc)
Archemar
5
Você normalmente não deseja usar flutuadores como um índice de loop . Você está acumulando erro em cada iteração.
Isanae 01/09/2015

Respostas:

18

A leitura da bash página de manual fornece as seguintes informações:

for (( expr1 ; expr2 ; expr3 )) ; do list ; done

Primeiro, a expressão aritmética expr1é avaliada de acordo com as regras descritas abaixo em AVALIAÇÃO ARITMÉTICA. [...]

e então temos esta seção

AVALIAÇÃO ARITMÉTICA

O shell permite expressões aritméticas a serem avaliadas, sob certas circunstâncias (veja o lete declarebuiltin comandos e Expansão Aritmética). A avaliação é feita em números inteiros de largura fixa, sem verificação de estouro [...]

Portanto, pode-se ver claramente que você não pode usar um forloop com valores não inteiros.

Uma solução pode ser simplesmente multiplicar todos os seus componentes de loop por 100, permitindo que você os use posteriormente, desta forma:

for ((k=400;k<542;k+=2))
do
    i=$(bc <<<"scale=2; $k / 100" )    # when k=402 you get i=4.02, etc.
    ...
done
roaima
fonte
Eu acho que essa é a melhor solução, k=400;k<542;k+=2pois evita possíveis problemas aritméticos de ponto flutuante.
Huygens
1
Observe que, para cada iteração no loop, você cria um canal (para ler a saída de bc), bifurca um processo, cria um arquivo temporário (para a cadeia de caracteres here), executa bcnele (o que implica carregar bibliotecas executáveis ​​e compartilhadas e inicializá-los), aguarde e limpe. Executar bcuma vez para executar o loop seria muito mais eficiente.
Stéphane Chazelas
@ StéphaneChazelas sim, concordou. Mas se esse é o gargalo, provavelmente estamos escrevendo o código no idioma errado de qualquer maneira. OOI que é menos ineficiente (!)? i=$(bc <<< "scale...")oui=$(echo "scale..." | bc)
roaima 02/09
1
No meu teste rápido, a versão do pipe é mais rápida no zsh (de onde <<<vem) bashe ksh. Observe que alternar para outro shell que bashlhe dará um aumento de desempenho melhor do que usar a outra sintaxe.
Stéphane Chazelas
(e a maioria das conchas que o apoio <<<(zsh, mksh, ksh93, yash) também suportam aritmética de vírgula flutuante ( zsh, ksh93, yash)).
Stéphane Chazelas
18

Evite loops nas conchas.

Se você deseja fazer aritmética, use awkou bc:

awk '
  BEGIN{
    for (i = 4.00; i < 5.42; i+ = 0.02)
      print i
  }'

Ou

bc << EOF
for (i = 4.00; i < 5.42; i += 0.02)  i
EOF

Observe que awk(ao contrário de bc) funciona com a doublerepresentação do número de ponto flutuante dos seus processadores (provavelmente do tipo IEEE 754 ). Como resultado, como esses números são aproximações binárias desses números decimais, você pode ter algumas surpresas:

$ gawk 'BEGIN{for (i=0; i<=0.3; i+=0.1) print i}'
0
0.1
0.2

Se você adicionar um, OFMT="%.17g"poderá ver o motivo da falta 0.3:

$ gawk 'BEGIN{OFMT="%.17g"; for (i=0; i<=0.5; i+=0.1) print i}'
0
0.10000000000000001
0.20000000000000001
0.30000000000000004
0.40000000000000002
0.5

bc faz precisão arbitrária e não tem esse tipo de problema.

Observe que, por padrão (a menos que você modifique o formato de saída com OFMTou use printfespecificações de formato explícitas), awkuse %.6gpara exibir números de ponto flutuante, portanto, alternaria para 1e6 e acima para números de ponto flutuante acima de 1.000.000 e truncaria a parte fracionária para números altos (100000.02 seria exibido como 100000).

Se você realmente precisa usar um loop shell, porque, por exemplo, você deseja executar comandos específicos para cada iteração desse loop, use um shell com flutuante apoio aritmética de ponto como zsh, yashou ksh93ou gerar a lista de valores com um comando como acima (ou, seqse disponível) e faça um loop sobre sua saída.

Gostar:

unset -v IFS # configure split+glob for default word splitting
for i in $(seq 4 0.02 5.42); do
  something with "$i"
done

Ou:

seq 4 0.02 5.42 | while IFS= read i; do
  something with "$i"
done

a menos que você ultrapasse os limites dos números de ponto flutuante do processador, seqlida com os erros incorridos pelas aproximações de ponto flutuante com mais graciosidade do que a awkversão acima.

Se você não possui seq(um comando GNU), pode torná-lo mais confiável como uma função como:

seq() { # args: first increment last
  bc << EOF
    for (i = $1; i <= $3; i += $2) i
EOF
}

Isso funcionaria melhor para coisas como seq 100000000001 0.000000001 100000000001.000000005. Observe, no entanto, que ter números com precisão arbitrariamente alta não ajudará muito se vamos passá-los para comandos que não os suportam.

Stéphane Chazelas
fonte
Eu aprecio o uso do awk! +1
Pandya
Por que você precisa unset IFSno primeiro exemplo?
User1717828
@ user1717828, idealmente, com essa chamada split + glob, queremos dividir em caracteres de nova linha. Podemos fazer isso com, IFS=$'\n'mas isso não funciona em todas as conchas. Ou IFS='<a-litteral-newline-here>'mas isso não é muito legível. Ou então, podemos dividir as palavras (espaço, tabulação, nova linha) como você obtém com o valor padrão de $ IFS ou se você desarmar o IFS e também trabalhar aqui.
Stéphane Chazelas
@ user1717828: não precisamos mexer IFS, porque sabemos que seqa saída não possui espaços em que precisamos evitar a divisão. Está presente principalmente para garantir que você saiba que esse exemplo depende IFS, o que pode ser importante para um comando de geração de lista diferente.
Peter Cordes
1
@ PeterCordes, está lá, então não precisamos assumir que o IFS foi definido anteriormente.
Stéphane Chazelas
2

Use "seq" - imprima uma sequência de números

seq PRIMEIRO INCREMENTO ÚLTIMO

for i in $(seq 4.00 0.02 5.42)
do 
  echo $i 
done
borzol
fonte
Essa resposta já foi dada .
Stéphane Chazelas
0

Como outros sugeriram, você pode usar o bc:

i="4.00"

while [[ "$(bc <<< "$i < 5.42")" == "1" ]]; do
    # do something with i
    i="$(bc <<< "$i + 0.02")"
done
Andy Dalton
fonte