Comment comparer deux nombres à virgule flottante dans Bash?

156

J'essaye de comparer deux nombres à virgule flottante dans un script bash. Je dois des variables, par exemple

let num1=3.17648e-22
let num2=1.5

Maintenant, je veux juste faire une simple comparaison de ces deux nombres:

st=`echo "$num1 < $num2" | bc`
if [ $st -eq 1]; then
  echo -e "$num1 < $num2"
else
  echo -e "$num1 >= $num2"
fi

Malheureusement, j'ai quelques problèmes avec le bon traitement du num1 qui peut être du "e-format". :(

Toute aide, conseils sont les bienvenus!

Jonas
la source
2
Avec "e-format", je veux dire la notation exponentielle (également appelée notation scientifique)
Jonas

Réponses:

181

Plus commodément

Cela peut être fait plus facilement en utilisant le contexte numérique de Bash:

if (( $(echo "$num1 > $num2" |bc -l) )); then
  
fi

Explication

Le fait de passer par la commande de base de la calculatrice bcrenvoie 1 ou 0.

L'option -léquivaut à --mathlib; il charge la bibliothèque mathématique standard.

Mettre l'expression entière entre doubles parenthèses (( ))traduira ces valeurs respectivement en vrai ou en faux.

Veuillez vous assurer que le bcpackage de base de la calculatrice est installé.

Cela fonctionne également pour les flotteurs au format scientifique, à condition qu'une lettre majuscule Esoit utilisée, par exemplenum1=3.44E6

Serge Stroobandt
la source
1
Même problème que stackoverflow.com/questions/8654051/… par exemple $ echo "1.1 + 2e + 02" | bc (standard_in) 1: erreur de syntaxe
Nemo
1
@MohitArora Veuillez vous assurer que le bcpackage de calculatrice est installé.
Serge Stroobandt
1
J'obtiens un 0: not foundavec la déclaration if (( $(echo "$TOP_PROCESS_PERCENTAGE > $THRESHOLD" | bc -l) )); then.
Stephane
1
Pour tous ceux qui obtiennent "commande introuvable", rappelez-vous que vous devez bcplacer le soit dans les backticks ou $()puis dans (( ))... c'est (( $(bc -l<<<"$a>$b") ))-à- dire et non (( bc -l<<<"$a>$b" )).
Normadize
@Nemo Écrivez des nombres en notation scientifique avec une majuscule Eet toutes les erreurs de syntaxe auront disparu.
Serge Stroobandt
100

bash ne gère que les nombres entiers, mais vous pouvez utiliser la bccommande comme suit:

$ num1=3.17648E-22
$ num2=1.5
$ echo $num1'>'$num2 | bc -l
0
$ echo $num2'>'$num1 | bc -l
1

Notez que le signe de l'exposant doit être en majuscule

alrusdi
la source
3
oui, mais pour contourner les calculs incorrects, il est nécessaire de mettre en majuscule le signe «e» dans la notation numérique scientifique et d'utiliser le drapeau -l pour bc
program
2
vous devriez le souligner dans votre réponse, au lieu de simplement publier une solution très similaire et de ne pas mentionner les différences importantes.
Daniel Persson
4
Ce n'est pas une solution très similaire . La solution d'Alrusdi utilise l' bcoutil et c'est ce que je recommanderais à tout programmeur BASH. BASH est un langage sans type. Oui, il peut faire de l'arithmétique entière, mais pour la virgule flottante, vous devez utiliser un outil externe. BC est le meilleur parce que c'est pour cela qu'il est fait.
DejanLekic
8
Puisqu'il essaie de l'utiliser dans une déclaration if, je le montrerais. si [$ (... | bc -l) == 1]; puis ...
Robert Jacobs
27

Il est préférable de l'utiliser awkpour les mathématiques non entières. Vous pouvez utiliser cette fonction utilitaire bash:

numCompare() {
   awk -v n1="$1" -v n2="$2" 'BEGIN {printf "%s " (n1<n2?"<":">=") " %s\n", n1, n2}'
}

Et appelez-le comme:

numCompare 5.65 3.14e-22
5.65 >= 3.14e-22

numCompare 5.65e-23 3.14e-22
5.65e-23 < 3.14e-22

