Vergleich mit Gleitkommazahlen in einem Shell-Skript


22

Ich möchte zwei Gleitkommazahlen in einem Shell-Skript vergleichen. Der folgende Code funktioniert nicht:

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

Antworten:


5

Sie können den ganzzahligen und den gebrochenen Teil getrennt prüfen:

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

Wie in den Kommentaren geäußert, funktioniert es nur, wenn beide Zahlen Bruchteile und beide Bruchteile die gleiche Anzahl von Ziffern haben. Hier ist eine Version, die für Integer- oder Fractional-Operatoren und alle Bash-Operatoren funktioniert:

#!/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
Dies kann nicht ohne viel Arbeit behoben werden (versuchen Sie zu vergleichen 0.5und 0.06). Sie sollten ein Tool verwenden, das die Dezimalschreibweise bereits versteht.
Gilles 'SO - hör auf, böse zu sein'

Vielen Dank, Gilles, aktualisiert es allgemeiner als die frühere Version.
ata

Beachten Sie, dass es heißt, dass 1.00000000000000000000000001größer ist als 2.
Stéphane Chazelas

Stéphane hat recht. Das liegt an den Bit-Grenzen in der Zahlendarstellung von Bash. Natürlich wollen , wenn Sie mehr Leiden Sie Ihre eigene Darstellung verwenden könnte .... :)
ata

35

Bash versteht keine Gleitkomma-Arithmetik. Zahlen, die ein Dezimalzeichen enthalten, werden als Zeichenfolgen behandelt.

Verwenden Sie stattdessen awk oder bc.

#!/bin/bash

min=12.45
val=10.35

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

echo "$min"

Wenn Sie vorhaben, viele mathematische Operationen durchzuführen, ist es wahrscheinlich besser, sich auf Python oder Perl zu verlassen.


12

Sie können package num-utils für einfache Manipulationen verwenden ...

Weitere Informationen zu Mathematik finden Sie unter diesem Link. Hier werden verschiedene Optionen beschrieben, z.

  • R / Rscript (GNU R statistisches Berechnungs- und Grafiksystem)
  • Oktave (meistens Matlab kompatibel)
  • bc (Die GNU bc Arbitrary Precision Calculator Sprache)

Ein Beispiel für 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.

Hier ist ein bashHack ... Er fügt der Ganzzahl führende Nullen hinzu, um einen String-Vergleich von links nach rechts aussagekräftig zu machen. Für diesen speziellen Code ist es erforderlich, dass sowohl min als auch val tatsächlich einen Dezimalpunkt und mindestens eine Dezimalstelle haben.

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

Ausgabe:

min=10.35

10

Für einfache Berechnungen von Gleitkommazahlen (+ - * / und Vergleiche) können Sie awk verwenden.

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

Wenn Sie ksh93 oder zsh (nicht bash) haben, können Sie die integrierte Arithmetik Ihrer Shell verwenden, die Gleitkommazahlen unterstützt.

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

Weitere Informationen zu Gleitkommaberechnungen finden Sie unter bc . Es funktioniert tatsächlich mit Fixpunktzahlen mit beliebiger Genauigkeit.

Wenn Sie mit Zahlentabellen arbeiten möchten, schlagen Sie R nach ( Beispiel ).


6

Verwenden Sie die numerische Sortierung

Der Befehl sorthat eine Option -g( --general-numeric-sort), die für Vergleiche mit <"kleiner als" oder "kleiner als" verwendet werden kann> "größer als" verwendet werden kann, indem das Minimum oder Maximum ermittelt wird.

Diese Beispiele finden das Minimum:

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

Unterstützt E-Notation

Es funktioniert mit einer ziemlich allgemeinen Notation von Gleitkommazahlen, wie bei der E-Notation

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

Beachten Sie E-10, dass die erste Zahl 0.000000001245tatsächlich kleiner ist als10.35 .

Kann mit unendlich vergleichen

Der Gleitkomma-Standard IEEE754 definiert einige spezielle Werte. Für diese Vergleiche sind die interessantesten INFfür die Unendlichkeit. Es gibt auch die negative Unendlichkeit; Beides sind im Standard genau definierte Werte.

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

Um die maximale Verwendung finden sort -grstatt sort -g, die Sortierreihenfolge umzukehren:

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

Vergleichsoperation

Um den <Vergleich ("kleiner als") zu implementieren , damit er in ifetc verwendet werden kann, vergleichen Sie das Minimum mit einem der Werte. Wenn das Minimum dem als Text verglichenen Wert entspricht, ist es kleiner als der andere Wert:

$ 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

