TL; DR
Simplemente copie y use la función sigf
en la sección A reasonably good "significant numbers" function:
. Está escrito (como todo el código en esta respuesta) para trabajar con el guión .
Le dará la printf
aproximación a la parte entera de N con $sig
dígitos.
Sobre el separador decimal.
El primer problema a resolver con printf es el efecto y el uso de la "marca decimal", que en EE. UU. Es un punto, y en DE es una coma (por ejemplo). Es un problema porque lo que funciona para alguna configuración regional (o shell) fallará con otra configuración regional. Ejemplo:
$ 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 solución común (e incorrecta) es establecer LC_ALL=C
el comando printf. Pero eso establece la marca decimal en un punto decimal fijo. Para entornos locales donde una coma (u otra) es el carácter utilizado comúnmente que es un problema.
La solución es descubrir dentro del script para el shell que lo ejecuta cuál es el separador decimal de la configuración regional. Eso es bastante simple:
$ printf '%1.1f' 0
0,0 # for a comma locale (or shell).
Eliminar ceros:
$ dec="$(IFS=0; printf '%s' $(printf '%.1f'))"; echo "$dec"
, # for a comma locale (or shell).
Ese valor se usa para cambiar el archivo con la lista de pruebas:
sed -i 's/[,.]/'"$dec"'/g' infile
Eso hace que las ejecuciones en cualquier shell o configuración regional sean automáticamente válidas.
Algunos conceptos básicos
Debería ser intuitivo cortar el número que se formateará con el formato %.*e
o incluso con %.*g
printf. La principal diferencia entre usar %.*e
o %.*g
es cómo cuentan los dígitos. Uno usa el conteo completo, el otro necesita el conteo menos 1:
$ printf '%.*e %.*g' $((4-1)) 1,23456e0 4 1,23456e0
1,235e+00 1,235
Eso funcionó bien durante 4 dígitos significativos.
Después de que el número de dígitos se ha cortado del número, necesitamos un paso adicional para formatear números con exponentes diferentes a 0 (como se indicó anteriormente).
$ N=$(printf '%.*e' $((4-1)) 1,23456e3); echo "$N"
1,235e+03
$ printf '%4.0f' "$N"
1235
Esto funciona correctamente El recuento de la parte entera (a la izquierda de la marca decimal) es solo el valor del exponente ($ exp). El recuento de decimales necesarios es la cantidad de dígitos significativos ($ sig) menos la cantidad de dígitos ya utilizados en la parte izquierda del separador decimal:
a=$((exp<0?0:exp)) ### count of integer characters.
b=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%*.*f' "$a" "$b" "$N"
Como la parte integral del f
formato no tiene límite, de hecho no hay necesidad de declararlo explícitamente y este código (más simple) funciona:
a=$((exp<sig?sig-exp:0)) ### count of decimal characters.
printf '%0.*f' "$a" "$N"
Primer intento.
Una primera función que podría hacer esto de una manera más automatizada:
# 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.
}
Este primer intento funciona con muchos números, pero fallará con números para los cuales la cantidad de dígitos disponibles es menor que el recuento significativo solicitado y el exponente es menor que -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
Agregará muchos ceros que no son necesarios.
Segundo juicio
Para resolver eso, necesitamos limpiar N del exponente y los ceros finales. Entonces podemos obtener la longitud efectiva de dígitos disponibles y trabajar con eso:
# 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.
}
Sin embargo, eso está usando matemática de coma flotante, y "nada es simple en coma flotante": ¿Por qué no se suman mis números?
Pero nada en "coma flotante" es simple.
printf "%.2g " 76500,00001 76500
7,7e+04 7,6e+04
Sin embargo:
printf "%.2g " 75500,00001 75500
7,6e+04 7,6e+04
¿Por qué?:
printf "%.32g\n" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34
Y, además, el comando printf
está integrado por muchos proyectiles.
Qué printf
impresiones pueden cambiar con el 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 función razonablemente buena de "números significativos":
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"
}
Y los resultados son:
$ 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
, pero ese es elprintf
argumento, y uno no necesita un POSIXprintf
para tener un shell POSIX. Creo que deberías haber comentado en lugar de editar allí.