¿Cómo comparar dos números de coma flotante en Bash?

156

Estoy tratando de comparar dos números de coma flotante dentro de un script bash. Tengo que variables, por ejemplo

let num1=3.17648e-22
let num2=1.5

Ahora, solo quiero hacer una comparación simple de estos dos números:

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

Desafortunadamente, tengo algunos problemas con el tratamiento correcto de num1 que puede ser del "formato e". :(

Cualquier ayuda, sugerencias son bienvenidas!

Jonas
fuente
2
Con "formato e" me refiero a la notación exponencial (también llamada notación científica)
Jonas

Respuestas:

181

Más convenientemente

Esto se puede hacer más convenientemente usando el contexto numérico de Bash:

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

Explicación

La canalización a través del comando de la calculadora básica bcdevuelve 1 o 0.

La opción -les equivalente a --mathlib; carga la biblioteca matemática estándar.

El encerrar la expresión completa entre paréntesis dobles (( ))traducirá estos valores a verdadero o falso respectivamente.

Por favor, asegúrese de que bcesté instalado el paquete básico de la calculadora.

Esto también funciona para carrozas en formato científico, siempre que Ese utilice una letra mayúscula , por ejemplonum1=3.44E6

Serge Stroobandt
fuente
1
Mismo problema que stackoverflow.com/questions/8654051/… ej. $ Echo "1.1 + 2e + 02" | bc (standard_in) 1: error de sintaxis
Nemo
1
@MohitArora Por favor, asegúrese de tener bcinstalado el paquete de la calculadora.
Serge Stroobandt
1
Me sale un 0: not foundcon la declaración if (( $(echo "$TOP_PROCESS_PERCENTAGE > $THRESHOLD" | bc -l) )); then.
Stephane
1
Para todos aquellos que obtienen "comando no encontrado", recuerden que deben encerrarlo bcen comillas inversas o $()luego en (( ))... es decir (( $(bc -l<<<"$a>$b") ))y no (( bc -l<<<"$a>$b" )).
Normadize
@Nemo Escriba números en notación científica con mayúscula E, y todos los errores de sintaxis desaparecerán.
Serge Stroobandt
100

bash maneja solo matemáticas enteras, pero puede usar el bccomando de la siguiente manera:

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

Tenga en cuenta que el signo exponente debe estar en mayúscula

alrusdi
fuente
3
Sí, pero para solucionar los cálculos incorrectos es necesario poner en mayúscula 'e' en la notación de número científico y usar el indicador -l en el programa bc para rutinas matemáticas predefinidas
alrusdi
2
deberías señalar eso en tu respuesta, en lugar de solo publicar una solución muy similar y no mencionar las diferencias importantes.
Daniel Persson
44
No es una solución muy similar . La solución de Alrusdi usa la bcherramienta y eso es lo que recomendaría a cualquier programador de BASH. BASH es lenguaje sin tipo. Sí, puede hacer aritmética de enteros, pero para el punto flotante debe usar alguna herramienta externa. BC es el mejor porque para eso está hecho.
DejanLekic
8
Como está tratando de usarlo en una declaración if, lo demostraría. si [$ (... | bc -l) == 1]; entonces ...
Robert Jacobs
27

Es mejor usarlo awkpara matemáticas no enteras. Puede usar esta función de utilidad bash:

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

Y llámalo como:

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
fuente
2
Me gusta esta respuesta, las personas tienden a rehuir a los principiantes de awk esp, parecen pensar que es más difícil de lo que realmente es, creo que las personas se sienten intimidadas por las llaves y la sintaxis aparentemente mixta (de un vistazo). Y dado que es casi seguro que awk también estará presente en el sistema de destino, al igual que bc (no estoy seguro de cuál, si es que alguna, NO está nunca instalado). Me encanta bash scripting, pero que ningún punto flotante, ni siquiera un magro 2 decimales (supongo que alguien podría escribir un envoltorio de 'falso' para eso), realmente es molesto ...
osirisgothra
2
Usar awky bcen scripts de shell es una práctica estándar desde la antigüedad, diría que algunas funciones nunca se han agregado a los shells porque están disponibles en awk, bc y otras herramientas de Unix. No hay necesidad de pureza en los scripts de shell.
piokuc
1
@WanderingMind Una forma de hacerlo sería pasar el 0 o el 1 para exitque Awk comunique el resultado al shell de una manera adecuada y legible por máquina. if awk -v n1="123.456" -v n2="3.14159e17" 'BEGIN { exit (n1 <= n2) }' /dev/null; then echo bigger; else echo not; fi... aunque tenga en cuenta cómo se invierte la condición (el estado de salida 0 significa éxito en el shell).
tripleee
1
¿Por qué solo python? Se ha perlinstalado por defecto en muchos sistemas Linux / Unix ... incluso phptambién
anubhava
1
Esta awksolución es más sólida en mi caso que la bcque arroja resultados incorrectos por una razón que no obtuve.
MBR
22

Solución de bash pura para comparar flotadores sin notación exponencial, ceros iniciales o finales:

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

El orden de los operadores lógicos es importante . Las partes enteras se comparan como números y las partes fraccionarias se comparan intencionalmente como cadenas. Las variables se dividen en partes enteras y fraccionarias utilizando este método .

No comparará flotantes con enteros (sin punto).

usuario
fuente
15

puede usar awk combinado con una condición bash if, awk imprimirá 1 o 0 y estos se interpretarán mediante la cláusula if con verdadero o falso .

if awk 'BEGIN {print ('$d1' >= '$d2')}'; then
    echo "yes"
else 
    echo "no"
fi
ungalcrys
fuente
Usar awk es excelente ya que es capaz de manejar números de coma flotante, pero personalmente prefiero el Synthaxif (( $(echo $d1 $d2 | awk '{if ($1 > $2) print 1;}') )); then echo "yes"; else echo "no"; fi
David Georg Reichelt
7

tenga cuidado al comparar números que son versiones de paquetes, como verificar si grep 2.20 es mayor que la versión 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

Resolví tal problema con dicha función 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
fuente
En un sistema basado en Debian, a dpkg --compare-versionsmenudo es útil. Tiene la lógica completa para comparar las versiones de paquetes de Debian incorporadas, que son más complejas que simplemente x.y.
Neil Mayhew
5

Por supuesto, si no necesita realmente la aritmética de coma flotante, solo la aritmética en, por ejemplo, valores de dólar donde siempre hay exactamente dos dígitos decimales, puede simplemente soltar el punto (multiplicando efectivamente por 100) y comparar los enteros resultantes.

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

Obviamente, esto requiere que esté seguro de que ambos valores tienen el mismo número de lugares decimales.

tripleee
fuente
3

Usé las respuestas de aquí y las puse en una función, puedes usarlas así:

is_first_floating_number_bigger 1.5 1.2
result="${__FUNCTION_RETURN}"

Una vez llamado, echo $resultserá 1en este caso, de lo contrario 0.

La función:

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}"
}

