bucle bash con incremento de 0.02

11

Quiero hacer un bucle for en bash con 0.02 como incrementos intenté

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

Pero no funcionó.

Mehrshad
fuente
99
Bash no hace matemática de coma flotante.
jordanm
1
puede hacerse un incremento bc, pero detenerse en 4.52 puede ser complicado. use la sugerencia de @roaima, tenga una var auxiliar con el paso 2 y usei=$(echo $tmp_var / 100 | bc)
Archemar el
55
Normalmente no desea usar flotantes como índice de bucle . Estás acumulando errores en cada iteración.
isanae

Respuestas:

18

La lectura de la bash página del manual proporciona la siguiente información:

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

Primero, la expresión aritmética expr1se evalúa de acuerdo con las reglas que se describen a continuación en EVALUACIÓN ARITMÉTICA. [...]

y luego obtenemos esta sección

EVALUACIÓN ARITMÉTICA

La cáscara permite expresiones aritméticas a ser evaluados, en ciertas circunstancias (ver el lety declareorden interna comandos y expansión aritmética). La evaluación se realiza en enteros de ancho fijo sin verificación de desbordamiento [...]

Por lo tanto, se puede ver claramente que no puede usar un forbucle con valores no enteros.

Una solución puede ser simplemente multiplicar todos sus componentes de bucle por 100, permitiendo esto donde más tarde los use, así:

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
fuente
Creo que esta es la mejor solución, ya k=400;k<542;k+=2que evita posibles problemas aritméticos de coma flotante.
Huygens
1
Tenga en cuenta que para cada iteración en el bucle, crea una tubería (para leer la salida de bc), bifurca un proceso, crea un archivo temporal (para la cadena here), ejecuta bcen él (lo que implica cargar un archivo ejecutable y bibliotecas compartidas y inicializándolos), espere y limpie. Correr bcuna vez para hacer el ciclo sería mucho más eficiente.
Stéphane Chazelas
@ StéphaneChazelas sí, de acuerdo. Pero si este es el cuello de botella, entonces probablemente estamos escribiendo el código en el idioma incorrecto de todos modos. OOI que es menos ineficiente (!)? i=$(bc <<< "scale...")oi=$(echo "scale..." | bc)
roaima
1
Desde mi prueba rápida, la versión de tubería es más rápida en zsh (de dónde <<<viene) bashy ksh. Tenga en cuenta que cambiar a otro shell que bashle dará un mejor impulso de rendimiento que usar la otra sintaxis de todos modos.
Stéphane Chazelas
(y la mayoría de las conchas que el apoyo <<<(zsh, mksh, ksh93, Yash) también apoyan la aritmética de punto flotante ( zsh, ksh93, yash)).
Stéphane Chazelas
18

Evite bucles en conchas.

Si quieres hacer aritmética, usa awko bc:

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

O

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

Tenga en cuenta que awk(al contrario de bc) funciona con la doublerepresentación de números de coma flotante de sus procesadores (probablemente tipo IEEE 754 ). Como resultado, dado que esos números son aproximaciones binarias de esos números decimales, puede tener algunas sorpresas:

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

Si agrega un OFMT="%.17g"puede ver el motivo de la 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 tiene una precisión arbitraria, por lo que no tiene este tipo de problema.

Tenga en cuenta que, de manera predeterminada (a menos que modifique el formato de salida OFMTo use printfespecificaciones de formato explícito), se awkusa %.6gpara mostrar números de coma flotante, por lo que cambiaría a 1e6 y superior para números de coma flotante superiores a 1,000,000 y truncaría la parte fraccionaria para números altos (100000.02 se mostrará como 100000).

Si realmente necesita usar un bucle cáscara, porque, por ejemplo, desea ejecutar comandos específicos para cada iteración de este bucle, o bien utilizar un depósito de flotación apoyo aritmética de punto como zsh, yasho ksh93o generar la lista de valores con un comando que el anterior (o seqsi está disponible) y repita su salida.

Me gusta:

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

O:

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

a menos que supere los límites de los números de coma flotante de su procesador, seqmaneja los errores incurridos por aproximaciones de coma flotante con más gracia que la awkversión anterior.

Si no tiene seq(un comando GNU), puede hacer uno más confiable como una función como:

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

Eso funcionaría mejor para cosas como seq 100000000001 0.000000001 100000000001.000000005. Sin embargo, tenga en cuenta que tener números con una precisión arbitrariamente alta no ayudará mucho si vamos a pasarlos a comandos que no los admiten.

Stéphane Chazelas
fuente
Agradezco el uso de awk! +1
Pandya
¿Por qué lo necesitas unset IFSen el primer ejemplo?
user1717828
@ user1717828, idealmente, con esa invocación split + glob, queremos dividir en caracteres de nueva línea. Podemos hacerlo con eso, IFS=$'\n'pero eso no funciona en todos los shells. O IFS='<a-litteral-newline-here>'eso no es muy legible. O podemos dividirnos en palabras (espacio, tabulación, nueva línea) como se obtiene con el valor predeterminado de $ IFS o si desarma IFS y también funciona aquí.
Stéphane Chazelas
@ user1717828: no necesitamos meternos con él IFS, porque sabemos que seqla salida no tiene espacios en los que debemos evitar dividirnos. Principalmente está ahí para asegurarse de que se dé cuenta de que este ejemplo depende IFS, lo que podría ser importante para un comando de generación de lista diferente.
Peter Cordes
1
@PeterCordes, está ahí, por lo que no necesitamos hacer ninguna suposición sobre lo que IFS se configuró de antemano.
Stéphane Chazelas
2

Use "seq" - imprima una secuencia de números

seq PRIMER INCREMENTO ÚLTIMO

for i in $(seq 4.00 0.02 5.42)
do 
  echo $i 
done
borzole
fuente
Esa respuesta ya ha sido dada .
Stéphane Chazelas
0

Como otros han sugerido, puede usar bc:

i="4.00"

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