Comment comparer au nombre à virgule flottante dans un script shell


22

Je veux comparer deux nombres à virgule flottante dans un script shell. Le code suivant ne fonctionne pas:

#!/bin/bash   
min=12.45
val=10.35    
if (( $val < $min )) ; then    
  min=$val
fi
echo $min 

Réponses:


5

Vous pouvez vérifier séparément les parties entières et fractionnaires:

#!/bin/bash
min=12.45
val=12.35    
if (( ${val%%.*} < ${min%%.*} || ( ${val%%.*} == ${min%%.*} && ${val##*.} < ${min##*.} ) )) ; then    
    min=$val
fi
echo $min

Comme le dit Fered dans les commentaires, cela ne fonctionne que si les deux nombres ont des parties fractionnaires et que les deux parties fractionnaires ont le même nombre de chiffres. Voici une version qui fonctionne pour entier ou fractionnaire et tout opérateur bash:

#!/bin/bash
shopt -s extglob
fcomp() {
    local oldIFS="$IFS" op=$2 x y digitx digity
    IFS='.' x=( ${1##+([0]|[-]|[+])}) y=( ${3##+([0]|[-]|[+])}) IFS="$oldIFS"
    while [[ "${x[1]}${y[1]}" =~ [^0] ]]; do
        digitx=${x[1]:0:1} digity=${y[1]:0:1}
        (( x[0] = x[0] * 10 + ${digitx:-0} , y[0] = y[0] * 10 + ${digity:-0} ))
        x[1]=${x[1]:1} y[1]=${y[1]:1} 
    done
    [[ ${1:0:1} == '-' ]] && (( x[0] *= -1 ))
    [[ ${3:0:1} == '-' ]] && (( y[0] *= -1 ))
    (( ${x:-0} $op ${y:-0} ))
}

for op in '==' '!=' '>' '<' '<=' '>='; do
    fcomp $1 $op $2 && echo "$1 $op $2"
done

4
Cela ne peut pas être résolu sans beaucoup de travail (essayez de comparer 0.5et 0.06). Vous feriez mieux d'utiliser un outil qui comprend déjà la notation décimale.
Gilles 'SO- arrête d'être méchant'

Merci Gilles, il l'a mis à jour pour fonctionner plus généralement que la version précédente.
ata

Notez qu'il dit que 1.00000000000000000000000001c'est plus grand que 2.
Stéphane Chazelas

Stéphane a raison. C'est ainsi à cause des limites de bits dans la représentation numérique de bash. Bien sûr, si vous voulez plus de souffrance, vous pouvez utiliser votre propre représentation .... :)
ata

35

Bash ne comprend pas l'arithmétique à virgule flottante. Il traite les nombres contenant un point décimal comme des chaînes.

Utilisez plutôt awk ou bc.

#!/bin/bash

min=12.45
val=10.35

if [ 1 -eq "$(echo "${val} < ${min}" | bc)" ]
then  
    min=${val}
fi

echo "$min"

Si vous avez l'intention de faire beaucoup d'opérations mathématiques, il est probablement préférable de s'appuyer sur python ou perl.


12

Vous pouvez utiliser le paquet num-utils pour des manipulations simples ...

Pour des mathématiques plus sérieuses, voir ce lien ... Il décrit plusieurs options, par exemple.

  • R / Rscript (système de calcul statistique et graphique GNU R)
  • octave (principalement compatible Matlab)
  • bc (langage de calculateur de précision arbitraire GNU bc)

Un exemple de numprocess

echo "123.456" | numprocess /+33.267,%2.33777/
# 67.0395291239087  

A programs for dealing with numbers from the command line

The 'num-utils' are a set of programs for dealing with numbers from the
Unix command line. Much like the other Unix command line utilities like
grep, awk, sort, cut, etc. these utilities work on data from both
standard in and data from files.

Includes these programs:
 * numaverage: A program for calculating the average of numbers.
 * numbound: Finds the boundary numbers (min and max) of input.
 * numinterval: Shows the numeric intervals between each number in a sequence.
 * numnormalize: Normalizes a set of numbers between 0 and 1 by default.
 * numgrep: Like normal grep, but for sets of numbers.
 * numprocess: Do mathematical operations on numbers.
 * numsum: Add up all the numbers.
 * numrandom: Generate a random number from a given expression.
 * numrange: Generate a set of numbers in a range expression.
 * numround: Round each number according to its value.

Voici un bashhack ... Il ajoute des 0 en tête à l'entier pour donner une signification à une comparaison de gauche à droite. Ce morceau de code particulier nécessite que min et val aient en fait un point décimal et au moins un chiffre décimal.

min=12.45
val=10.35

MIN=0; VAL=1 # named array indexes, for clarity
IFS=.; tmp=($min $val); unset IFS 
tmp=($(printf -- "%09d.%s\n" ${tmp[@]}))
[[ ${tmp[VAL]} < ${tmp[MIN]} ]] && min=$val
echo min=$min

sortie:

min=10.35

10

Pour des calculs simples sur des nombres à virgule flottante (+ - * / et des comparaisons), vous pouvez utiliser awk.

min=$(echo 12.45 10.35 | awk '{if ($1 < $2) print $1; else print $2}')

Ou, si vous avez ksh93 ou zsh (pas bash), vous pouvez utiliser l'arithmétique intégrée de votre shell, qui prend en charge les nombres à virgule flottante.

if ((min>val)); then ((val=min)); fi

Pour des calculs en virgule flottante plus avancés, recherchez bc . Il fonctionne en fait sur les nombres de points fixes à précision arbitraire.

Pour travailler sur des tableaux de nombres, recherchez R ( exemple ).


6

Utiliser le tri numérique

La commande sorta une option -g( --general-numeric-sort) qui peut être utilisée pour des comparaisons sur <«moins de» ou >«plus grand que» en trouvant le minimum ou le maximum.

Ces exemples trouvent le minimum:

$ printf '12.45\n10.35\n' | sort -g | head -1
10.35

Prend en charge la notation électronique

Il fonctionne avec une notation assez générale des nombres à virgule flottante, comme avec la notation E

$ printf '12.45E-10\n10.35\n' | sort -g | head -1
12.45E-10

Notez le E-10, faisant le premier nombre 0.000000001245, en effet inférieur à 10.35.

Peut se comparer à l'infini

La norme à virgule flottante, IEEE754 , définit certaines valeurs spéciales. Pour ces comparaisons, les plus intéressantes sont INFpour l'infini. Il y a aussi l'infini négatif; Les deux sont des valeurs bien définies dans la norme.

$ printf 'INF\n10.35\n' | sort -g | head -1
10.35
$ printf '-INF\n10.35\n' | sort -g | head -1
-INF

Pour trouver l'utilisation maximale sort -grau lieu de sort -g, inverser l'ordre de tri:

$ printf '12.45\n10.35\n' | sort -gr | head -1
12.45

Opération de comparaison

Pour implémenter la <comparaison ("moins de"), afin qu'elle puisse être utilisée dans ifetc, comparez le minimum à l'une des valeurs. Si le minimum est égal à la valeur, comparé au texte , il est inférieur à l'autre valeur:

$ a=12.45; b=10.35                                    
$ [ "$a" = "$(printf "$a\n$b\n" | sort -g | head -1)" ]
$ echo $?
1
$ a=12.45; b=100.35                                    
$ [ "$a" = "$(printf "$a\n$b\n" | sort -g | head -1)" ]
$ echo $?                                              
0

Bon conseil! J'aime vraiment votre idée que la vérification a == min(a, b)est la même que a <= b. Il convient de noter que cela ne vérifie pas strictement moins que cela. Si vous voulez le faire, vous devez vérifier a == min(a, b) && a != max(a, b), en d'autres termesa <= b and not a >= b
Dave

3

Utilisez simplement ksh( ksh93précisément) ou zsh, qui prennent tous les deux en charge l'arithmétique en virgule flottante:

$ cat test.ksh
#!/bin/ksh 
min=12.45
val=10.35    
if (( $val < $min )) ; then    
  min=$val
fi
echo "$min"
$ ./test.ksh
10.35

Edit: Désolé, j'ai raté ksh93a déjà été suggéré. Garder ma réponse juste pour clarifier le script publié dans la question d'ouverture peut être utilisé sans changement en dehors du commutateur shell.

Edit2: Notez que ksh93le contenu de la variable doit être cohérent avec votre environnement local, c'est-à-dire avec un environnement local français, une virgule au lieu d'un point doit être utilisée:

...
min=12,45
val=10,35
...

Une solution plus robuste consiste à définir les paramètres régionaux au début du script pour vous assurer qu'il fonctionnera indépendamment des paramètres régionaux de l'utilisateur:

...
export LC_ALL=C
min=12.45
val=10.35
...

Notez que le script ksh93 ci-dessus ne fonctionne que dans les locales où se trouve le séparateur décimal .(donc pas dans la moitié du monde où se trouve le séparateur décimal ,). zshn'a pas ce problème.
Stéphane Chazelas

En effet, la réponse a été modifiée pour clarifier ce point.
jlliagre

La définition de LC_NUMERIC ne fonctionnera pas si l'utilisateur a défini LC_ALL, cela signifie également que les nombres ne seront pas affichés (ou saisis) dans le format préféré de l'utilisateur. Voir unix.stackexchange.com/questions/87745/what-does-lc-all-c-do/… pour une approche potentiellement meilleure.
Stéphane Chazelas

@ StéphaneChazelas a corrigé le problème LC_NUMERIC. Compte tenu de la syntaxe du script OP, je suppose que son séparateur préféré est de .toute façon.
jlliagre

Oui, mais ce sont les paramètres régionaux de l'utilisateur du script, et non les paramètres régionaux de l'auteur du script qui importent. En tant qu'auteur de script, vous devez prendre en compte la localisation et ses effets secondaires.
Stéphane Chazelas

1
min=$(echo "${min}sa ${val}d la <a p" | dc)

Cela utilise la dccalculatrice pour sdéchirer la valeur de $minin register aet drépliquer la valeur de en $valhaut de sa pile d'exécution principale. Il lrépertorie ensuite le contenu de asur le haut de la pile, auquel cas il ressemble à:

${min} ${val} ${val}

Le fait <apparaître les deux premières entrées de la pile et les compare. La pile ressemble alors à:

${val}

Si l'entrée supérieure était inférieure à la seconde en haut, elle pousse le contenu de avers le haut, de sorte que la pile ressemble à:

${min} ${val}

Sinon, cela ne fait rien et la pile ressemble toujours à:

${val} 

Ensuite, il pimprime simplement l'entrée de la pile supérieure.

Donc pour votre problème:

min=12.45
val=12.35
echo "${min}sa ${val}d la <a p" | dc

###OUTPUT

12.35

Mais:

min=12.45
val=12.55
echo "${min}sa ${val}d la <a p" | dc

###OUTPUT

12.45

0

Pourquoi ne pas utiliser vieux, bon expr?

Exemple de syntaxe:

if expr 1.09 '>' 1.1 1>/dev/null; then
    echo 'not greater'
fi

Pour les expressions vraies , le code de sortie expr est 0, avec la chaîne «1» envoyée à stdout. Inverser pour les fausses expressions.

J'ai vérifié cela avec GNU et FreeBSD 8 expr.


GNU expr ne prend en charge que la comparaison arithmétique sur les entiers. Votre exemple utilise une comparaison lexicographique qui échouera sur les nombres négatifs. Par exemple, expr 1.09 '<' -1.1imprimera 1et quittera avec 0(succès).
Adrian Günter

0

Pour vérifier si deux nombres (éventuellement fractionnaires) sont en ordre, sortest (raisonnablement) portable:

min=12.45
val=12.55
if { echo $min ; echo $val ; } | sort -n -c 2>/dev/null
then
  echo min is smallest
else
  echo val is smallest
fi

Cependant, si vous voulez réellement mettre à jour une valeur minimale, vous n'avez pas besoin d'un if. Triez les chiffres et utilisez toujours le premier (le moins):

min=12.45
val=12.55
smallest=$({ echo $min ; echo $val ; } | sort -n | head -n 1)
echo $smallest
min=$smallest

0

Habituellement, je fais des choses similaires avec du code python intégré:

#!/bin/sh

min=12.45
val=10.35

python - $min $val<<EOF
if ($min > $val):
        print $min
else: 
        print $val
EOF

-1
$ min=12.45
$ val=10.35
$ [ "$min" \< "$val" ] && echo $val || echo $min
$ 12.45
$ val=13
$ [ "$min" \< "$val" ] && echo $val || echo $min
$ 13

3
Pouvez-vous commenter votre réponse et ajouter quelques explications
Romeo Ninov
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.