Bash-Schleife mit 0,02 Inkrement

11

Ich möchte eine for-Schleife in Bash mit 0,02 als Inkremente erstellen, die ich versucht habe

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

aber es hat nicht funktioniert.

Mehrshad
quelle
9
Bash macht keine Gleitkomma-Mathematik.
Jordanm
1
Inkrementieren kann durch erfolgen bc, aber das Stoppen auf 4.52 kann schwierig sein. Verwenden Sie @roaima Vorschlag, haben Sie Hilfs-Var mit Schritt 2, und verwenden Siei=$(echo $tmp_var / 100 | bc)
Archemar
5
Normalerweise möchten Sie Floats nicht als Schleifenindex verwenden . Sie sammeln bei jeder Iteration Fehler.
Isanae

Antworten:

18

Das Lesen der bash Manpage enthält folgende Informationen:

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

Zunächst wird der arithmetische Ausdruck expr1gemäß den nachstehend unter ARITHMETISCHE BEWERTUNG beschriebenen Regeln ausgewertet. [...]

und dann bekommen wir diesen Abschnitt

ARITHMETISCHE BEWERTUNG

Die Shell ermöglicht arithmetische Ausdrücke unter bestimmten Umständen ausgewertet werden (siehe letund declarebuiltin Befehle und Arithmetik Expansion). Die Auswertung erfolgt in Ganzzahlen mit fester Breite ohne Überprüfung auf Überlauf [...]

Es ist also deutlich zu erkennen, dass Sie keine forSchleife mit nicht ganzzahligen Werten verwenden können.

Eine Lösung kann einfach darin bestehen, alle Ihre Schleifenkomponenten mit 100 zu multiplizieren, um dies dort zu ermöglichen, wo Sie sie später verwenden, wie folgt:

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
quelle
Ich denke, dies ist die beste Lösung, k=400;k<542;k+=2da dadurch auch potenzielle Gleitkomma-Rechenprobleme vermieden werden.
Huygens
1
Beachten Sie, dass Sie für jede Iteration in der Schleife eine Pipe erstellen (um die Ausgabe von zu lesen bc), einen Prozess verzweigen, eine temporäre Datei (für die Here-Zeichenfolge) erstellen und darin ausführen bc(was das Laden einer ausführbaren und gemeinsam genutzten Bibliotheken und impliziert) initialisieren), warten und aufräumen. bcEinmal laufen , um die Schleife zu machen, wäre viel effizienter.
Stéphane Chazelas
@ StéphaneChazelas ja, stimmte zu. Aber wenn dies der Engpass ist, schreiben wir den Code wahrscheinlich trotzdem in der falschen Sprache. OOI was ist weniger ineffizient (!)? i=$(bc <<< "scale...")oderi=$(echo "scale..." | bc)
Roaima
1
Nach meinem Schnelltest ist die Pipe-Version in zsh (woher <<<kommt) bashund schneller ksh. Beachten Sie, dass Sie durch den Wechsel zu einer anderen Shell basheine bessere Leistungssteigerung erzielen als mit der anderen Syntax.
Stéphane Chazelas
(und die meisten der Schalen , dass die Unterstützung <<<(zsh, mksh, ksh93, yash) auch Gleitkomma - Arithmetik - Unterstützung ( zsh, ksh93, yash)).
Stéphane Chazelas
18

Vermeiden Sie Schleifen in Muscheln.

Wenn Sie rechnen möchten, verwenden Sie awkoder bc:

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

Oder

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

Beachten Sie, dass awk(im Gegensatz zu bc) die doubleGleitkommazahlendarstellung Ihres Prozessors funktioniert (wahrscheinlich IEEE 754- Typ). Da diese Zahlen binäre Annäherungen an diese Dezimalzahlen sind, können Sie einige Überraschungen erleben:

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

Wenn Sie ein hinzufügen OFMT="%.17g", können Sie den Grund für das Fehlen sehen 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 macht willkürliche Präzision, hat also kein solches Problem.

Beachten Sie, dass standardmäßig (sofern Sie das Ausgabeformat nicht mit expliziten Formatspezifikationen ändern OFMToder printfmit expliziten Formatspezifikationen awkverwenden ) die %.6gAnzeige von Gleitkommazahlen verwendet wird. Wechseln Sie daher für Gleitkommazahlen über 1.000.000 zu 1e6 und höher und kürzen Sie den Bruchteil für hohe Zahlen (100000.02) würde als 100000 angezeigt werden).

Wenn Sie wirklich brauchen eine Shell - Schleife zu verwenden, da zum Beispiel mögen Sie bestimmte Befehle für jede Iteration dieser Schleife laufen, entweder eine Shell verwenden , um mit Gleitkomma - Arithmetik - Unterstützung wie zsh, yashoderksh93 oder die Liste der Werte mit einem Befehl zu erzeugen , wie oben (oder seqfalls verfügbar) und durchlaufen den Ausgang.

Mögen:

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

Oder:

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

Wenn Sie die Grenzen Ihrer Prozessor-Gleitkommazahlen nicht überschreiten, werden seqFehler, die durch Gleitkomma-Approximationen auftreten, eleganter behandelt als dieawk obigen Version.

Wenn Sie keinen seq(GNU-Befehl) haben, können Sie einen zuverlässigeren Befehl als Funktion wie folgt erstellen:

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

Das würde besser funktionieren für Dinge wie seq 100000000001 0.000000001 100000000001.000000005. Beachten Sie jedoch, dass Zahlen mit willkürlich hoher Genauigkeit nicht viel helfen, wenn wir sie an Befehle übergeben, die sie nicht unterstützen.

Stéphane Chazelas
quelle
Ich schätze die Verwendung von awk! +1
Pandya
Warum müssen Sie unset IFSim ersten Beispiel?
user1717828
@ user1717828, idealerweise möchten wir mit diesem Aufruf von split + glob auf Zeilenumbrüche aufteilen. Wir können das damit machen, IFS=$'\n'aber das funktioniert nicht in allen Muscheln. Oder IFS='<a-litteral-newline-here>'aber das ist nicht sehr gut lesbar. Oder wir können stattdessen Wörter aufteilen (Leerzeichen, Tabulator, Zeilenumbruch), wie Sie sie mit dem Standardwert $ IFS erhalten, oder wenn Sie IFS deaktivieren und auch hier arbeiten.
Stéphane Chazelas
@ user1717828: Wir müssen uns nicht damit anlegen IFS, da wir wissen, dass seqdie Ausgabe keine Leerzeichen enthält, auf die wir uns nicht aufteilen müssen. Es ist meistens da, um sicherzustellen, dass Sie erkennen, dass dieses Beispiel davon abhängt IFS, was für einen anderen Befehl zum Generieren von Listen von Bedeutung sein kann.
Peter Cordes
1
@PeterCordes, es ist da, so dass wir keine Annahme darüber treffen müssen, auf was IFS zuvor eingestellt wurde.
Stéphane Chazelas
2

Verwenden Sie "seq" - drucken Sie eine Folge von Zahlen

seq ERSTE ERHÖHUNG LETZT

for i in $(seq 4.00 0.02 5.42)
do 
  echo $i 
done
Borzol
quelle
Diese Antwort wurde bereits gegeben .
Stéphane Chazelas
0

Wie andere vorgeschlagen haben, können Sie bc verwenden:

i="4.00"

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