Come formattare il numero in virgola mobile con esattamente 2 cifre significative in bash?


17

Voglio stampare il numero in virgola mobile con esattamente due cifre significative in bash (magari usando uno strumento comune come awk, bc, dc, perl ecc.).

Esempi:

  • 76543 deve essere stampato come 76000
  • 0,0076543 deve essere stampato come 0,0076

In entrambi i casi le cifre significative sono 7 e 6. Ho letto alcune risposte per problemi simili come:

Come arrotondare i numeri in virgola mobile nella shell?

Precisione limitante di bash di variabili in virgola mobile

ma le risposte si concentrano sulla limitazione del numero di cifre decimali (es. bccomando con scale=2o printfcomando con %.2f) anziché cifre significative.

Esiste un modo semplice per formattare il numero con esattamente 2 cifre significative o devo scrivere la mia funzione?

Risposte:


13

Questa risposta alla prima domanda collegata ha la linea quasi da buttare alla fine:

Vedere anche %gper arrotondare a un numero specificato di cifre significative.

Quindi puoi semplicemente scrivere

printf "%.2g" "$n"

(ma vedere la sezione seguente sul separatore decimale e le impostazioni internazionali e notare che non Bash printfnon deve supportare %fe %g).

Esempi:

$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077

Ovviamente, ora hai una rappresentazione con esponente della mantissa anziché un decimale puro, quindi ti consigliamo di riconvertire:

$ printf "%0.f\n" 7.7e+06
7700000

$ printf "%0.7f\n" 7.7e-06
0.0000077

Mettendo tutto questo insieme e avvolgendolo in una funzione:

