Como comparar com o número de ponto flutuante em um script de shell

22

Eu quero comparar dois números de ponto flutuante em um script de shell. O código a seguir não está funcionando:

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

Respostas:

5

Você pode verificar separadamente as partes inteiras e fracionárias:

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

Como diz Fered nos comentários, ele funciona apenas se ambos os números tiverem partes fracionárias e ambas as partes fracionais tiverem o mesmo número de dígitos. Aqui está uma versão que funciona para números inteiros ou fracionários e para qualquer 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
ata
fonte
4
Isso não pode ser corrigido sem muito trabalho (tente comparar 0.5e 0.06). É melhor usar uma ferramenta que já entenda a notação decimal.
Gilles 'SO- stop be evil'
Obrigado Gilles, atualizou-o para funcionar de maneira mais geral que a versão anterior.
ata
Observe que ele diz que 1.00000000000000000000000001é maior que 2.
Stéphane Chazelas
Stéphane está certo. É assim por causa dos limites de bits na representação numérica do bash. Claro, se você quiser mais sofrimento que você poderia usar a sua própria representação .... :)
ata
35

O Bash não entende a aritmética de ponto flutuante. Ele trata números que contêm um ponto decimal como seqüências de caracteres.

Use awk ou bc.

#!/bin/bash

min=12.45
val=10.35

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

echo "$min"

Se você pretende fazer muitas operações matemáticas, provavelmente é melhor confiar em python ou perl.

Daniel Näslund
fonte
12

Você pode usar o pacote num-utils para manipulações simples ...

Para matemática mais séria, consulte este link ... Ele descreve várias opções, por exemplo.

  • R / Rscript (sistema estatístico de computação e gráficos GNU R)
  • oitava (principalmente compatível com Matlab)
  • bc (a linguagem da calculadora de precisão arbitrária GNU bc)

Um exemplo 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.

Aqui está um bashhack ... Ele adiciona zeros iniciais ao número inteiro para tornar significativa uma comparação da esquerda para a direita. Esse trecho de código específico requer que min e val tenham um ponto decimal e pelo menos um 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

saída:

min=10.35
Peter.O
fonte
10

Para cálculos simples em números de ponto flutuante (+ - * / e comparações), você pode usar o awk.

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

Ou, se você tiver ksh93 ou zsh (não bash), poderá usar a aritmética interna do seu shell, que suporta números de ponto flutuante.

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

Para cálculos mais avançados de ponto flutuante, procure bc . Na verdade, ele funciona em números de pontos de correção de precisão arbitrária.

Para trabalhar em tabelas de números, procure R ( exemplo ).

Gilles 'SO- parar de ser mau'
fonte
6

Usar classificação numérica

O comando sortpossui uma opção -g( --general-numeric-sort) que pode ser usada para comparações em <"menor que" ou >"maior que", localizando o mínimo ou o máximo.

Estes exemplos estão encontrando o mínimo:

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

Suporta Notação E

Ele funciona com notação bastante geral de números de ponto flutuante, como na Notação E

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

Observe o E-10, tornando o primeiro número 0.000000001245, na verdade menor que 10.35.

Pode comparar com o infinito

O padrão de ponto flutuante, IEEE754 , define alguns valores especiais. Para essas comparações, as interessantes são INFpara o infinito. Há também o infinito negativo; Ambos são valores bem definidos no padrão.

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

Para encontrar o uso máximo em sort -grvez de sort -g, invertendo a ordem de classificação:

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

Operação de comparação

Para implementar a <comparação ("menor que"), para que possa ser usada em ifetc, compare o mínimo com um dos valores. Se o mínimo for igual ao valor, comparado como texto , é menor que o outro 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
Volker Siegel
fonte
Boa dica! Eu realmente gosto do seu insight de que verificar a == min(a, b)é o mesmo que a <= b. Vale a pena notar que isso não verifica estritamente menos do que no entanto. Se você quiser fazer isso, precisará verificar a == min(a, b) && a != max(a, b), em outras palavrasa <= b and not a >= b
Dave
3