Guter Tipp! Ich mag deine Einsicht wirklich, dass das Überprüfen a == min(a, b)das Gleiche ist wie a <= b. Es ist erwähnenswert, dass dies nicht unbedingt weniger prüft als wenn. Wenn Sie das tun möchten, müssen Sie a == min(a, b) && a != max(a, b)in anderen a <= b and not a >= b
Dave

3

Verwenden Sie einfach ksh( ksh93genau) oder zsh, die beide Fließkomma-Arithmetiken unterstützen:

$ 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: Sorry, ich habe vermisst, ksh93wurde bereits vorgeschlagen. Wenn ich meine Antwort behalte, um das in der Eröffnungsfrage veröffentlichte Skript zu verdeutlichen, kann es ohne Änderung außerhalb des Shell-Switches verwendet werden.

Edit2: Beachten Sie, ksh93dass der Inhalt der Variablen mit Ihrem Gebietsschema übereinstimmen muss, dh bei einem französischen Gebietsschema muss ein Komma anstelle eines Punkts verwendet werden:

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

Eine robustere Lösung besteht darin, das Gebietsschema am Anfang des Skripts festzulegen, um sicherzustellen, dass es unabhängig vom Gebietsschema des Benutzers funktioniert:

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

Beachten Sie, dass das obige Skript ksh93 nur in Gebieten funktioniert, in denen sich das Dezimaltrennzeichen befindet .(also nicht in der halben Welt, in der sich das Dezimaltrennzeichen befindet ,). zshhat dieses Problem nicht.
Stéphane Chazelas

Antworten Sie in der Tat bearbeitet, um diesen Punkt zu verdeutlichen.
jlliagre

Die Einstellung von LC_NUMERIC funktioniert nicht, wenn der Benutzer die Einstellung vorgenommen hat. Dies LC_ALLbedeutet auch, dass Zahlen nicht im vom Benutzer bevorzugten Format angezeigt (oder eingegeben) werden. Eine möglicherweise bessere Vorgehensweise finden Sie unter unix.stackexchange.com/questions/87745/what-does-lc-all-c-do/… .
Stéphane Chazelas

@ StéphaneChazelas hat das LC_NUMERIC-Problem behoben. Angesichts der OP-Skriptsyntax gehe ich davon aus, dass sein bevorzugtes Trennzeichen .sowieso ist.
Uhr

Ja, aber es ist das Gebietsschema des Skriptbenutzers, nicht das Gebietsschema des Skriptautors, auf das es ankommt. Als Drehbuchautor sollten Sie die Lokalisierung und deren Nebenwirkungen berücksichtigen.
Stéphane Chazelas

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

Das benutzt den dcRechner, um sden Wert für $minim Register zu speichern aund dkopiert den Wert von $valoben auf seinen Hauptausführungsstapel. Dann wird lder Inhalt von aoben auf den Stapel gelegt und sieht dann so aus:

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

Die <beiden obersten Einträge werden vom Stapel entfernt und verglichen. Der Stack sieht dann so aus:

${val}

Wenn der oberste Eintrag kleiner als der zweithöchste war, wird der Inhalt von nach oben geschoben a, sodass der Stapel wie folgt aussieht:

${min} ${val}

Sonst macht es nichts und der Stack sieht immer noch so aus:

${val} 

Dann wird nur pder oberste Stapeleintrag gedruckt.

Also für dein Problem:

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

###OUTPUT

12.35

Aber:

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

###OUTPUT

12.45

0

Warum nicht alt, gut expr?

Beispielsyntax:

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

Bei wahren Ausdrücken ist der Exit-Code expr 0, und die Zeichenfolge '1' wird an stdout gesendet. Reverse für falsche Ausdrücke.

Ich habe dies mit GNU und FreeBSD 8 expr überprüft.


GNU expr unterstützt nur den arithmetischen Vergleich von ganzen Zahlen. In Ihrem Beispiel wird ein lexikografischer Vergleich verwendet, der bei negativen Zahlen fehlschlägt. Zum Beispiel expr 1.09 '<' -1.1wird gedruckt 1und beendet mit 0(Erfolg).
Adrian Günter

0

Um zu überprüfen, ob zwei (möglicherweise gebrochene) Zahlen in Ordnung sind, sortist (vernünftigerweise) Folgendes übertragbar:

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

Wenn Sie jedoch tatsächlich einen Mindestwert aktualisieren möchten, benötigen Sie keinen if. Sortieren Sie die Zahlen und verwenden Sie immer die erste (kleinste):

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

0

Normalerweise mache ich ähnliche Dinge mit eingebettetem Python-Code:

#!/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
Können Sie bitte Ihre Antwort kommentieren und einige Erklärungen hinzufügen
Romeo Ninov
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.