Je veux imprimer le nombre à virgule flottante avec exactement deux chiffres significatifs dans bash (peut-être en utilisant un outil commun comme awk, bc, dc, Perl etc.).
Exemples:
Dans les deux cas, les chiffres significatifs sont 7 et 6. J'ai lu quelques réponses pour des problèmes similaires comme:
Comment arrondir des nombres à virgule flottante dans le shell?
Bash limitant la précision des variables à virgule flottante
mais les réponses se concentrent sur la limitation du nombre de décimales (par exemple. bc
commande avec scale=2
ou printf
commande avec %.2f
) au lieu de chiffres significatifs.
Existe-t-il un moyen simple de formater le nombre avec exactement 2 chiffres significatifs ou dois-je écrire ma propre fonction?
Cette réponse à la première question liée a la ligne presque jetable à la fin:
Voir également
%g
pour arrondir à un nombre spécifié de chiffres significatifs.
Vous pouvez donc simplement écrire
printf "%.2g" "$n"
(mais voir la section ci-dessous sur le séparateur décimal et les paramètres régionaux, et notez que les non-Bash printf
n'ont pas besoin de supporter %f
et %g
).
Exemples:
$ printf "%.2g\n" 76543 0.0076543
7.7e+04
0.0077
Bien sûr, vous avez maintenant une représentation des exposants de mantisse plutôt que la décimale pure, vous voudrez donc reconvertir:
$ printf "%0.f\n" 7.7e+06
7700000
$ printf "%0.7f\n" 7.7e-06
0.0000077
Mettre tout cela ensemble et l'envelopper dans une fonction:
# 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
}
(Remarque - cette fonction est écrite dans un shell portable (POSIX), mais suppose que printf
gère les conversions en virgule flottante. Bash a un printf
intégré qui le fait, donc ça va ici, et l'implémentation GNU fonctionne également, donc la plupart des systèmes GNU/Linux peuvent utiliser Dash en toute sécurité).
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
.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
Tout ce qui précède suppose que le caractère radical (également connu sous le nom de séparateur décimal) est .
, comme dans la plupart des environnements linguistiques anglais. D'autres paramètres régionaux utilisent ,
à la place, et certains shells ont un printf
intégré qui respecte les paramètres régionaux. Dans ces shells, vous devrez peut-être définir LC_NUMERIC=C
pour forcer l'utilisation de .
comme caractère radical, ou écrivez /usr/bin/printf
pour empêcher l'utilisation de la version intégrée. Cette dernière est compliquée par le fait que (au moins certaines versions) semblent toujours analyser les arguments en utilisant .
, mais imprimez en utilisant les paramètres régionaux actuels.
Copiez et utilisez simplement la fonction sigf
dans la section A reasonably good "significant numbers" function:
. Il est écrit (comme tout le code dans cette réponse) pour fonctionner avec tiret .
Cela donnera l'approximation printf
à partie entière de N avec $sig
chiffres.
Le premier problème à résoudre avec printf est l'effet et l'utilisation de la "marque décimale", qui aux États-Unis est un point, et en DE est une virgule (par exemple). C'est un problème car ce qui fonctionne pour certains paramètres régionaux (ou Shell) échouera avec certains autres paramètres régionaux. Exemple:
$ 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
Une solution courante (et incorrecte) consiste à définir LC_ALL=C
pour la commande printf. Mais cela définit la marque décimale à un point décimal fixe. Pour les paramètres régionaux où une virgule (ou autre) est le caractère couramment utilisé qui pose problème.
La solution consiste à découvrir à l'intérieur du script du shell qui l'exécute quel est le séparateur décimal local. C'est assez simple:
$ printf '%1.1f' 0
0,0 # for a comma locale (or Shell).
Suppression des zéros:
$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
, # for a comma locale (or Shell).
Cette valeur est utilisée pour modifier le fichier avec la liste des tests:
sed -i 's/[,.]/'"$dec"'/g' infile
Cela rend les exécutions sur n'importe quel shell ou paramètres régionaux automatiquement valides.
Il devrait être intuitif de couper le nombre à formater au format %.*e
ou même %.*g
de printf. La principale différence entre l'utilisation de %.*e
ou %.*g
est la façon dont ils comptent les chiffres. L'une utilise le nombre total, l'autre a besoin du nombre moins 1:
$ printf '%.*e %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00 1,235
Cela a bien fonctionné pour 4 chiffres significatifs.
Une fois que le nombre de chiffres a été coupé du nombre, nous avons besoin d'une étape supplémentaire pour formater les nombres avec des exposants différents de 0 (comme c'était le cas ci-dessus).
$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235
Cela fonctionne correctement. Le nombre de la partie entière (à gauche du séparateur décimal) n'est que la valeur de l'exposant ($ exp). Le nombre de décimales nécessaires est le nombre de chiffres significatifs ($ sig) moins le nombre de chiffres déjà utilisés sur la partie gauche du séparateur décimal:
a=$((exp<0?0:exp)) ### count of integer characters.
b=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"
Comme la partie intégrante du format f
n'a pas de limite, il n'est en fait pas nécessaire de la déclarer explicitement et ce code (plus simple) fonctionne:
a=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%0.*f' "$a" "$N"
Une première fonction qui pourrait le faire de manière plus automatisée:
# 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.
}
Cette première tentative fonctionne avec de nombreux nombres mais échouera avec les nombres pour lesquels le nombre de chiffres disponibles est inférieur au nombre significatif demandé et l'exposant est inférieur à -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
Il ajoutera de nombreux zéros inutiles.
Pour résoudre ce problème, nous devons nettoyer N de l'exposant et tous les zéros à la fin. Ensuite, nous pouvons obtenir la longueur effective des chiffres disponibles et travailler avec cela:
# 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.
}
Cependant, cela utilise des mathématiques en virgule flottante, et "rien n'est simple en virgule flottante": Pourquoi mes chiffres ne s'additionnent-ils pas?
Mais rien en "virgule flottante" n'est simple.
printf "%.2g " 76500,00001 76500
7,7e+04 7,6e+04
Toutefois:
printf "%.2g " 75500,00001 75500
7,6e+04 7,6e+04
Pourquoi?:
printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34
Et, aussi, la commande printf
est une commande intégrée de nombreux shells.
Ce que printf
imprime peut changer avec le 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
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"
}
Et les résultats sont:
$ 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
Si vous avez déjà le nombre sous forme de chaîne, c'est-à-dire "3456" ou "0,003756", vous ne pouvez le faire qu'en utilisant la manipulation de chaînes. Ce qui suit est sur le dessus de ma tête, et n'est pas minutieusement testé, et utilise sed, mais considérez:
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"
}
Où, fondamentalement, vous supprimez et enregistrez tout "-0,000" au début, puis utilisez une opération de sous-chaîne simple sur le reste. Une mise en garde à propos de ce qui précède est que les multiples 0 en tête ne sont pas supprimés. Je vais laisser cela comme un exercice.