TL; DR
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 dash .
Il donnera l' printf
approximation de la partie entière de N avec des $sig
chiffres.
À 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 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
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 locale automatiquement valides.
Quelques notions de base.
Il devrait être intuitif de couper le nombre à formater avec le format %.*e
ou même %.*g
printf. La principale différence entre l'utilisation%.*e
ou la %.*g
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 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égrante du f
format n'a pas de limite, il n'est en fait 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 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.
Deuxième procès.
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
Pourtant:
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, également, la commande printf
est une fonction intégrée de nombreux obus.
Quelles printf
impressions peuvent changer avec la coque:
$ 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 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
%f
/%g
, mais c'est l'printf
argument, et on n'a pas besoin d'un POSIXprintf
pour avoir un shell POSIX. Je pense que vous auriez dû commenter au lieu de le modifier.