Basta usar ksh(com ksh93precisão) ou zsh, que suportam nativamente aritmética de ponto flutuante:

$ 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: Desculpe, eu perdi ksh93já foi sugerido. Manter minha resposta apenas para esclarecer o script postado na pergunta de abertura pode ser usada sem alterações fora do switch do shell.

Edit2: Observe que ksh93exige que o conteúdo da variável seja consistente com o seu código do idioma, ou seja, com um código do idioma francês, uma vírgula em vez de um ponto deve ser usada:

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

Uma solução mais robusta é definir o código do idioma no início do script para garantir que ele funcione independentemente do código do idioma do usuário:

...
export LC_ALL=C
min=12.45
val=10.35
...
jlliagre
fonte
Observe que o script ksh93 acima funciona apenas em locais onde o separador decimal está .(portanto, não na metade do mundo onde o separador decimal está ,). zshnão tem esse problema.
Stéphane Chazelas
De fato, resposta editada para esclarecer esse ponto.
Jlliagre
Definir LC_NUMERIC não funcionará se o usuário tiver definido LC_ALL, isso também significa que os números não serão exibidos (ou inseridos) no formato preferido do usuário. Consulte unix.stackexchange.com/questions/87745/what-does-lc-all-c-do/… para obter uma abordagem potencialmente melhor.
Stéphane Chazelas
@ StéphaneChazelas corrigiu o problema LC_NUMERIC. Dada a sintaxe do script OP, presumo que seu separador preferido seja o que for ..
Jlliagre
Sim, mas é a localidade do usuário do script, não a localidade do autor do script que importa. Como autor do script, você deve levar em consideração a localização e seus efeitos colaterais.
Stéphane Chazelas
1
min=$(echo "${min}sa ${val}d la <a p" | dc)

Que usa a dccalculadora para srasgou o valor $minno registo ae duplicates o valor $valpara o topo de sua principal pilha de execução. Em seguida, lcoloca o conteúdo ano topo da pilha, quando é parecido com:

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

O <aparece no topo duas entradas fora da pilha e compara-los. Portanto, a pilha se parece com:

${val}

Se a entrada superior for menor que a segunda para cima, ela enviará o conteúdo apara a parte superior, para que a pilha se pareça com:

${min} ${val}

Senão, ele não faz nada e a pilha ainda se parece com:

${val} 

Depois, apenas pcria a entrada da pilha superior.

Então, para o seu problema:

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

###OUTPUT

12.35

Mas:

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

###OUTPUT

12.45
mikeserv
fonte
0

Por que não usar velho, bom expr?

Exemplo de sintaxe:

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

Para expressões verdadeiras , o código de saída expr é 0, com a string '1' enviada para stdout. Inverter para expressões falsas .

Eu verifiquei isso com o GNU e FreeBSD 8 expr.

sam_pan_mariusz
fonte
O GNU expr suporta apenas comparação aritmética em números inteiros. Seu exemplo usa comparação lexicográfica que falhará em números negativos. Por exemplo, expr 1.09 '<' -1.1imprimirá 1e sairá com 0(sucesso).
Adrian Günter
0

Para verificar se dois números (possivelmente fracionários) estão em ordem, sorté (razoavelmente) 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

No entanto, se você realmente deseja manter um valor mínimo atualizado, não precisa de um if. Classifique os números e use sempre o primeiro (menos):

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

Normalmente, faço coisas semelhantes com o código python incorporado:

#!/bin/sh

min=12.45
val=10.35

python - $min $val<<EOF
if ($min > $val):
        print $min
else: 
        print $val
EOF
dganesh2002
fonte
-1
$ min=12.45
$ val=10.35
$ [ "$min" \< "$val" ] && echo $val || echo $min
$ 12.45
$ val=13
$ [ "$min" \< "$val" ] && echo $val || echo $min
$ 13
沙漠 之 子
fonte
3
Você pode, por favor, comentar sua resposta e adicionar algumas explicações
Romeo Ninov