Jak porównać z liczbą zmiennoprzecinkową w skrypcie powłoki


22

Chcę porównać dwie liczby zmiennoprzecinkowe w skrypcie powłoki. Poniższy kod nie działa:

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

Odpowiedzi:


5

Można osobno sprawdzić części całkowite i ułamkowe:

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

Jak zauważył Fered w komentarzach, działa to tylko wtedy, gdy obie liczby mają części ułamkowe, a obie części ułamkowe mają tę samą liczbę cyfr. Oto wersja, która działa dla liczb całkowitych lub ułamkowych i dowolnego operatora 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

4
Nie można tego naprawić bez dużego nakładu pracy (spróbuj porównać 0.5i 0.06). Lepiej użyj narzędzia, które już rozumie zapis dziesiętny.
Gilles „SO- przestań być zły”

Dzięki Gilles, zaktualizowałem go, aby działał bardziej ogólnie niż wcześniejsza wersja.
ata

Zauważ, że jest napisane, że 1.00000000000000000000000001jest większe niż 2.
Stéphane Chazelas

Stéphane ma rację. Jest tak z powodu ograniczeń bitowych w reprezentacji liczbowej bash. Oczywiście, jeśli chcesz więcej cierpienia można użyć własnej reprezentacji .... :)
ATA

35

Bash nie rozumie arytmetyki zmiennoprzecinkowej. Liczby zawierające przecinek dziesiętny traktuje jak ciągi znaków.

Zamiast tego użyj awk lub bc.

#!/bin/bash

min=12.45
val=10.35

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

echo "$min"

Jeśli zamierzasz wykonywać wiele operacji matematycznych, prawdopodobnie lepiej polegać na Pythonie lub Perlu.


12

Możesz użyć pakietu num-utils do prostych manipulacji ...

Aby uzyskać więcej informacji na temat matematyki, zobacz ten link ... Opisuje kilka opcji, np.

  • R / Rscript (system obliczeń statystycznych GNU R i system graficzny)
  • oktawa (głównie kompatybilny z Matlab)
  • bc (język kalkulatora dowolnej precyzji GNU bc)

Przykład 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.

Oto bashhack ... Dodaje wiodące zera do liczby całkowitej, aby sensowne było porównywanie ciągów od lewej do prawej. Ten konkretny fragment kodu wymaga, aby zarówno min, jak i val faktycznie miały kropkę dziesiętną i co najmniej jedną cyfrę dziesiętną.

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

wydajność:

min=10.35

10

Do prostych obliczeń na liczbach zmiennoprzecinkowych (+ - * / i porównaniach) możesz użyć awk.

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

Lub, jeśli masz ksh93 lub zsh (nie bash), możesz użyć wbudowanej arytmetyki powłoki, która obsługuje liczby zmiennoprzecinkowe.

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

Aby uzyskać bardziej zaawansowane obliczenia zmiennoprzecinkowe, wyszukaj temat bc . W rzeczywistości działa na liczbach punktów o dowolnej dokładności.

Aby pracować na tablicach liczb, poszukaj R ( przykład ).


6

Użyj sortowania numerycznego

Polecenie sortma opcję -g( --general-numeric-sort), której można użyć do porównań <„mniej niż” lub >„większy niż” poprzez znalezienie minimum lub maksimum.

Te przykłady znajdują minimum:

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

Obsługuje notację elektroniczną

Działa z dość ogólną notacją liczb zmiennoprzecinkowych, jak w przypadku notacji E.

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

Zwróć uwagę na to E-10, że pierwsza liczba jest 0.000000001245rzeczywiście mniejsza niż 10.35.

Można porównać do nieskończoności

Standard zmiennoprzecinkowy, IEEE754 , definiuje niektóre wartości specjalne. Dla tych porównań interesujące są INFdla nieskończoności. Istnieje również ujemna nieskończoność; Obie są dobrze zdefiniowanymi wartościami w standardzie.

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

Aby znaleźć maksymalne wykorzystanie sort -grzamiast sort -g, odwracanie kolejności sortowania:

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

Operacja porównania

Aby wdrożyć porównanie <(„mniejsze niż”), aby można go było stosować w ifitp., Porównaj minimum z jedną z wartości. Jeśli minimum jest równe wartości, w porównaniu z tekstem , jest mniejsze niż inna wartość:

$ 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

