Jak porównać dwie liczby zmiennoprzecinkowe w Bash?

156

Bardzo się staram porównać dwie liczby zmiennoprzecinkowe w skrypcie bash. Mam do zmiennych np

let num1=3.17648e-22
let num2=1.5

Teraz chcę tylko zrobić proste porównanie tych dwóch liczb:

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

Niestety mam pewne problemy z odpowiednim traktowaniem num1, który może mieć „e-format”. :(

Każda pomoc, podpowiedzi są mile widziane!

Jonas
źródło
2
Przez „e-format” mam na myśli notację wykładniczą (zwaną również notacją naukową)
Jonas,

Odpowiedzi:

181

Wygodniej

Można to zrobić wygodniej, korzystając z kontekstu numerycznego Basha:

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

Wyjaśnienie

Przepływ przez podstawowe polecenie kalkulatora bczwraca 1 lub 0.

Ta opcja -ljest równoważna --mathlib; ładuje standardową bibliotekę matematyczną.

Umieszczenie całego wyrażenia w podwójnym nawiasie (( ))spowoduje przetłumaczenie tych wartości odpowiednio na prawdę lub fałsz.

Upewnij się, że bcjest zainstalowany podstawowy pakiet kalkulatora.

Działa to również w przypadku pływaków w formacie naukowym, pod warunkiem użycia dużej litery E, npnum1=3.44E6

Serge Stroobandt
źródło
1
Ten sam problem co stackoverflow.com/questions/8654051/ ... np. $ Echo "1.1 + 2e + 02" | bc (standard_in) 1: błąd składni
Nemo
1
@MohitArora Upewnij się, że masz bczainstalowany pakiet kalkulatora.
Serge Stroobandt
1
Dostaję 0: not foundz oświadczeniem if (( $(echo "$TOP_PROCESS_PERCENTAGE > $THRESHOLD" | bc -l) )); then.
Stephane
1
Wszystkim, którzy otrzymają komunikat „polecenie nie znaleziono”, pamiętajcie, że należy dołączyć znak bcw jeden z apostrofów lub, $()a następnie w (( ))… tj. (( $(bc -l<<<"$a>$b") ))I nie (( bc -l<<<"$a>$b" )).
Normadize
@Nemo Wpisz liczby w notacji naukowej wielką literą E, a wszystkie błędy składniowe znikną.
Serge Stroobandt
100

bash obsługuje tylko liczby całkowite, ale możesz użyć bcpolecenia w następujący sposób:

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

Zauważ, że znak potęgi musi być pisany wielką literą

alrusdi
źródło
3
tak, ale aby obejść niepoprawne obliczenia, konieczne jest
wpisanie dużej litery ``
2
Powinieneś więc zwrócić na to uwagę w swojej odpowiedzi, zamiast po prostu zamieścić bardzo podobne rozwiązanie i nie wspominać o ważnych różnicach.
Daniel Persson,
4
Nie jest to bardzo podobne rozwiązanie. Rozwiązanie Alrusdiego wykorzystuje to bcnarzędzie i to właśnie poleciłbym każdemu programiście BASH. BASH jest językiem bez typu. Tak, może wykonywać arytmetykę na liczbach całkowitych, ale dla zmiennoprzecinkowych musisz użyć jakiegoś zewnętrznego narzędzia. BC jest najlepsza, ponieważ do tego jest stworzona.
DejanLekic
8
Ponieważ próbuje go użyć w stwierdzeniu if, pokazałbym to. if [$ (... | bc -l) == 1]; potem ...
Robert Jacobs
27

Lepiej jest używać awkdo matematyki niecałkowitej. Możesz użyć tej funkcji narzędzia bash:

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

I nazwij to:

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
źródło
2
Podoba mi się ta odpowiedź, ludzie mają tendencję do unikania początkujących awk esp, wydaje im się, że jest to trudniejsze niż w rzeczywistości, myślę, że ludzie są zastraszani przez nawiasy klamrowe i pozornie mieszaną składnię językową (na pierwszy rzut oka). A ponieważ awk jest praktycznie również obecny w systemie docelowym, tak jak bc (nie wiadomo, który z nich, jeśli w ogóle, NIE jest kiedykolwiek zainstalowany). Uwielbiam skrypty bash, ale to brak zmiennoprzecinkowych, nawet skąpych 2 miejsc po przecinku (chyba ktoś mógłby napisać na to „fałszywe” opakowanie), naprawdę jest irytujące ...
osirisgothra
2
Używanie awki bcw skryptach powłoki jest standardową praktyką od czasów starożytnych, powiedziałbym, że niektóre funkcje nigdy nie zostały dodane do powłok, ponieważ są dostępne w awk, bc i innych narzędziach Uniksa. Nie ma potrzeby zachowania czystości w skryptach powłoki.
piokuc
1
@WanderingMind Jednym ze sposobów byłoby przekazanie 0 lub 1 do, exittak aby Awk przekazał wynik z powrotem do powłoki we właściwy, czytelny dla komputera sposób. if awk -v n1="123.456" -v n2="3.14159e17" 'BEGIN { exit (n1 <= n2) }' /dev/null; then echo bigger; else echo not; fi... jednak zwróć uwagę, jak warunek jest odwrócony (kod wyjścia 0 oznacza powodzenie powłoki).
tripleee
1
Dlaczego właśnie python. Masz perldomyślnie instalowany na wielu systemach Linux / Unix .. nawet phptakże
anubhava
1
To awkrozwiązanie jest bardziej niezawodne w moim przypadku niż to, bcktóre zwraca nieprawidłowe wyniki z powodu, którego nie dostałem.
MBR,
22

Czyste rozwiązanie bash do porównywania liczb zmiennoprzecinkowych bez wykładniczej notacji, wiodących lub końcowych zer:

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

Kolejność operatorów logicznych ma znaczenie . Części całkowite są porównywane jako liczby, a części ułamkowe są celowo porównywane jako łańcuchy. Za pomocą tej metody zmienne są dzielone na części całkowite i ułamkowe .

Nie porównuje liczb zmiennoprzecinkowych z liczbami całkowitymi (bez kropki).

użytkownik
źródło
15

można użyć awk w połączeniu z warunkiem bash if, awk wypisze 1 lub 0, a te zostaną zinterpretowane przez klauzulę if z wartością true lub false .

if awk 'BEGIN {print ('$d1' >= '$d2')}'; then
    echo "yes"
else 
    echo "no"
fi
ungalcrys
źródło
Używanie awk jest świetne, ponieważ obsługuje liczby zmiennoprzecinkowe, ale osobiście wolę syntaktykęif (( $(echo $d1 $d2 | awk '{if ($1 > $2) print 1;}') )); then echo "yes"; else echo "no"; fi
David Georg Reichelt
7

uważaj podczas porównywania liczb, które są wersjami pakietów, na przykład sprawdzając, czy grep 2.20 jest większy niż wersja 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

Rozwiązałem taki problem z taką funkcją powłoki / 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
źródło
W systemach opartych na Debianie dpkg --compare-versionsjest często przydatne. Posiada pełną logikę porównywania wbudowanych wersji pakietów Debiana, które są bardziej złożone niż tylko x.y.
Neil Mayhew
5

Oczywiście, jeśli nie potrzebujesz naprawdę arytmetyki zmiennoprzecinkowej, tylko arytmetyki na np. Wartościach w dolarach, gdzie zawsze są dokładnie dwie cyfry dziesiętne, możesz po prostu upuścić kropkę (skutecznie mnożąc przez 100) i porównać otrzymane liczby całkowite.

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

To oczywiście wymaga upewnienia się, że obie wartości mają taką samą liczbę miejsc po przecinku.

tripleee
źródło
3

Użyłem odpowiedzi stąd i umieściłem je w funkcji, możesz jej użyć w ten sposób:

is_first_floating_number_bigger 1.5 1.2
result="${__FUNCTION_RETURN}"

Po wywołaniu echo $resultbędzie 1w tym przypadku, w przeciwnym razie0 .

Funkcja:

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

Lub wersja z wyjściem debugowania:

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

Po prostu zapisz funkcję w oddzielnym .shpliku i dołącz ją w następujący sposób:

. /path/to/the/new-file.sh
Thomas Kekeisen
źródło
3

Publikowałem to jako odpowiedź na https://stackoverflow.com/a/56415379/1745001, kiedy zostało zamknięte jako dupek tego pytania, więc tutaj jest tak, jak ma to zastosowanie również tutaj:

Dla uproszczenia i przejrzystości po prostu użyj awk do obliczeń, ponieważ jest to standardowe narzędzie UNIX, a więc tak samo prawdopodobne, jak bc i znacznie łatwiejsze w obsłudze składniowej.

W przypadku tego pytania:

$ 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

i dla tego innego pytania, które zostało zamknięte jako dupek tego:

$ 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
źródło
@DudiBoy nie, to przejrzysty, prosty, przenośny kod awk lub nieoczywisty, niejasny, zależny od powłoki kod powłoki + kod BC.
Ed Morton
3

awki takie narzędzia (patrzę na ciebie sed...) powinny zostać wyrzucone na śmietnik starych projektów, z kodem, którego wszyscy boją się dotykać, ponieważ został napisany w języku, którego nigdy nie czytasz.

Lub jesteś stosunkowo rzadkim projektem, który wymaga priorytetu optymalizacji wykorzystania procesora nad optymalizacją konserwacji kodu ... w takim przypadku kontynuuj.

Jeśli nie, to dlaczego nie zamiast tego po prostu użyć czegoś czytelnego i wyraźnego, takiego jak python? Twoi koledzy z kodowania i przyszłe ja będą Ci wdzięczni. Możesz używać pythoninline z bashem, tak jak wszystkich innych.

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
źródło
@Witiko Moja oryginalna wersja była nieco bardziej złośliwa.
CivFan
Jeszcze bardziej zwięźle: użyj not(...)zamiast0 if ... else 1
Neil Mayhew
1
Jeśli przenosisz awk i sed (patrzę na ciebie CivFan) na śmietnik historii, jesteś kiepskim administratorem systemów i wpisujesz za dużo kodu. (A ja lubię i używam Pythona, więc nie chodzi o to). -1 za zagubioną złośliwość. W domenie systemów jest miejsce na te narzędzia, Python lub nie.
Mike S
1
Co ciekawe, skończyło się na starym dobrym Perlu! awk '${print $5}' ptpd_log_file | perl -ne '$_ > 0.000100 && print' > /tmp/outfile. Bułka z masłem. Każdy język ma swoje miejsce.
Mike S,
1
Nie wbijaj awk w syntacic zwariowane seda. W przeciwieństwie do pythona, awk jest obowiązkowym narzędziem w każdej instalacji systemu UNIX, a jego odpowiednikiem python -c "import sys; sys.exit(0 if float($num1) < float($num2) else 1)"jest po prostu awk "BEGIN{exit ($num1 > $num2 ? 0 : 1)}".
Ed Morton
2

Ten skrypt może pomóc, gdy sprawdzam, czy zainstalowana grailswersja jest większa niż wymagane minimum. Mam nadzieję, że to pomoże.

#!/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
źródło
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
źródło
2

sprawdź poniższy edytowany kod: -

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

to działa dobrze.

Gopika BG
źródło
2

Rozwiązanie obsługujące wszystkie możliwe notacje, w tym notację naukową z wykładnikami zarówno dużymi, jak i małymi literami (np. 12.00e4):

if (( $(bc -l <<< "${value1/e/E} < ${value2/e/E}") ))
then
    echo "$value1 is below $value2"
fi 
Danila Piatov
źródło
1

Użyj powłoki Korn, w bashu może być konieczne oddzielne porównanie części dziesiętnej

#!/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
źródło
2
problem polega na tym, że wiele dystrybucji nie ma zainstalowanego ksh, a jeśli twój skrypt ma być używany przez innych, zwykle nie lubią oni instalować dodatkowych rzeczy, zwłaszcza gdy jest to tylko skrypt, który powinien być napisany w bash - ktoś mógłby pomyśleć, że nie potrzebuje do tego INNEJ powłoki, co podważa cały powód używania skryptu bash w pierwszej kolejności - jasne, że moglibyśmy RÓWNIEŻ zakodować go w C ++, ale dlaczego?
osirisgothra
Jakie dystrybucje są dostarczane bez zainstalowanego ksh?
piokuc
1
@piokuc, na przykład Ubuntu Desktop & Server. Powiedziałbym, że jest dość poważny ...
Olli
Ponadto pytanie dotyczy konkretnie rozwiązania, które działa w bash. Mogą istnieć naprawdę dobre powody. Powiedzmy, że jest to część dużej aplikacji i migracja wszystkiego do ksh nie jest możliwa. Lub działa na platformie osadzonej, gdzie instalacja innej powłoki jest naprawdę problemem.
Olli
1

Używając bashj ( https://sourceforge.net/projects/bashj/ ), mutanta bash z obsługą java, po prostu piszesz (i JEST łatwy do odczytania):

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

Oczywiście hybryda bashj bash / java oferuje znacznie więcej ...

Fil
źródło
0

Co powiesz na to? = 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
źródło
1
Skrypt Awk powinien po prostu exit 0zgłaszać prawdę i exit 1zwracać fałsz; następnie możesz uprościć do niezwykle eleganckiego if awk 'BEGIN { exit (ARGV[1] >= ARGV[2]) ? 0 : 1 }' "$VAL_TO_CHECK" 1; then... (jeszcze bardziej eleganckiego, jeśli umieścisz skrypt Awk w funkcji powłoki).
tripleee