boucle bash avec incrément de 0,02

11

Je veux faire une boucle for en bash avec 0,02 comme incréments que j'ai essayé

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

mais ça n'a pas marché.

Mehrshad
la source
9
Bash ne fait pas de calcul en virgule flottante.
jordanm
1
l'incrémentation peut être effectuée par bc, mais l'arrêt sur 4,52 peut être délicat. utiliser la suggestion @roaima, avoir une var auxiliaire avec un pas de 2 et utiliseri=$(echo $tmp_var / 100 | bc)
Archemar
5
Normalement, vous ne voulez pas utiliser de flottants comme index de boucle . Vous accumulez des erreurs à chaque itération.
isanae

Réponses:

18

La lecture de la bash page de manuel donne les informations suivantes:

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

Premièrement, l'expression arithmétique expr1est évaluée selon les règles décrites ci-dessous sous ÉVALUATION ARITHMÉTIQUE. [...]

puis nous obtenons cette section

ÉVALUATION ARITHMÉTIQUE

L'enveloppe permet d' expressions arithmétiques à évaluer, dans certaines circonstances (voir letet declareintégrée commandes et Arithmétique). L'évaluation se fait en nombres entiers à largeur fixe sans vérification du débordement [...]

On voit donc clairement que vous ne pouvez pas utiliser une forboucle avec des valeurs non entières.

Une solution peut être simplement de multiplier tous vos composants de boucle par 100, ce qui permet de les utiliser ultérieurement, comme ceci:

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
la source
Je pense que c'est la meilleure solution, k=400;k<542;k+=2car cela évite également d'éventuels problèmes d'arithmétique en virgule flottante.
Huygens
1
Notez que pour chaque itération dans la boucle, vous créez un canal (pour lire la sortie de bc), créez un processus, créez un fichier temporaire (pour la chaîne ici), exécutez- bcle (ce qui implique de charger un exécutable et des bibliothèques partagées et les initialiser), attendez et nettoyez. Courir bcune fois pour faire la boucle serait beaucoup plus efficace.
Stéphane Chazelas
@ StéphaneChazelas oui, d'accord. Mais si c'est le goulot d'étranglement, nous écrivons probablement le code dans la mauvaise langue de toute façon. OOI qui est moins inefficace (!)? i=$(bc <<< "scale...")oui=$(echo "scale..." | bc)
roaima
1
D'après mon test rapide, la version pipe est plus rapide en zsh (d'où <<<vient), bashet ksh. Notez que le passage à un autre shell bashvous donnera une meilleure amélioration des performances que l'utilisation de l'autre syntaxe.
Stéphane Chazelas
(et la plupart des coquilles que le soutien <<<(zsh, mksh, ksh93, Yash) prennent également en charge arithmétiques en virgule flottante ( zsh, ksh93, yash)).
Stéphane Chazelas
18

Évitez les boucles dans les coquilles.

Si vous voulez faire de l'arithmétique, utilisez 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

Notez que awk(contrairement à bc) fonctionne avec la représentation numérique à doublevirgule flottante de votre processeur (probablement de type IEEE 754 ). Par conséquent, puisque ces nombres sont des approximations binaires de ces nombres décimaux, vous pouvez avoir quelques surprises:

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

Si vous ajoutez un, OFMT="%.17g"vous pouvez voir la raison de la disparition 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 fait une précision arbitraire donc n'a pas ce genre de problème.

Notez que par défaut (sauf si vous modifiez le format de sortie avec OFMTou utilisez printfdes spécifications de format explicites), awkutilise %.6gpour afficher les nombres à virgule flottante, donc passerait à 1e6 et au-dessus pour les nombres à virgule flottante supérieurs à 1000000 et tronquer la partie fractionnaire pour les nombres élevés (100000.02 serait affiché comme 100000).

Si vous avez vraiment besoin d'utiliser une boucle shell, par exemple parce que vous voulez exécuter des commandes spécifiques pour chaque itération de cette boucle, soit utiliser une coque avec support arithmétique en virgule flottante comme zsh, yashou ksh93ou générer la liste des valeurs avec une commande comme ci - dessus (ou seqsi disponible) et boucle sur sa sortie.

Comme:

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

sauf si vous repoussez les limites des nombres à virgule flottante de votre processeur, seqgère les erreurs encourues par les approximations à virgule flottante plus gracieusement que ne le awkferait la version ci-dessus.

Si vous n'en avez pas seq(une commande GNU), vous pouvez en faire une plus fiable comme une fonction comme:

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

Cela fonctionnerait mieux pour des choses comme seq 100000000001 0.000000001 100000000001.000000005. Notez cependant qu'avoir des nombres avec une précision arbitraire élevée n'aidera pas beaucoup si nous allons les passer à des commandes qui ne les prennent pas en charge.

Stéphane Chazelas
la source
J'apprécie l'utilisation de awk! +1
Pandya
Pourquoi en avez-vous besoin unset IFSdans le premier exemple?
user1717828
@ user1717828, idéalement, avec cette invocation split + glob, nous voulons diviser sur les caractères de nouvelle ligne. Nous pouvons le faire avec IFS=$'\n'mais cela ne fonctionne pas dans tous les shells. Ou IFS='<a-litteral-newline-here>'mais ce n'est pas très lisible. Ou nous pouvons diviser les mots à la place (espace, tabulation, nouvelle ligne) comme vous obtenez avec la valeur par défaut de $ IFS ou si vous désactivez IFS et fonctionne également ici.
Stéphane Chazelas
@ user1717828: nous n'avons pas besoin de jouer avec IFS, car nous savons que seqla sortie de n'a pas d'espaces sur lesquels nous devons éviter de nous séparer. Il est principalement là pour vous assurer que vous comprenez que cet exemple dépend IFS, ce qui pourrait avoir une importance pour une commande de génération de liste différente.
Peter Cordes
1
@PeterCordes, il est là, nous n'avons donc pas besoin de faire d'hypothèse sur ce que IFS a été défini à l'avance.
Stéphane Chazelas
2

Utiliser "seq" - imprimer une séquence de chiffres

SEQ PREMIER INCREMENT DERNIER

for i in $(seq 4.00 0.02 5.42)
do 
  echo $i 
done
borzole
la source
Cette réponse a déjà été donnée .
Stéphane Chazelas
0

Comme d'autres l'ont suggéré, vous pouvez utiliser bc:

i="4.00"

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