Comment formater un nombre à floating point avec exactement 2 numbers significatifs dans bash?

Je veux imprimer le nombre à floating point avec exactement deux numbers significatifs dans bash (peut-être en utilisant un outil commun comme awk, bc, dc, perl etc.).

Exemples:

  • 76543 devrait être imprimé comme 76000
  • 0.0076543 doit être imprimé en tant que 0.0076

Dans les deux cas, les numbers significatifs sont 7 et 6. J'ai lu quelques réponses pour des problèmes similaires comme:

Comment arrondir les nombres à floating point dans la coquille?

Bash limitant la précision des variables à floating point

mais les réponses se concentrent sur la limitation du nombre de décimales (par exemple, la command bc avec l' scale=2 ou la command printf avec %.2f ) au lieu des numbers significatifs.

Existe-t-il un moyen facile de formater le nombre avec exactement 2 numbers significatifs ou dois-je écrire ma propre fonction?

Cette réponse à la première question liée a la ligne quasi-jetable à la fin:

Voir aussi %g pour arrondir à un nombre spécifié de numbers significatifs.

Donc, vous pouvez simplement écrire

 printf "%.2g" $n 

Exemples:

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

Bien sûr, vous avez maintenant une représentation mantisse-exposant plutôt que décimal pur, vous aurez donc envie de reconvertir:

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

Mettre tout cela set, et l'envelopper dans une fonction:

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

Cas de test

 for i in $(seq 12 | sed -e 's/.*/dc -e "12k 1.234 10 & 6 -^*p"/e') do echo $i "->" $(round 2 $i) done 

Résultats de 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 

TL; DR

Il suffit de copyr et d'utiliser la fonction sigf dans la section A reasonably good "significant numbers" function: sigf Il est écrit (comme tout le code dans cette réponse) pour fonctionner avec tiret .

Il donnera l'approximation printf à la partie entière de N avec $sig numbers.

À propos du séparateur décimal.

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 dans DE est une virgule (par exemple). C'est un problème car ce qui fonctionne pour certains parameters régionaux (ou shell) échouera avec d'autres parameters 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 commune (et incorrecte) consiste à définir LC_ALL=C pour la command printf. Mais cela définit la marque décimale à un point décimal fixe. Pour les locales où une virgule (ou autre) est le caractère utilisé commun qui est un problème.

La solution est de find à l'intérieur du script pour le shell qui l'exécute ce qui est le séparateur décimal de la locale. C'est assez simple:

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

Supprimer des zéros:

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

Cette valeur est utilisée pour changer le file avec la list des tests:

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

Cela rend les exécutions sur n'importe quel shell ou parameters régionaux automatiquement valides.


Quelques bases.

Il devrait être intuitif de couper le nombre à formater avec le format %.*e ou même %.*g d'printf. La principale différence entre l'utilisation de %.*e ou %.*g est la façon dont ils countnt les numbers. On utilise le count complet, l'autre a besoin du count less 1:

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

Cela a bien fonctionné pour 4 numbers significatifs.

Une fois le nombre de numbers supprimé, nous avons besoin d'une étape supplémentaire pour formater les nombres avec des exposants différents de 0 (comme ci-dessus).

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

Cela fonctionne correctement. Le count de la partie entière (à gauche de la marque décimale) est juste la valeur de l'exposant ($ exp). Le nombre de décimales nécessaire est le nombre de numbers significatifs ($ sig) less le nombre de numbers déjà utilisés dans 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égrale du format f n'a pas de limite, il n'est en effet pas nécessaire de le déclarer explicitement et ce code (plus simple) fonctionne:

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

Premier essai.

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 des nombres pour lesquels le nombre de numbers 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 appenda de nombreux zéros qui ne sont pas nécessaires.

Deuxième essai.

Pour résoudre ce problème, il faut nettoyer N de l'exposant et de tous les zéros qui suivent. Ensuite, nous pouvons get la longueur effective des numbers 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 calculs en floating point, et "rien n'est simple en floating point": Pourquoi mes numbers ne s'additionnent-ils pas?

Mais rien en "floating point" 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 command printf est une construction de plusieurs coquilles.
Quelles printings printf peuvent 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 

Une fonction "nombres significatifs" raisonnablement bonne:

 dec=$(IFS=0; printf '%s' $(printf '%.1f')) ### What is the decimal separator?. sed -i 's/[,.]/'"$dec"'/g' infile zeros(){ # create an ssortingng 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 bc 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 les suivants:

 $ 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 string, c'est-à-dire "3456" ou "0.003756", vous pouvez le faire uniquement en utilisant la manipulation de string. Ce qui suit est sur le dessus de ma tête, et pas complètement 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" } 

En gros, lorsque vous supprimez et enregistrez des trucs «-0.000» au début, utilisez une simple opération de sous-string sur le rest. Une mise en garde à propos de ce qui précède est que plusieurs 0 principaux ne sont pas supprimés. Je vais laisser cela comme un exercice.