numCompare 3.145678 3.145679
3.145678 < 3.145679
anubhava
la source
2
J'aime cette réponse, les gens ont tendance à éviter les débutants, ils semblent penser que c'est plus difficile qu'il ne l'est en réalité, je pense que les gens sont intimidés par les accolades et la syntaxe apparemment mixte du langage (en un coup d'œil). Et comme awk est à peu près garanti d'être également présent sur le système cible, tout comme bc (je ne sais pas lequel, le cas échéant, n'est jamais installé). J'adore les scripts bash mais pas de virgule flottante, même pas un maigre 2 décimales (je suppose que quelqu'un pourrait écrire un `` faux '' wrapper pour ça), c'est vraiment ennuyeux ...
osirisgothra
2
Utiliser awket bcdans les scripts shell est une pratique standard depuis l'Antiquité, je dirais que certaines fonctionnalités n'ont jamais été ajoutées aux shells car elles sont disponibles dans awk, bc et d'autres outils Unix. Pas besoin de pureté dans les scripts shell.
piokuc
1
@WanderingMind Une façon de faire cela serait de passer le 0 ou le 1 à exitpour qu'Awk communique le résultat au shell d'une manière correcte et lisible par machine. if awk -v n1="123.456" -v n2="3.14159e17" 'BEGIN { exit (n1 <= n2) }' /dev/null; then echo bigger; else echo not; fi... mais notez comment la condition est inversée (le statut de sortie 0 signifie que le shell réussit).
tripleee
1
Pourquoi juste python. Vous avez perlinstallé par défaut sur de nombreux systèmes Linux / Unix .. même phpaussi
anubhava
1
Cette awksolution est plus robuste dans mon cas que celle avec bcqui renvoie des résultats erronés pour une raison que je n'ai pas obtenue.
MBR
22

Solution pure bash pour comparer des flottants sans notation exponentielle, zéros de début ou de fin:

if [ ${FOO%.*} -eq ${BAR%.*} ] && [ ${FOO#*.} \> ${BAR#*.} ] || [ ${FOO%.*} -gt ${BAR%.*} ]; then
  echo "${FOO} > ${BAR}";
else
  echo "${FOO} <= ${BAR}";
fi

L'ordre des opérateurs logiques est important . Les parties entières sont comparées sous forme de nombres et les parties fractionnaires sont intentionnellement comparées sous forme de chaînes. Les variables sont divisées en parties entières et fractionnaires à l'aide de cette méthode .

Ne comparera pas les flottants avec des entiers (sans point).

utilisateur
la source
15

vous pouvez utiliser awk combiné avec une condition bash if, awk affichera 1 ou 0 et ceux-ci seront interprétés par la clause if avec vrai ou faux .

if awk 'BEGIN {print ('$d1' >= '$d2')}'; then
    echo "yes"
else 
    echo "no"
fi
ungalcrys
la source
Utiliser awk est génial car il est capable de gérer les nombres à virgule flottante, mais je préfère personnellement la synthaxif (( $(echo $d1 $d2 | awk '{if ($1 > $2) print 1;}') )); then echo "yes"; else echo "no"; fi
David Georg Reichelt
7

méfiez-vous lorsque vous comparez des nombres qui sont des versions de package, comme vérifier si grep 2.20 est supérieur à la version 2.6:

$ awk 'BEGIN { print (2.20 >= 2.6) ? "YES" : "NO" }'
NO

$ awk 'BEGIN { print (2.2 >= 2.6) ? "YES" : "NO" }'
NO

$ awk 'BEGIN { print (2.60 == 2.6) ? "YES" : "NO" }'
YES

J'ai résolu ce problème avec une telle fonction shell / awk:

# get version of GNU tool
toolversion() {
    local prog="$1" operator="$2" value="$3" version

    version=$($prog --version | awk '{print $NF; exit}')

    awk -vv1="$version" -vv2="$value" 'BEGIN {
        split(v1, a, /\./); split(v2, b, /\./);
        if (a[1] == b[1]) {
            exit (a[2] '$operator' b[2]) ? 0 : 1
        }
        else {
            exit (a[1] '$operator' b[1]) ? 0 : 1
        }
    }'
}

if toolversion grep '>=' 2.6; then
   # do something awesome
fi
Elan Ruusamäe
la source
Sur un système basé sur Debian, dpkg --compare-versionsest souvent utile. Il a toute la logique pour comparer les versions de paquet Debian intégrées, qui sont plus complexes que juste x.y.
Neil Mayhew
5

Bien sûr, si vous n'avez pas vraiment besoin d'arithmétique à virgule flottante, juste de l'arithmétique sur, par exemple, des valeurs en dollars où il y a toujours exactement deux chiffres décimaux, vous pouvez simplement supprimer le point (multipliant effectivement par 100) et comparer les entiers résultants.

if [[ $((10#${num1/.})) < $((10#${num2/.})) ]]; then
    ...

Cela nécessite évidemment que vous soyez sûr que les deux valeurs ont le même nombre de décimales.

tripleee
la source
3

J'ai utilisé les réponses d'ici et les ai mises dans une fonction, vous pouvez l'utiliser comme ceci:

is_first_floating_number_bigger 1.5 1.2
result="${__FUNCTION_RETURN}"

Une fois appelé, echo $resultsera 1dans ce cas, sinon 0.

La fonction:

is_first_floating_number_bigger () {
    number1="$1"
    number2="$2"

    [ ${number1%.*} -eq ${number2%.*} ] && [ ${number1#*.} \> ${number2#*.} ] || [ ${number1%.*} -gt ${number2%.*} ];
    result=$?
    if [ "$result" -eq 0 ]; then result=1; else result=0; fi

    __FUNCTION_RETURN="${result}"
}

Ou une version avec sortie de débogage:

is_first_floating_number_bigger () {
    number1="$1"
    number2="$2"

    echo "... is_first_floating_number_bigger: comparing ${number1} with ${number2} (to check if the first one is bigger)"

    [ ${number1%.*} -eq ${number2%.*} ] && [ ${number1#*.} \> ${number2#*.} ] || [ ${number1%.*} -gt ${number2%.*} ];
    result=$?
    if [ "$result" -eq 0 ]; then result=1; else result=0; fi

    echo "... is_first_floating_number_bigger: result is: ${result}"

    if [ "$result" -eq 0 ]; then
        echo "... is_first_floating_number_bigger: ${number1} is not bigger than ${number2}"
    else
        echo "... is_first_floating_number_bigger: ${number1} is bigger than ${number2}"
    fi

    __FUNCTION_RETURN="${result}"
}

Enregistrez simplement la fonction dans un .shfichier séparé et incluez-la comme ceci:

. /path/to/the/new-file.sh
Thomas Kekeisen
la source
3

Je postais ceci en réponse à https://stackoverflow.com/a/56415379/1745001 quand il a été fermé en tant que dup de cette question, alors le voici comme cela s'applique ici aussi:

Pour plus de simplicité et de clarté, utilisez simplement awk pour les calculs car c'est un outil UNIX standard et donc tout aussi susceptible d'être présent que bc et beaucoup plus facile à utiliser syntaxiquement.

Pour cette question:

$ cat tst.sh
#!/bin/bash

num1=3.17648e-22
num2=1.5

awk -v num1="$num1" -v num2="$num2" '
BEGIN {
    print "num1", (num1 < num2 ? "<" : ">="), "num2"
}
'

$ ./tst.sh
num1 < num2

et pour cette autre question qui a été fermée comme une dupe de celle-ci:

$ cat tst.sh
#!/bin/bash

read -p "Operator: " operator
read -p "First number: " ch1
read -p "Second number: " ch2

awk -v ch1="$ch1" -v ch2="$ch2" -v op="$operator" '
BEGIN {
    if ( ( op == "/" ) && ( ch2 == 0 ) ) {
        print "Nope..."
    }
    else {
        print ch1 '"$operator"' ch2
    }
}
'

$ ./tst.sh
Operator: /
First number: 4.5
Second number: 2
2.25

$ ./tst.sh
Operator: /
First number: 4.5
Second number: 0
Nope...
Ed Morton
la source
@DudiBoy non, c'est un code awk clair, simple et portable ou un shell + bc non évident, obscur et dépendant du shell.
Ed Morton
3

awket des outils comme celui-ci (je vous regarde fixement sed...) devraient être relégués à la poubelle des vieux projets, avec du code que tout le monde a trop peur de toucher puisqu'il a été écrit dans un langage sans lecture.

Ou vous êtes le projet relativement rare qui doit donner la priorité à l'optimisation de l'utilisation du processeur par rapport à l'optimisation de la maintenance du code ... auquel cas, continuez.

Sinon, pourquoi ne pas utiliser à la place quelque chose de lisible et explicite, comme python? Vos collègues codeurs et votre futur moi vous remercieront. Vous pouvez utiliser en pythonligne avec bash comme tous les autres.

num1=3.17648E-22
num2=1.5
if python -c "exit(0 if $num1 < $num2 else 1)"; then
    echo "yes, $num1 < $num2"
else
    echo "no, $num1 >= $num2"
fi
CivFan
la source
@Witiko Ma version originale était un peu plus sournoise.
CivFan
Encore plus succinct: utiliser à la not(...)place de0 if ... else 1
Neil Mayhew
1
Si vous reléguez awk et sed (je vous regarde CivFan) à la poubelle de l'histoire, vous êtes un administrateur système moche et vous tapez trop de code. (Et j'aime et j'utilise Python, donc ce n'est pas à propos de ça). -1 pour la sarcasme mal placée. Il y a une place dans le domaine des systèmes pour ces outils, Python ou non.
Mike S
1
Fait intéressant, j'ai fini avec le bon vieux Perl! awk '${print $5}' ptpd_log_file | perl -ne '$_ > 0.000100 && print' > /tmp/outfile. Peasy facile. Chaque langue a sa place.
Mike S
1
Ne vous mêlez pas de la folie syntacique des seds. Contrairement à python, awk est un utilitaire obligatoire sur chaque installation UNIX et l'équivalent awk de l' python -c "import sys; sys.exit(0 if float($num1) < float($num2) else 1)"est simplement awk "BEGIN{exit ($num1 > $num2 ? 0 : 1)}".
Ed Morton
2

Ce script peut aider lorsque je vérifie si la grailsversion installée est supérieure au minimum requis. J'espère que ça aide.

#!/bin/bash                                                                                         

min=1.4                                                                                             
current=`echo $(grails --version | head -n 2 | awk '{print $NF}' | cut -c 1-3)`                         

if [ 1 -eq `echo "${current} < ${min}" | bc` ]                                                          
then                                                                                                
    echo "yo, you have older version of grails."                                                   
else                                                                                                                                                                                                                       
    echo "Hurray, you have the latest version" 
fi
prier
la source
2
num1=0.555
num2=2.555


if [ `echo "$num1>$num2"|bc` -eq 1 ]; then
       echo "$num1 is greater then $num2"
else
       echo "$num2 is greater then $num1"
fi
rmil
la source
2

veuillez vérifier le code édité ci-dessous: -

#!/bin/bash

export num1=(3.17648*e-22)
export num2=1.5

st=$((`echo "$num1 < $num2"| bc`))
if [ $st -eq 1 ]
  then
    echo -e "$num1 < $num2"
  else
    echo -e "$num1 >= $num2"
fi

cela fonctionne bien.

Gopika BG
la source
2

Une solution prenant en charge toutes les notations possibles, y compris la notation scientifique avec des exposants majuscules et minuscules (par exemple, 12.00e4):

if (( $(bc -l <<< "${value1/e/E} < ${value2/e/E}") ))
then
    echo "$value1 is below $value2"
fi 
Danila Piatov
la source
1

Utilisez korn shell, dans bash, vous devrez peut-être comparer la partie décimale séparément

#!/bin/ksh
X=0.2
Y=0.2
echo $X
echo $Y

if [[ $X -lt $Y ]]
then
     echo "X is less than Y"
elif [[ $X -gt $Y ]]
then
     echo "X is greater than Y"
elif [[ $X -eq $Y ]]
then
     echo "X is equal to Y"
fi
Alan Joseph
la source
2
le problème est que de nombreuses distributions ne sont pas livrées avec ksh installé, et si votre script va être utilisé par d'autres, ils ont tendance à ne pas aimer avoir à installer des éléments supplémentaires, surtout quand il s'agit simplement d'un script censé être écrit en bash -On pourrait penser qu'ils n'ont pas besoin d'UN AUTRE shell pour faire cela, ce qui mine toute la raison de l'utilisation d'un script bash en premier lieu - bien sûr que nous pourrions également le coder en C ++, mais pourquoi?
osirisgothra
Quelles sont les distributions fournies sans ksh installé?
piokuc
1
@piokuc par exemple, Ubuntu Desktop & Server. Je dirais que c'est assez majeur ...
Olli
En outre, la question demande spécifiquement une solution qui fonctionne dans bash. Il peut y avoir de très bonnes raisons à cela. Dites, cela fait partie d'une grande application et la migration de tout vers ksh n'est pas possible. Ou il fonctionne sur une plate-forme intégrée où l'installation d'un autre shell est vraiment un problème.
Olli
1

En utilisant bashj ( https://sourceforge.net/projects/bashj/ ), un mutant bash avec support java, il vous suffit d'écrire (et c'est facile à lire):

#!/usr/bin/bashj

#!java
static int doubleCompare(double a,double b) {return((a>b) ? 1 : (a<b) ? -1 : 0);}

#!bashj
num1=3.17648e-22
num2=1.5
comp=j.doubleCompare($num1,$num2)
if [ $comp == 0 ] ; then echo "Equal" ; fi
if [ $comp == 1 ] ; then echo "$num1 > $num2" ; fi
if [ $comp == -1 ] ; then echo "$num2 > $num1" ; fi

Bien sûr, l'hybridation bashj bash / java offre bien plus ...

Fil
la source
0

Que dis-tu de ça? = D

VAL_TO_CHECK="1.00001"
if [ $(awk '{printf($1 >= $2) ? 1 : 0}' <<<" $VAL_TO_CHECK 1 ") -eq 1 ] ; then
    echo "$VAL_TO_CHECK >= 1"
else
    echo "$VAL_TO_CHECK < 1"
fi
Eduardo Lucio
la source
1
Le script Awk devrait simplement exit 0rapporter la vérité et exit 1renvoyer faux; alors vous pouvez simplifier au remarquablement élégant if awk 'BEGIN { exit (ARGV[1] >= ARGV[2]) ? 0 : 1 }' "$VAL_TO_CHECK" 1; then... (plus élégant encore si vous encapsulez le script Awk dans une fonction shell).
tripleee