Dobra wskazówka! Naprawdę podoba mi się twój pogląd, że sprawdzanie a == min(a, b)jest takie samo jak a <= b. Warto zauważyć, że nie sprawdza to dokładnie mniej niż chociaż. Jeśli chcesz to zrobić, musisz sprawdzić a == min(a, b) && a != max(a, b), innymi słowya <= b and not a >= b
Dave

3

Wystarczy użyć ksh( ksh93dokładnie) lub zsh, które oba natywnie obsługują arytmetykę zmiennoprzecinkową:

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

Edycja: Przepraszam, że tęskniłem ksh93już zaproponowano. Zachowując moją odpowiedź, aby wyjaśnić, skrypt zamieszczony w pytaniu otwierającym może być używany bez żadnych zmian poza przełącznikiem powłoki.

Edycja2: Zauważ, że ksh93wymaga , aby zawartość zmiennej była zgodna z twoimi ustawieniami regionalnymi, tj. W przypadku francuskich ustawień regionalnych, zamiast kropki należy użyć przecinka:

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

Bardziej niezawodnym rozwiązaniem jest ustawienie ustawień regionalnych na początku skryptu, aby upewnić się, że będzie on działał niezależnie od ustawień regionalnych użytkownika:

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

Zauważ, że powyższy skrypt ksh93 działa tylko w lokalizacjach, w których znajduje się separator dziesiętny .(więc nie w połowie świata, w którym znajduje się separator dziesiętny ,). zshnie ma tego problemu.
Stéphane Chazelas

Rzeczywiście, odpowiedź została zredagowana w celu wyjaśnienia tej kwestii.
jlliagre

Ustawienie LC_NUMERIC nie będzie działać, jeśli użytkownik ustawił LC_ALL, co oznacza również, że liczby nie będą wyświetlane (lub wprowadzane) w preferowanym przez użytkownika formacie. Potencjalnie lepsze podejście można znaleźć na stronie unix.stackexchange.com/questions/87745/what-does-lc-all-c-do/ ...
Stéphane Chazelas

@ StéphaneChazelas naprawił problem LC_NUMERIC. Biorąc pod uwagę składnię skryptu OP, zakładam, że jego preferowanym separatorem jest i .tak.
jlliagre

Tak, ale ważne są ustawienia regionalne użytkownika skryptu, a nie ustawienia regionalne autora skryptu. Jako autor skryptu powinieneś wziąć pod uwagę lokalizację i jej skutki uboczne.
Stéphane Chazelas

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

To używa dckalkulatora do zerwania swartości $minw rejestrze ai dumieszcza wartość $valna szczycie głównego stosu wykonawczego. Następnie lumieszcza zawartość ana górze stosu, w którym to momencie wygląda następująco:

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

<Wyskakuje szczycie dwa wpisy od stosu i porównuje je. Tak więc stos wygląda następująco:

${val}

Jeśli górna pozycja była mniejsza niż druga od góry, wypycha zawartość ana górę, więc stos wygląda następująco:

${min} ${val}

W przeciwnym razie nic nie robi, a stos nadal wygląda:

${val} 

Następnie po prostu psprawdza, czy wpis najwyższego stosu jest zaznaczony.

Więc dla twojego problemu:

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

###OUTPUT

12.35

Ale:

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

###OUTPUT

12.45

0

Dlaczego nie użyć starego, dobrego expr?

Przykładowa składnia:

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

Dla prawdziwych wyrażeń kod wyjścia expr wynosi 0, a ciąg „1” jest wysyłany do standardowego wyjścia. Odwróć dla fałszywych wyrażeń.

Sprawdziłem to z GNU i FreeBSD 8 expr.


GNU expr obsługuje tylko porównania arytmetyczne na liczbach całkowitych. Twój przykład wykorzystuje porównanie leksykograficzne, które zawiedzie na liczbach ujemnych. Na przykład expr 1.09 '<' -1.1wydrukuje 1i wyjdzie za pomocą 0(sukces).
Adrian Günter

0

Aby sprawdzić, czy dwie (ewentualnie ułamkowe) liczby są w porządku, sortjest (w miarę rozsądnie) przenośne:

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

Jeśli jednak chcesz aktualizować minimalną wartość, nie potrzebujesz if. Sortuj liczby i zawsze używaj pierwszego (najmniejszego):

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

0

Zwykle robię podobne rzeczy z osadzonym kodem python:

#!/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
Czy możesz skomentować swoją odpowiedź i dodać wyjaśnienia
Romeo Ninov
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.