Come confrontare con il numero in virgola mobile in uno script di shell


22

Voglio confrontare due numeri in virgola mobile in uno script di shell. Il seguente codice non funziona:

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

Risposte:


5

È possibile controllare separatamente le parti intere e frazionarie:

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

Come dice fered nei commenti, funziona solo se entrambi i numeri hanno parti frazionarie ed entrambe le parti frazionarie hanno lo stesso numero di cifre. Ecco una versione che funziona per intero o frazionario e qualsiasi operatore 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
Questo non può essere risolto senza molto lavoro (prova a confrontare 0.5e 0.06). Faresti meglio a usare uno strumento che già capisce la notazione decimale.
Gilles 'SO- smetti di essere malvagio' il

Grazie Gilles, l'ho aggiornato per funzionare più in generale rispetto alla versione precedente.
ata

Si noti che dice che 1.00000000000000000000000001è maggiore di 2.
Stéphane Chazelas,

Stéphane ha ragione. È così a causa dei limiti di bit nella rappresentazione numerica di bash. Naturalmente, se vuoi più sofferenza, puoi usare la tua rappresentazione .... :)
ata

35

Bash non capisce l'aritmetica in virgola mobile. Tratta i numeri contenenti un punto decimale come stringhe.

Usa invece awk o bc.

#!/bin/bash

min=12.45
val=10.35

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

echo "$min"

Se hai intenzione di fare molte operazioni matematiche, probabilmente è meglio fare affidamento su Python o Perl.


12

Puoi usare il pacchetto num-utils per semplici manipolazioni ...

Per la matematica più seria, vedi questo link ... Descrive diverse opzioni, ad es.

  • R / Rscript (calcolo statistico GNU R e sistema grafico)
  • ottava (principalmente compatibile con Matlab)
  • bc (il linguaggio della calcolatrice di precisione arbitraria GNU bc)

Un esempio di 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.

Ecco un bashtrucco ... Aggiunge gli 0 iniziali all'intero per rendere significativa una comparazione da sinistra a destra della stringa. Questo particolare pezzo di codice richiede che sia min che val abbiano effettivamente un punto decimale e almeno una cifra decimale.

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

produzione:

min=10.35

10

Per semplici calcoli su numeri in virgola mobile (+ - * / e confronti), è possibile utilizzare awk.

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

Oppure, se hai ksh93 o zsh (non bash), puoi usare l'aritmetica integrata della shell, che supporta i numeri in virgola mobile.

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

Per calcoli in virgola mobile più avanzati, cercare bc . In realtà funziona su numeri di punti fissi di precisione arbitraria.

Per lavorare su tabelle di numeri, cerca R ( esempio ).


6

Usa ordinamento numerico

Il comando sortha un'opzione -g( --general-numeric-sort) che può essere utilizzata per i confronti su <"minore di" o >"maggiore di", trovando il minimo o il massimo.

Questi esempi stanno trovando il minimo:

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

Supporta notazione elettronica

Funziona con una notazione piuttosto generale di numeri in virgola mobile, come con la notazione elettronica

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

Nota il E-10, rendendo il primo numero 0.000000001245, anzi inferiore a 10.35.

Può paragonare all'infinito

Lo standard a virgola mobile, IEEE754 , definisce alcuni valori speciali. Per questi confronti, quelli interessanti sono INFper l'infinito. C'è anche l'infinito negativo; Entrambi sono valori ben definiti nello standard.

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

Per trovare il massimo utilizzo sort -granziché sort -ginvertire l'ordinamento:

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

Operazione di confronto

Per implementare il confronto <("minore di"), in modo che possa essere utilizzato in ifecc., Confrontare il minimo con uno dei valori. Se il minimo è uguale al valore, rispetto al testo , è inferiore all'altro valore:

$ 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

Bel consiglio! Mi piace molto la tua intuizione che il controllo per a == min(a, b)è lo stesso di a <= b. Vale la pena notare che questo non controlla rigorosamente meno di quanto però. Se vuoi farlo, devi controllare a == min(a, b) && a != max(a, b), in altre parolea <= b and not a >= b
Dave,

3

Basta usare ksh( ksh93precisamente) o zsh, che entrambi supportano nativamente l'aritmetica in virgola mobile:

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

Modifica: mi dispiace, ho perso ksh93era già stato suggerito. Mantenere la mia risposta solo per chiarire che lo script pubblicato nella domanda iniziale può essere utilizzato senza modifiche al di fuori dell'interruttore della shell.

Modifica2: Nota che ksh93richiede che il contenuto della variabile sia coerente con la tua locale, ovvero con una locale francese, è necessario utilizzare una virgola anziché un punto:

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

Una soluzione più efficace consiste nell'impostare la locale all'inizio dello script per assicurarsi che funzionerà indipendentemente dalla locale dell'utente:

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

Nota che lo script ksh93 sopra funziona solo in locali in cui si trova il separatore decimale .(quindi non in mezzo mondo in cui si trova il separatore decimale ,). zshnon ha questo problema.
Stéphane Chazelas,

In effetti, risposta modificata per chiarire questo punto.
jlliagre,

L'impostazione di LC_NUMERIC non funzionerà se l'utente ha impostato LC_ALL, ciò significa anche che i numeri non verranno visualizzati (o inseriti) nel formato preferito dell'utente. Vedi unix.stackexchange.com/questions/87745/what-does-lc-all-c-do/… per un approccio potenzialmente migliore.
Stéphane Chazelas,

@ StéphaneChazelas risolto il problema LC_NUMERIC. Data la sintassi dello script OP, presumo che il suo separatore preferito sia .comunque.
jlliagre,

Sì, ma sono le impostazioni internazionali dell'utente dello script, non le impostazioni internazionali dell'autore dello script che contano. Come autore di script, dovresti tenere conto della localizzazione e dei suoi effetti collaterali.
Stéphane Chazelas,

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

Quello usa la dccalcolatrice per sstrappare il valore in $minin register ae lo daumenta nella $valparte superiore del suo stack di esecuzione principale. lElenca quindi il contenuto della aparte superiore della pila, a quel punto sembra:

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

Il <pop espone le prime due voci dallo stack e le confronta. Quindi lo stack appare quindi:

${val}

Se la voce in alto era inferiore alla seconda in cima, spinge il contenuto ain cima, quindi lo stack appare come:

${min} ${val}

Altrimenti non fa nulla e lo stack sembra ancora:

${val} 

Quindi pstrappa semplicemente la voce dello stack superiore.

Quindi per il tuo problema:

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

###OUTPUT

12.35

Ma:

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

###OUTPUT

12.45

0

Perché non usare il vecchio, bene expr?

Sintassi di esempio:

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

Per le espressioni vere , il codice di uscita expr è 0, con la stringa '1' inviata a stdout. Invertire per false espressioni.

Ho controllato questo con GNU e FreeBSD 8 expr.


GNU expr supporta solo il confronto aritmetico sugli interi. Il tuo esempio usa il confronto lessicografico che fallirà sui numeri negativi. Ad esempio, expr 1.09 '<' -1.1stamperà 1e uscirà con 0(successo).
Adrian Günter,

0

Per verificare se due numeri (possibilmente frazionari) sono in ordine, sort è (ragionevolmente) portatile:

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

Tuttavia, se si desidera effettivamente aggiornare un valore minimo, non è necessario un if. Ordina i numeri e usa sempre il primo (minimo):

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

0

Di solito faccio cose simili con il codice Python incorporato:

#!/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
Puoi per favore commentare la tua risposta e aggiungere alcune spiegazioni
Romeo Ninov,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.