Cómo comparar con el número de coma flotante en un script de shell


22

Quiero comparar dos números de coma flotante en un script de shell. El siguiente código no funciona:

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

Respuestas:


5

Puede verificar por separado las partes enteras y fraccionarias:

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

Como dice Fered en los comentarios, funciona solo si ambos números tienen partes fraccionarias y ambas partes fraccionarias tienen el mismo número de dígitos. Aquí hay una versión que funciona para enteros o fraccionarios y cualquier operador 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

44
Esto no se puede solucionar sin mucho trabajo (intente comparar 0.5y 0.06). Será mejor que use una herramienta que ya entienda la notación decimal.
Gilles 'SO- deja de ser malvado'

Gracias Gilles, lo actualizó para que funcione de manera más general que la versión anterior.
ata

Tenga en cuenta que dice que 1.00000000000000000000000001es mayor que 2.
Stéphane Chazelas

Stéphane tiene razón. Es así debido a los límites de bits en la representación numérica de bash. Por supuesto, si quieres más sufrimiento, puedes usar tu propia representación ... :)
ata

35

Bash no entiende la aritmética de coma flotante. Trata los números que contienen un punto decimal como cadenas.

Use awk o bc en su lugar.

#!/bin/bash

min=12.45
val=10.35

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

echo "$min"

Si tiene la intención de hacer muchas operaciones matemáticas, probablemente sea mejor confiar en python o perl.


12

Puede usar el paquete num-utils para manipulaciones simples ...

Para matemáticas más serias, vea este enlace ... Describe varias opciones, por ejemplo.

  • R / Rscript (computación estadística GNU R y sistema de gráficos)
  • octava (principalmente compatible con Matlab)
  • bc (El lenguaje de calculadora de precisión arbitraria GNU bc)

Un ejemplo 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.

Aquí hay un bashtruco ... Agrega los primeros 0 al entero para hacer que una comparación de cadena de izquierda a derecha sea significativa. Este código particular requiere que tanto min como val tengan un punto decimal y al menos un dígito decimal.

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

salida:

min=10.35

10

Para cálculos simples sobre números de coma flotante (+ - * / y comparaciones), puede usar awk.

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

O, si tiene ksh93 o zsh (no bash), puede usar la aritmética integrada de su shell, que admite números de coma flotante.

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

Para cálculos de coma flotante más avanzados, busque bc . Realmente funciona en números de punto de fijación de precisión arbitraria.

Para trabajar en tablas de números, busque R ( ejemplo ).


6

Usar ordenación numérica

El comando sorttiene una opción -g( --general-numeric-sort) que se puede usar para comparaciones en <"menor que" o >"mayor que", encontrando el mínimo o el máximo.

Estos ejemplos son encontrar el mínimo:

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

Admite notación electrónica

Funciona con notación bastante general de números de coma flotante, como con la notación E

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

Tenga en cuenta que E-10, haciendo el primer número 0.000000001245, de hecho menor que 10.35.

Se puede comparar con el infinito

El estándar de coma flotante, IEEE754 , define algunos valores especiales. Para estas comparaciones, las interesantes son INFpara el infinito. También está el infinito negativo; Ambos son valores bien definidos en el estándar.

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

Para encontrar el uso máximo sort -gr lugar de sort -ginvertir el orden de clasificación:

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

Operación de comparación

Para implementar la <comparación ("menor que"), de modo que pueda usarse en ifetc., compare el mínimo con uno de los valores. Si el mínimo es igual al valor, comparado como texto , es menor que el otro valor:

$ 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

¡Buen consejo! Realmente me gusta su idea de que verificar a == min(a, b)es lo mismo que a <= b. Vale la pena señalar que esto no verifica estrictamente menos que sin embargo. Si desea hacer eso, debe verificar a == min(a, b) && a != max(a, b), en otras palabrasa <= b and not a >= b
Dave

3

Simplemente use ksh( ksh93precisamente) o zsh, que ambos admiten de forma nativa la aritmética de coma flotante:

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

Editar: Lo siento, me perdí ksh93ya fue sugerido. Mantener mi respuesta solo para dejar en claro que el script publicado en la pregunta de apertura se puede usar sin cambios fuera del interruptor de shell.

Edit2: tenga en cuenta que ksh93requiere que el contenido variable sea coherente con su configuración regional, es decir, con una configuración regional en francés, se debe utilizar una coma en lugar de un punto:

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

Una solución más robusta es establecer la configuración regional al comienzo del script para asegurarse de que funcionará independientemente de la configuración regional del usuario:

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

Tenga en cuenta que el script ksh93 anterior solo funciona en entornos locales donde está el separador decimal .(por lo tanto, no en la mitad del mundo donde está el separador decimal ,). zshNo tiene ese problema.
Stéphane Chazelas

De hecho, la respuesta editada para aclarar ese punto.
jlliagre

La configuración LC_NUMERIC no funcionará si el usuario ha configurado LC_ALL , eso también significa que los números no se mostrarán (o ingresarán) en el formato preferido del usuario. Ver unix.stackexchange.com/questions/87745/what-does-lc-all-c-do/… para un enfoque potencialmente mejor.
Stéphane Chazelas

@ StéphaneChazelas solucionó el problema LC_NUMERIC. Dada la sintaxis de la secuencia de comandos OP, supongo que su separador preferido es de .todos modos.
jlliagre

Sí, pero lo que importa es la configuración regional del usuario del script, no la configuración regional del autor del script. Como autor de guiones, debe tener en cuenta la localización y sus efectos secundarios.
Stéphane Chazelas

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

Que utiliza la dccalculadora para srasgaron el valor $minen el registro ay duplicates el valor de $valen la parte superior de su pila de ejecución principal. Luego lcoloca el contenido aen la parte superior de la pila, en cuyo punto se ve así:

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

La <hace estallar la parte superior dos entradas fuera de la pila y los compara. Entonces la pila se ve así:

${val}

Si la entrada superior era menor que la segunda hacia arriba, empuja el contenido ahacia arriba, por lo que la pila se ve así:

${min} ${val}

De lo contrario, no hace nada y la pila todavía se ve así:

${val} 

Luego simplemente pborra la entrada de la pila superior.

Entonces para tu problema:

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

###OUTPUT

12.35

Pero:

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

###OUTPUT

12.45

0

¿Por qué no usar viejo, bueno expr?

Ejemplo de sintaxis:

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

Para expresiones verdaderas , el código de salida expr es 0, con la cadena '1' enviada a stdout. Invertir para expresiones falsas .

He comprobado esto con GNU y FreeBSD 8 expr.


GNU expr solo admite la comparación aritmética en enteros. Su ejemplo utiliza una comparación lexicográfica que fallará en números negativos. Por ejemplo, expr 1.09 '<' -1.1imprimirá 1y saldrá con 0(éxito).
Adrian Günter

0

Para verificar si dos números (posiblemente fraccionarios) están en orden, sortes (razonablemente) portátil:

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

Sin embargo, si realmente desea mantener actualizado un valor mínimo, entonces no necesita un if. Ordena los números y usa siempre el primero (mínimo):

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

0

Por lo general, hago cosas similares con el código de Python incrustado:

#!/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
¿Puede comentar su respuesta y agregar algunas explicaciones
Romeo Ninov
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.