TL; DR
Einfach kopieren und die Funktion sigf
im Abschnitt verwenden A reasonably good "significant numbers" function:
. Es ist geschrieben (wie der gesamte Code in dieser Antwort), um mit Bindestrich zu arbeiten .
Es wird die printf
Annäherung an den ganzzahligen Teil von N mit $sig
Ziffern geben.
Über das Dezimaltrennzeichen.
Das erste Problem, das mit printf gelöst werden muss, ist der Effekt und die Verwendung des "Dezimalzeichens", das in den USA ein Punkt und in DE ein Komma ist (zum Beispiel). Dies ist ein Problem, da das, was für ein Gebietsschema (oder eine Shell) funktioniert, bei einem anderen Gebietsschema fehlschlägt. Beispiel:
$ 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
Eine häufige (und falsche) Lösung besteht darin, LC_ALL=C
den Befehl printf festzulegen. Dadurch wird die Dezimalstelle jedoch auf einen festen Dezimalpunkt gesetzt. Für Gebietsschemata, bei denen ein Komma (oder ein anderes) das häufig verwendete Zeichen ist, das ein Problem darstellt.
Die Lösung besteht darin, im Skript herauszufinden, in welcher Shell das Dezimaltrennzeichen für das Gebietsschema ausgeführt wird. Das ist ganz einfach:
$ printf '%1.1f' 0
0,0 # for a comma locale (or shell).
Nullen entfernen:
$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
, # for a comma locale (or shell).
Dieser Wert wird verwendet, um die Datei mit der Liste der Tests zu ändern:
sed -i 's/[,.]/'"$dec"'/g' infile
Das macht die Läufe auf jeder Shell oder jedem Gebietsschema automatisch gültig.
Einige Grundlagen.
Es sollte intuitiv sein, die zu formatierende Zahl mit dem Format %.*e
oder sogar %.*g
printf auszuschneiden. Der Hauptunterschied zwischen der Verwendung von %.*e
oder %.*g
besteht darin, wie die Ziffern gezählt werden. Einer verwendet die volle Zählung, der andere benötigt die Zählung minus 1:
$ printf '%.*e %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00 1,235
Das funktionierte gut für 4 signifikante Stellen.
Nachdem die Anzahl der Stellen aus der Zahl herausgeschnitten wurde, müssen wir einen zusätzlichen Schritt ausführen, um Zahlen mit Exponenten ungleich 0 (wie oben) zu formatieren.
$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235
Das funktioniert einwandfrei. Die Zählung des ganzzahligen Teils (links von der Dezimalstelle) entspricht nur dem Wert des Exponenten ($ exp). Die Anzahl der benötigten Dezimalstellen ist die Anzahl der signifikanten Stellen ($ sig) abzüglich der Anzahl der Stellen, die bereits im linken Teil des Dezimaltrennzeichens verwendet wurden:
a=$((exp<0?0:exp)) ### count of integer characters.
b=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"
Da der integrale Teil des f
Formats keine Begrenzung hat, muss er nicht explizit deklariert werden, und dieser (einfachere) Code funktioniert:
a=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%0.*f' "$a" "$N"
Erster Versuch.
Eine erste Funktion, die dies automatisiert ausführen könnte:
# 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.
}
Dieser erste Versuch funktioniert mit vielen Zahlen, schlägt jedoch mit Zahlen fehl, für die die Anzahl der verfügbaren Stellen geringer als die angeforderte signifikante Anzahl und der Exponent kleiner als -4 ist:
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
Es werden viele Nullen hinzugefügt, die nicht benötigt werden.
Zweiter Versuch.
Um dies zu lösen, müssen wir N des Exponenten und alle nachfolgenden Nullen entfernen. Dann können wir die effektive Länge der verfügbaren Ziffern ermitteln und damit arbeiten:
# 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.
}
In diesem Fall wird jedoch Gleitkomma-Mathematik verwendet, und "im Gleitkomma ist nichts einfach": Warum addieren sich meine Zahlen nicht?
Aber nichts in "Fließkomma" ist einfach.
printf "%.2g " 76500,00001 76500
7,7e+04 7,6e+04
Jedoch:
printf "%.2g " 75500,00001 75500
7,6e+04 7,6e+04
Warum?:
printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34
Außerdem besteht der Befehl printf
aus vielen Muscheln.
Welche printf
Ausdrucke können sich mit der Shell ändern:
$ 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
Eine einigermaßen gute "signifikante Zahl" -Funktion:
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"
}
Und die Ergebnisse sind:
$ 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
%f
/%g
, aber das ist dasprintf
Argument, und man braucht kein POSIXprintf
, um eine POSIX-Shell zu haben. Ich denke, du hättest dort kommentieren statt editieren sollen.