O una versión con salida de depuración:

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}"
}

Simplemente guarde la función en un .sharchivo separado e inclúyalo así:

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

Estaba publicando esto como respuesta a https://stackoverflow.com/a/56415379/1745001 cuando se cerró como un duplicado de esta pregunta, así que aquí está como se aplica aquí también:

Por simplicidad y claridad, simplemente use awk para los cálculos, ya que es una herramienta estándar de UNIX y es tan probable que esté presente como bc y mucho más fácil de trabajar sintácticamente.

Para esta pregunta:

$ 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

y para esa otra pregunta que se cerró como duplicación de esta:

$ 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
fuente
@DudiBoy no, es un código awk claro, simple y portátil o un código shell + bc no obvio, oscuro y dependiente de shell.
Ed Morton
3

awky herramientas como esta (te estoy mirando sed...) deberían relegarse al basurero de los proyectos antiguos, con un código que todo el mundo tiene demasiado miedo de tocar, ya que fue escrito en un lenguaje de nunca lectura.

O usted es el proyecto relativamente raro que necesita priorizar la optimización del uso de la CPU sobre la optimización del mantenimiento del código ... en cuyo caso, continúe.

Si no es así, ¿por qué no usar algo legible y explícito, como python? Tus compañeros codificadores y tu futuro yo te lo agradecerán. Puede usar en pythonlínea con bash como todos los demás.

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
fuente
@Witiko Mi versión original era un poco más sarcástica.
CivFan
Aún más sucinto: usar en not(...)lugar de0 if ... else 1
Neil Mayhew
1
Si relegas awk y sed (te estoy mirando CivFan) al basurero de la historia, eres un pésimo administrador de sistemas y estás escribiendo demasiado código. (Y me gusta y uso Python, así que no se trata de eso). -1 por mal humor fuera de lugar. Hay un lugar en el dominio de sistemas para esas herramientas, Python o no.
Mike S
1
Curiosamente, terminé con el buen viejo Perl! awk '${print $5}' ptpd_log_file | perl -ne '$_ > 0.000100 && print' > /tmp/outfile. Pan comido. Cada idioma tiene su lugar.
Mike S
1
No acumule awk con seck sintáctico seds. A diferencia de python, awk es una utilidad obligatoria en todas las instalaciones de UNIX y el equivalente de awk python -c "import sys; sys.exit(0 if float($num1) < float($num2) else 1)"es simplemente awk "BEGIN{exit ($num1 > $num2 ? 0 : 1)}".
Ed Morton
2

