Como comparar dois números de ponto flutuante no Bash?

156

Estou tentando comparar dois números de ponto flutuante em um script bash. Eu tenho que variáveis, por exemplo

let num1=3.17648e-22
let num2=1.5

Agora, eu só quero fazer uma comparação simples desses dois números:

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

Infelizmente, tenho alguns problemas com o tratamento correto do num1, que pode ser do "formato eletrônico". :(

Qualquer ajuda, dicas são bem-vindas!

Jonas
fonte
2
Com o "e-formato" Quero dizer a notação exponencial (também chamado de notação científica)
Jonas

Respostas:

181

Mais convenientemente

Isso pode ser feito de maneira mais conveniente usando o contexto numérico do Bash:

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

Explicação

A tubulação através do comando básico da calculadora bcretorna 1 ou 0.

A opção -lé equivalente a --mathlib; carrega a biblioteca matemática padrão.

Colocar a expressão inteira entre parênteses duplos (( ))converterá esses valores em respectivamente verdadeiro ou falso.

Por favor, verifique se o bcpacote básico da calculadora está instalado.

Isso também funciona para carros alegóricos em formato científico, desde que uma letra maiúscula Eseja empregada, por exemplonum1=3.44E6

Serge Stroobandt
fonte
1
Mesmo problema que o stackoverflow.com/questions/8654051/…, por exemplo, $ echo "1.1 + 2e + 02" | bc (standard_in) 1: erro de sintaxe
Nemo
1
@MohitArora Por favor, verifique se você tem o bcpacote da calculadora instalado.
Serge Stroobandt
1
Eu recebo um 0: not foundcom a declaração if (( $(echo "$TOP_PROCESS_PERCENTAGE > $THRESHOLD" | bc -l) )); then.
Stephane
1
Para todos aqueles que obtiverem "comando não encontrado", lembre-se de que você deve colocar o bcbackticks nos backticks ou $()então em (( ))... ie (( $(bc -l<<<"$a>$b") ))e não (( bc -l<<<"$a>$b" )).
precisa saber é o seguinte
@ Nemo Escreva números em notação científica com uma letra maiúscula Ee todos os erros de sintaxe desaparecerão.
Serge Stroobandt
100

O bash lida apenas com números inteiros, mas você pode usar o bccomando da seguinte maneira:

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

Observe que o sinal do expoente deve estar em maiúsculas

alrusdi
fonte
3
sim, mas para resolver cálculos incorretos sua necessidade para maiúsculas 'e' sinal na notação de número científico e usar flag -l para programm bc para rotinas de matemática predefinidos
alrusdi
2
você deve apontar isso na sua resposta, em vez de apenas postar uma solução muito semelhante e não mencionar as diferenças importantes.
Daniel Persson
4
Não é uma solução muito semelhante . A solução de Alrusdi usa a bcferramenta e é isso que eu recomendaria a qualquer programador BASH. BASH é uma linguagem sem letra. Sim, ele pode fazer aritmética inteira, mas para ponto flutuante você deve usar alguma ferramenta externa. BC é o melhor, porque é para isso que é feito.
DejanLekic
8
Como ele está tentando usá-lo em uma declaração if, eu mostraria isso. if [$ (... | bc -l) == 1]; então ...
Robert Jacobs
27

É melhor usar awkpara matemática não inteira. Você pode usar esta função do utilitário bash:

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

E chame-o 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
fonte
2
Eu gosto dessa resposta, as pessoas tendem a se afastar dos iniciantes, especialmente os que parecem mais difíceis do que realmente são, acho que as pessoas ficam intimidadas com o uso de chaves e a sintaxe aparentemente mista da linguagem (de relance). E como também é garantido que o awk também esteja presente no sistema de destino, assim como o bc (não tenho certeza de qual deles, se houver, NÃO está instalado). Eu amo o bash scripting, mas que nenhum ponto flutuante, nem mesmo um magro 2 casas decimais (eu acho que alguém poderia escrever um wrapper 'fake' para isso), é realmente irritante ...
osirisgothra
2
Usar awke bcem scripts de shell é uma prática padrão desde os tempos antigos, eu diria que alguns recursos nunca foram adicionados aos shells porque estão disponíveis no awk, bc e outras ferramentas Unix. Não há necessidade de pureza em scripts de shell.
Piokuc 15/09/14
1
@WanderingMind Uma maneira de fazer isso seria passar o 0 ou 1 para exitque o Awk comunique o resultado de volta ao shell de maneira adequada e legível 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... embora observe como a condição é invertida (o status de saída 0 significa sucesso para o shell).
Tripleee
1
Por que apenas python. Você perlinstalado por padrão em muitos sistemas Linux / Unix .. mesmo phptambém
anubhava
1
Essa awksolução é mais robusta no meu caso do que a bcque retorna resultados errados por um motivo que não obtive.
MBR
22

Solução de bash pura para comparar flutuadores sem notação exponencial, zeros à esquerda ou à direita:

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

A ordem dos operadores lógicos é importante . Partes inteiras são comparadas como números e partes fracionárias são intencionalmente comparadas como seqüências de caracteres. As variáveis ​​são divididas em partes inteiras e fracionárias usando este método .

Não compara flutuações com números inteiros (sem ponto).

do utilizador
fonte
15

você pode usar o awk combinado com uma condição bash if, o awk imprimirá 1 ou 0 e esses serão interpretados pela cláusula if com true ou false .

if awk 'BEGIN {print ('$d1' >= '$d2')}'; then
    echo "yes"
else 
    echo "no"
fi
ungalcrys
fonte
Usando awk é grande, uma vez que é capaz de lidar com números de ponto flutuante, mas eu pessoalmente prefiro o synthaxif (( $(echo $d1 $d2 | awk '{if ($1 > $2) print 1;}') )); then echo "yes"; else echo "no"; fi
David Georg Reichelt
7

tenha cuidado ao comparar números que são versões de pacotes, como verificar se o grep 2.20 é maior que a versão 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

Eu resolvi esse problema com essa função 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
fonte
Em um sistema baseado no Debian, dpkg --compare-versionsgeralmente é útil. Ele tem a lógica completa para comparar as versões dos pacotes Debian embutidas nele, que são mais complexas do que justas x.y.
Neil Mayhew
5

Obviamente, se você não precisa realmente de aritmética de ponto flutuante, apenas aritmética em, por exemplo, valores em dólares, onde sempre há exatamente dois dígitos decimais, você pode simplesmente soltar o ponto (multiplicando efetivamente por 100) e comparar os números inteiros resultantes.

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

Obviamente, isso exige que você tenha certeza de que ambos os valores tenham o mesmo número de casas decimais.

triplo
fonte
3

Eu usei as respostas daqui e as coloquei em uma função, você pode usá-las assim:

is_first_floating_number_bigger 1.5 1.2
result="${__FUNCTION_RETURN}"

Uma vez chamado, echo $resultserá 1neste caso, caso contrário 0.

A função:

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 uma versão com saída de depuração:

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

Apenas salve a função em um .sharquivo separado e inclua-a assim:

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

Eu estava postando isso como uma resposta para https://stackoverflow.com/a/56415379/1745001 quando ele foi fechado como um dup dessa pergunta, então aqui está como se aplica aqui também:

Para simplicidade e clareza, basta usar o awk para os cálculos, pois é uma ferramenta UNIX padrão e com a mesma probabilidade de estar presente como bc e muito mais fácil trabalhar sintaticamente.

Para esta pergunta:

$ 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

e para essa outra pergunta que foi encerrada como um dup desta:

$ 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
fonte
@DudiBoy não, é um código awk claro, simples e portátil ou um código bc não-óbvio, obscuro e dependente de shell.
Ed Morton
3

awke ferramentas como essa (estou olhando para você sed...) devem ser relegadas para o caixote do lixo de projetos antigos, com um código que todo mundo tem medo de tocar desde que foi escrito em um idioma de leitura nunca.

Ou você é um projeto relativamente raro que precisa priorizar a otimização do uso da CPU em vez da otimização da manutenção do código ... nesse caso, continue.

Caso contrário, por que não usar apenas algo legível e explícito, como python? Seus colegas programadores e o seu futuro serão agradecidos. Você pode usar pythoninline com bash como todos os outros.

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
fonte
@Witiko Minha versão original era um pouco mais esquisita.
CivFan
Ainda mais sucinta: usar not(...), em vez de0 if ... else 1
Neil Mayhew
1
Se você está relegando awk e sed (estou olhando para você CivFan) para o caixote do lixo da história, você é um péssimo administrador de sistemas e está digitando muito código. (E eu gosto e uso Python, então não é sobre isso). -1 para snarkiness extraviado. Há um lugar no domínio de sistemas para essas ferramentas, Python ou não.
Mike S
1
Curiosamente, acabei com o bom e velho Perl! awk '${print $5}' ptpd_log_file | perl -ne '$_ > 0.000100 && print' > /tmp/outfile. Mole-mole. Toda língua tem seu lugar.
Mike S
1
Não agrupe awk com sack malícia sintática. Diferentemente do python, o awk é um utilitário obrigatório em todas as instalações do UNIX e o equivalente do awk python -c "import sys; sys.exit(0 if float($num1) < float($num2) else 1)"é simplesmente awk "BEGIN{exit ($num1 > $num2 ? 0 : 1)}".
Ed Morton
2

Esse script pode ajudar onde estou verificando se a grailsversão instalada é maior que o mínimo necessário. Espero que ajude.

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

verifique o código editado abaixo: -

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

isso funciona bem.

Gopika BG
fonte
2

Uma solução que suporta todas as notações possíveis, incluindo a notação científica com expoentes em maiúsculas e minúsculas (por exemplo, 12.00e4):

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

Use o shell korn, no bash você pode precisar comparar a parte decimal separadamente

#!/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
fonte
2
o problema é que muitas distribuições não vêm com o ksh instalado, e se o seu script for usado por outras pessoas, elas tendem a não gostar de instalar coisas extras, especialmente quando é apenas um script que deve ser escrito no bash - alguém pensaria que eles não precisavam de OUTRO shell para fazer isso, o que prejudica toda a razão de usar um script bash em primeiro lugar - com certeza nós também podemos codificá-lo em C ++, mas por quê?
Osirisgothra #
Quais são as distribuições que vêm sem o ksh instalado?
piokuc 15/09/14
1
@piokuc, por exemplo, Ubuntu Desktop & Server. Eu diria que é um dos principais ...
Olli
Além disso, a pergunta pede especificamente uma solução que funcione no bash. Pode haver realmente boas razões para isso. Digamos, faz parte de um aplicativo grande e a migração de tudo para o ksh não é viável. Ou está sendo executado em uma plataforma incorporada, onde a instalação de outro shell é realmente um problema.
Olli
1

Usando o bashj ( https://sourceforge.net/projects/bashj/ ), um mutante do bash com suporte a java, basta escrever (e é fácil de ler):

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

Obviamente, a hibridação bashj bash / java oferece muito mais ...

Fil
fonte
0

Que tal agora? = 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
fonte
1
O script Awk deve simplesmente exit 0relatar a verdade e exit 1retornar falso; então você pode simplificar para o extraordinariamente elegante if awk 'BEGIN { exit (ARGV[1] >= ARGV[2]) ? 0 : 1 }' "$VAL_TO_CHECK" 1; then... (mais elegante ainda se você encapsular o script Awk em uma função shell).
Tripleee