loop bash con incremento di 0,02


11

Voglio fare un ciclo for in bash con 0.02 come incrementi che ho provato

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

ma non ha funzionato.


9
Bash non fa matematica in virgola mobile.
Jordan

1
l'incremento può essere effettuato da bc, ma fermarsi su 4.52 potrebbe essere complicato. usare il suggerimento di @roaima, avere var ausiliario con passaggio di 2 e usarei=$(echo $tmp_var / 100 | bc)
Archemar


5
Normalmente non si desidera utilizzare float come indice di loop . Stai accumulando errori su ogni iterazione.
Isanae,

Risposte:


18

La lettura della bash pagina man fornisce le seguenti informazioni:

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

Innanzitutto, l'espressione aritmetica expr1viene valutata in base alle regole descritte di seguito in VALUTAZIONE ARITMETICA. [...]

e poi otteniamo questa sezione

VALUTAZIONE ARITMETICA

La shell permette di calcolare espressioni aritmetiche, sotto certe circostanze (vedi lete declarebuiltin comandi ed espansione aritmetica). La valutazione viene eseguita in numeri interi a larghezza fissa senza controllo per overflow [...]

Quindi si può vedere chiaramente che non è possibile utilizzare un forciclo con valori non interi.

Una soluzione potrebbe essere semplicemente quella di moltiplicare tutti i componenti del loop per 100, consentendo in questo modo di utilizzarli successivamente, in questo modo:

for ((k=400;k<542;k+=2))
do
    i=$(bc <<<"scale=2; $k / 100" )    # when k=402 you get i=4.02, etc.
    ...
done

Penso che questa sia la soluzione migliore in k=400;k<542;k+=2quanto evita anche potenziali problemi aritmetici in virgola mobile.
Huygens,

1
Si noti che per ogni iterazione nel ciclo, si crea una pipe (per leggere l'output di bc), fork un processo, si crea un file temporaneo (per la stringa here), si esegue bcin esso (il che implica il caricamento di un eseguibile e librerie condivise e inizializzandoli), attendere e ripulire. Correre bcuna volta per fare il ciclo sarebbe molto più efficiente.
Stéphane Chazelas,

@ StéphaneChazelas sì, d'accordo. Ma se questo è il collo di bottiglia, probabilmente stiamo comunque scrivendo il codice nella lingua sbagliata. OOI che è meno inefficiente (!)? i=$(bc <<< "scale...")oppurei=$(echo "scale..." | bc)
roaima,

1
Dal mio test rapido, la versione della pipe è più veloce in zsh (da dove <<<viene) bashe ksh. Nota che passare a un'altra shell che bashti darà un miglioramento delle prestazioni migliore rispetto all'utilizzo dell'altra sintassi comunque.
Stéphane Chazelas,

(e la maggior parte dei gusci che il supporto <<<(zsh, mksh, ksh93, yash) supportano anche aritmetica in virgola mobile ( zsh, ksh93, yash)).
Stéphane Chazelas,

18

Evitare anelli nei gusci.

Se vuoi fare l'aritmetica, 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

Si noti che awk(contrariamente a bc) funziona con la doublerappresentazione del numero in virgola mobile dei processori (probabilmente tipo IEEE 754 ). Di conseguenza, poiché quei numeri sono approssimazioni binarie di quei numeri decimali, potresti avere alcune sorprese:

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

Se aggiungi un OFMT="%.17g"puoi vedere il motivo della scomparsa 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 fa precisione arbitraria, quindi non ha questo tipo di problema.

Si noti che per impostazione predefinita (a meno che non si modifichi il formato di output con OFMTo si utilizzi printfcon specifiche di formato esplicite), gli awkutilizzi %.6gper visualizzare i numeri in virgola mobile, quindi passare a 1e6 e oltre per i numeri in virgola mobile superiori a 1.000.000 e troncare la parte frazionaria per i numeri alti (100000.02 sarebbe visualizzato come 100000).

Se si ha realmente bisogno di utilizzare un ciclo di shell, perché per esempio si desidera eseguire comandi specifici per ogni iterazione di quel ciclo, utilizzare una shell con galleggianti supporto aritmetica punto, come zsh, yasho ksh93o generare l'elenco dei valori con un comando come sopra (o seqse disponibile) e scorrere sopra la sua uscita.

Piace:

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 meno che non si spingano i limiti dei numeri in virgola mobile del processore, seqgestisce gli errori causati dalle approssimazioni in virgola mobile in modo più grazioso rispetto alla awkversione precedente.

Se non hai seq(un comando GNU), puoi renderne uno più affidabile come una funzione come:

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

Funzionerebbe meglio per cose del genere seq 100000000001 0.000000001 100000000001.000000005. Nota comunque che avere numeri con precisione arbitrariamente alta non sarà di grande aiuto se li passeremo a comandi che non li supportano.


Apprezzo l'uso di awk! +1
Pandya,

Perché è necessario unset IFSnel primo esempio?
user1717828

@utente1717828, idealmente, con quella divisione + invocazione glob, vogliamo dividere i caratteri di nuova riga. Possiamo farlo con IFS=$'\n'ma non funziona in tutte le shell. O IFS='<a-litteral-newline-here>'ma non è molto leggibile. Oppure possiamo dividere le parole invece (spazio, tabulazione, nuova riga) come si ottiene con il valore predefinito di $ IFS o se si disinserisce IFS e funziona anche qui.
Stéphane Chazelas,

@ user1717828: non abbiamo bisogno di scherzare IFS, perché sappiamo che seql'output non ha spazi su cui dobbiamo evitare di dividerci. È principalmente lì per assicurarti di capire che questo esempio dipende IFS, il che potrebbe essere importante per un diverso comando di generazione di elenchi.
Peter Cordes,

1
@PeterCordes, è lì, quindi non abbiamo bisogno di fare ipotesi su ciò che IFS era stato impostato in precedenza.
Stéphane Chazelas,


0

Come altri hanno suggerito, puoi usare bc:

i="4.00"

while [[ "$(bc <<< "$i < 5.42")" == "1" ]]; do
    # do something with i
    i="$(bc <<< "$i + 0.02")"
done
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.