# Function round(precision, number)
round() {
    n=$(printf "%.${1}g" "$2")
    if [ "$n" != "${n#*e}" ]
    then
        f="${n##*e-}"
        test "$n" = "$f" && f= || f=$(( ${f#0}+$1-1 ))
        printf "%0.${f}f" "$n"
    else
        printf "%s" "$n"
    fi
}

(Nota: questa funzione è scritta nella shell portatile (POSIX), ma presuppone che printfgestisca le conversioni in virgola mobile. Bash ha un built-in printfche lo fa, quindi stai bene qui, e anche l'implementazione GNU funziona, quindi la maggior parte di GNU / I sistemi Linux possono usare tranquillamente Dash).

Casi test

radix=$(printf %.1f 0)
for i in $(seq 12 | sed -e 's/.*/dc -e "12k 1.234 10 & 6 -^*p"/e' -e "y/_._/$radix/")
do
    echo $i "->" $(round 2 $i)
done

Risultati del test

.000012340000 -> 0.000012
.000123400000 -> 0.00012
.001234000000 -> 0.0012
.012340000000 -> 0.012
.123400000000 -> 0.12
1.234 -> 1.2
12.340 -> 12
123.400 -> 120
1234.000 -> 1200
12340.000 -> 12000
123400.000 -> 120000
1234000.000 -> 1200000

Una nota sul separatore decimale e le impostazioni internazionali

Tutto il lavoro sopra presuppone che il carattere radix (noto anche come separatore decimale) sia ., come nella maggior parte dei locali inglesi. ,Invece usano altre versioni locali e alcune shell hanno un built-in printfche rispetta le impostazioni locali. In queste shell, potrebbe essere necessario impostare LC_NUMERIC=Cper forzare l'uso di .come carattere radix o scrivere /usr/bin/printfper impedire l'uso della versione integrata. Quest'ultimo è complicato dal fatto che (almeno alcune versioni) sembrano sempre analizzare gli argomenti usando ., ma stampare usando le impostazioni locali correnti.


@ Stéphane Chazelas, perché hai cambiato il mio shebang POSIX accuratamente testato in Bash dopo aver rimosso il bashismo? Il tuo commento menziona %f/ %g, ma questo è l' printfargomento, e non è necessario un POSIX printfper avere una shell POSIX. Penso che avresti dovuto commentare invece di modificarlo lì.
Toby Speight,

printf %gnon può essere utilizzato in uno script POSIX. È vero printf, dipende dall'utilità, ma quell'utilità è integrata nella maggior parte delle shell. L'OP è stato etichettato come bash, quindi usare un bash shebang è un modo semplice per ottenere un printf che supporti% g. Altrimenti, dovresti aggiungere un presupposto che il tuo printf (o il printf incorporato del tuo shif printfsia incorporato lì) supporti lo non standard (ma abbastanza comune) %g...
Stéphane Chazelas

dash's ha un builtin printf(che supporta %g). Sui sistemi GNU, mkshè probabilmente l'unica shell in questi giorni che non avrà un builtin printf.
Stéphane Chazelas,

Grazie per i tuoi miglioramenti - ho modificato solo per rimuovere lo shebang (poiché la domanda è taggata bash) e relegare parte di questo alle note - ora sembra corretto?
Toby Speight,

1
Purtroppo questo non stampa il numero corretto di cifre se le cifre finali sono zeri. Ad esempio printf "%.3g\n" 0.400dà 0,4 non 0,400
phiresky

4

TL; DR

Basta copiare e utilizzare la funzione sigfnella sezione A reasonably good "significant numbers" function:. È scritto (come tutto il codice in questa risposta) per funzionare con trattino .

Darà l' printfapprossimazione alla parte intera di N con $sigcifre.

Informazioni sul separatore decimale.

Il primo problema da risolvere con printf è l'effetto e l'uso del "segno decimale", che negli Stati Uniti è un punto e in DE è una virgola (ad esempio). È un problema perché ciò che funziona per alcune impostazioni locali (o shell) fallirà con altre impostazioni locali. Esempio:

$ dash -c 'printf "%2.3f\n" 12.3045'
12.305
$  ksh -c 'printf "%2.3f\n" 12.3045'
ksh: printf: 12.3045: arithmetic syntax error
ksh: printf: 12.3045: arithmetic syntax error
ksh: printf: warning: invalid argument of type f
12,000
$ ksh -c 'printf "%2.2f\n" 12,3045'
12,304

Una soluzione comune (e non corretta) è l'impostazione LC_ALL=Cper il comando printf. Ma questo imposta il segno decimale su un punto decimale fisso. Per i locali in cui una virgola (o altro) è il carattere comunemente usato che costituisce un problema.

La soluzione è scoprire all'interno dello script per la shell che lo esegue qual è il separatore decimale locale. Questo è abbastanza semplice:

$ printf '%1.1f' 0
0,0                            # for a comma locale (or shell).

Rimozione di zeri:

$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
,                              # for a comma locale (or shell).

Tale valore viene utilizzato per modificare il file con l'elenco dei test:

sed -i 's/[,.]/'"$dec"'/g' infile

Ciò rende automaticamente valide le esecuzioni su qualsiasi shell o locale.


Alcune basi.

Dovrebbe essere intuitivo tagliare il numero da formattare con il formato %.*eo anche %.*gcon printf. La differenza principale tra l'utilizzo di %.*eo %.*gè il modo in cui contano le cifre. Uno usa il conteggio completo, l'altro ha bisogno del conteggio meno 1:

$ printf '%.*e  %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00  1,235

Funzionava bene per 4 cifre significative.

Dopo che il numero di cifre è stato tagliato dal numero, abbiamo bisogno di un passaggio aggiuntivo per formattare i numeri con esponenti diversi da 0 (come era sopra).

$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235

Questo funziona correttamente. Il conteggio della parte intera (a sinistra del segno decimale) è solo il valore dell'esponente ($ exp). Il conteggio dei decimali necessari è il numero di cifre significative ($ sig) meno la quantità di cifre già utilizzate nella parte sinistra del separatore decimale:

a=$((exp<0?0:exp))                      ### count of integer characters.
b=$((exp<sig?sig-exp:0))                ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"

Poiché la parte integrale del fformato non ha limiti, in realtà non è necessario dichiararlo esplicitamente e questo codice (più semplice) funziona:

a=$((exp<sig?sig-exp:0))                ### count of decimal characters.
printf '%0.*f' "$a" "$N"

Prima prova.

Una prima funzione che potrebbe farlo in un modo più automatizzato:

# Function significant (number, precision)
sig1(){
    sig=$(($2>0?$2:1))                      ### significant digits (>0)
    N=$(printf "%0.*e" "$(($sig-1))" "$1")  ### N in sci (cut to $sig digits).
    exp=$(echo "${N##*[eE+]}+1"|bc)         ### get the exponent.
    a="$((exp<sig?sig-exp:0))"              ### calc number of decimals.
    printf "%0.*f" "$a" "$N"                ### re-format number.
}

Questo primo tentativo funziona con molti numeri ma fallirà con numeri per i quali la quantità di cifre disponibili è inferiore al conteggio significativo richiesto e l'esponente è inferiore a -4:

   Number       sig                       Result        Correct?
   123456789 --> 4<                       123500000 >--| yes
       23455 --> 4<                           23460 >--| yes
       23465 --> 4<                           23460 >--| yes
      1,2e-5 --> 6<                    0,0000120000 >--| no
     1,2e-15 -->15< 0,00000000000000120000000000000 >--| no
          12 --> 6<                         12,0000 >--| no  

Aggiungerà molti zeri che non sono necessari.

Seconda prova.

Per risolverlo dobbiamo pulire N dell'esponente e tutti gli zeri finali. Quindi possiamo ottenere la lunghezza effettiva delle cifre disponibili e lavorare con quella:

# Function significant (number, precision)
sig2(){ local sig N exp n len a
    sig=$(($2>0?$2:1))                      ### significant digits (>0)
    N=$(printf "%+0.*e" "$(($sig-1))" "$1") ### N in sci (cut to $sig digits).
    exp=$(echo "${N##*[eE+]}+1"|bc)         ### get the exponent.
    n=${N%%[Ee]*}                           ### remove sign (first character).
    n=${n%"${n##*[!0]}"}                    ### remove all trailing zeros
    len=$(( ${#n}-2 ))                      ### len of N (less sign and dec).
    len=$((len<sig?len:sig))                ### select the minimum.
    a="$((exp<len?len-exp:0))"              ### use $len to count decimals.
    printf "%0.*f" "$a" "$N"                ### re-format the number.
}

Tuttavia, questo utilizza la matematica in virgola mobile e "nulla è semplice in virgola mobile": perché i miei numeri non si sommano?

Ma nulla in "virgola mobile" è semplice.

printf "%.2g  " 76500,00001 76500
7,7e+04  7,6e+04

Tuttavia:

 printf "%.2g  " 75500,00001 75500
 7,6e+04  7,6e+04

Perché?:

printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34

Inoltre, il comando printfè incorporato in molte shell.
Quali printfstampe possono cambiare con la shell:

$ dash -c 'printf "%.*f" 4 123456e+25'
1234560000000000020450486779904.0000
$  ksh -c 'printf "%.*f" 4 123456e+25'
1234559999999999999886313162278,3840

$  dash ./script.sh
   123456789 --> 4<                       123500000 >--| yes
       23455 --> 4<                           23460 >--| yes
       23465 --> 4<                           23460 >--| yes
      1.2e-5 --> 6<                        0.000012 >--| yes
     1.2e-15 -->15<              0.0000000000000012 >--| yes
          12 --> 6<                              12 >--| yes
  123456e+25 --> 4< 1234999999999999958410892148736 >--| no

Una funzione "numeri significativi" ragionevolmente buona:

dec=$(IFS=0; printf '%s' $(printf '%.1f'))   ### What is the decimal separator?.
sed -i 's/[,.]/'"$dec"'/g' infile

zeros(){ # create an string of $1 zeros (for $1 positive or zero).
         printf '%.*d' $(( $1>0?$1:0 )) 0
       }

# Function significant (number, precision)
sigf(){ local sig sci exp N sgn len z1 z2 b c
    sig=$(($2>0?$2:1))                      ### significant digits (>0)
    N=$(printf '%+e\n' $1)                  ### use scientific format.
    exp=$(echo "${N##*[eE+]}+1"|bc)         ### find ceiling{log(N)}.
    N=${N%%[eE]*}                           ### cut after `e` or `E`.
    sgn=${N%%"${N#-}"}                      ### keep the sign (if any).
    N=${N#[+-]}                             ### remove the sign
    N=${N%[!0-9]*}${N#??}                   ### remove the $dec
    N=${N#"${N%%[!0]*}"}                    ### remove all leading zeros
    N=${N%"${N##*[!0]}"}                    ### remove all trailing zeros
    len=$((${#N}<sig?${#N}:sig))            ### count of selected characters.
    N=$(printf '%0.*s' "$len" "$N")         ### use the first $len characters.

    result="$N"

    # add the decimal separator or lead zeros or trail zeros.
    if   [ "$exp" -gt 0 ] && [ "$exp" -lt "$len" ]; then
            b=$(printf '%0.*s' "$exp" "$result")
            c=${result#"$b"}
            result="$b$dec$c"
    elif [ "$exp" -le 0 ]; then
            # fill front with leading zeros ($exp length).
            z1="$(zeros "$((-exp))")"
            result="0$dec$z1$result"
    elif [ "$exp" -ge "$len" ]; then
            # fill back with trailing zeros.
            z2=$(zeros "$((exp-len))")
            result="$result$z2"
    fi
    # place the sign back.
    printf '%s' "$sgn$result"
}

E i risultati sono:

$ dash ./script.sh
       123456789 --> 4<                       123400000 >--| yes
           23455 --> 4<                           23450 >--| yes
           23465 --> 4<                           23460 >--| yes
          1.2e-5 --> 6<                        0.000012 >--| yes
         1.2e-15 -->15<              0.0000000000000012 >--| yes
              12 --> 6<                              12 >--| yes
      123456e+25 --> 4< 1234000000000000000000000000000 >--| yes
      123456e-25 --> 4<       0.00000000000000000001234 >--| yes
 -12345.61234e-3 --> 4<                          -12.34 >--| yes
 -1.234561234e-3 --> 4<                       -0.001234 >--| yes
           76543 --> 2<                           76000 >--| yes
          -76543 --> 2<                          -76000 >--| yes
          123456 --> 4<                          123400 >--| yes
           12345 --> 4<                           12340 >--| yes
            1234 --> 4<                            1234 >--| yes
           123.4 --> 4<                           123.4 >--| yes
       12.345678 --> 4<                           12.34 >--| yes
      1.23456789 --> 4<                           1.234 >--| yes
    0.1234555646 --> 4<                          0.1234 >--| yes
       0.0076543 --> 2<                          0.0076 >--| yes
   .000000123400 --> 2<                      0.00000012 >--| yes
   .000001234000 --> 2<                       0.0000012 >--| yes
   .000012340000 --> 2<                        0.000012 >--| yes
   .000123400000 --> 2<                         0.00012 >--| yes
   .001234000000 --> 2<                          0.0012 >--| yes
   .012340000000 --> 2<                           0.012 >--| yes
   .123400000000 --> 2<                            0.12 >--| yes
           1.234 --> 2<                             1.2 >--| yes
          12.340 --> 2<                              12 >--| yes
         123.400 --> 2<                             120 >--| yes
        1234.000 --> 2<                            1200 >--| yes
       12340.000 --> 2<                           12000 >--| yes
      123400.000 --> 2<                          120000 >--| yes

0

Se hai già il numero come stringa, ovvero come "3456" o "0.003756", potresti potenzialmente farlo solo usando la manipolazione di stringhe. Quanto segue è al di sopra della mia testa e non è stato accuratamente testato e usa sed, ma considera:

f() {
    local A="$1"
    local B="$(echo "$A" | sed -E "s/^-?0?\.?0*//")"
    local C="$(eval echo "${A%$B}")"
    if ((${#B} > 2)); then
        D="${B:0:2}"
    else
        D="$B"
    fi
    echo "$C$D"
}

Laddove fondamentalmente si rimuove e si salva qualsiasi elemento "-0.000" all'inizio, quindi si utilizza una semplice operazione di sottostringa sul resto. Un avvertimento su quanto sopra è che non vengono rimossi più 0 iniziali. Lo lascerò come esercizio.


1
Più che un esercizio: non riempie l'intero di zero, né tiene conto del punto decimale incorporato. Ma sì, è fattibile usando questo approccio (anche se il raggiungimento di ciò potrebbe essere al di là delle competenze di OP).
Thomas Dickey,
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.