Esta secuencia de comandos puede ayudar a comprobar si la grailsversión instalada es mayor que la mínima requerida. Espero eso ayude.

#!/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
prayagupd
fuente
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
fuente
2

compruebe el código editado a continuación: -

#!/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

Esto funciona bien.

Gopika BG
fuente
2

Una solución que admite todas las notaciones posibles, incluida la notación científica con exponentes en mayúsculas y minúsculas (por ejemplo, 12.00e4):

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

Use korn shell, en bash puede que tenga que comparar la parte decimal por separado

#!/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
fuente
2
el problema es que muchas distribuciones no vienen con ksh instalado, y si su script va a ser usado por otros, tienden a no gustarles tener que instalar cosas adicionales, especialmente cuando es solo un script que se supone que está escrito en bash -uno pensaría que no necesitaban OTRO shell para hacer eso, lo que socava toda la razón de usar un script bash en primer lugar - seguro que TAMBIÉN podríamos codificarlo en C ++, pero ¿por qué?
osirisgothra
¿Cuáles son las distribuciones que vienen sin ksh instalado?
piokuc
1
@piokuc, por ejemplo, Ubuntu Desktop & Server. Yo diría que es bastante importante ...
Olli
Además, la pregunta específicamente pide una solución que funcione en bash. Puede haber muy buenas razones para eso. Digamos que es parte de una gran aplicación y no es factible migrar todo a ksh. O se está ejecutando en una plataforma integrada donde instalar otro shell es realmente un problema.
Olli
1

Usando bashj ( https://sourceforge.net/projects/bashj/ ), un mutante bash con soporte java, solo escribe (y ES fácil de leer):

#!/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

Por supuesto, la hibridación bashj bash / java ofrece mucho más ...

Fil
fuente
0

¿Qué tal esto? = 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
fuente
1
El guión Awk simplemente debe exit 0informar la verdad y exit 1devolver falso; entonces puede simplificar a la notablemente elegante if awk 'BEGIN { exit (ARGV[1] >= ARGV[2]) ? 0 : 1 }' "$VAL_TO_CHECK" 1; then... (aún más elegante si encapsula el script Awk en una función de shell).
tripleee