Saltar al contenido

¿Cómo formatear el número de punto flotante con exactamente 2 dígitos significativos en bash?

Nuestro grupo especializado pasados muchos días de trabajo y de recopilar de información, hemos dado con los datos necesarios, deseamos que resulte de utilidad para tu proyecto.

Solución:

Esta respuesta a la primera pregunta vinculada tiene una línea casi desechable al final:

Ver también %g para redondear a un número específico de dígitos significativos.

Entonces puedes simplemente escribir

printf "%.2g" "$n"

(pero consulte la sección a continuación sobre el separador decimal y la configuración regional, y tenga en cuenta que no Bash printf no necesita apoyo %f y %g).

Ejemplos:

$ printf "%.2gn" 76543 0.0076543
7.7e+04
0.0077

Por supuesto, ahora tiene una representación de exponente de mantisa en lugar de decimal puro, por lo que querrá volver a convertir:

$ printf "%0.fn" 7.7e+06
7700000

$ printf "%0.7fn" 7.7e-06
0.0000077

Poniendo todo esto junto y envolviéndolo en una función:

# Function round(precision, number)
round() 
    n=$(printf "%.$1g" "$2")
    if [ "$n" != "$n#*e" ]
    then
        f="$n##*e-"
        test "$n" = "$f" && f= 

(Nota: esta función está escrita en un shell portátil (POSIX), pero asume que printf maneja las conversiones de punto flotante. Bash tiene una función printf eso sí, así que está bien aquí, y la implementación de GNU también funciona, por lo que la mayoría de los sistemas GNU / Linux pueden usar Dash de manera segura).

Casos de prueba

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

Resultados de la prueba

.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

Una nota sobre el separador decimal y la configuración regional

Todo el trabajo anterior asume que el carácter de radix (también conocido como separador decimal) es ., como en la mayoría de las configuraciones regionales en inglés. Uso de otras localizaciones , en su lugar, y algunas conchas tienen una función printf que respeta la localidad. En estos caparazones, es posible que deba configurar LC_NUMERIC=C para forzar el uso de . como carácter de base, o escribir /usr/bin/printf para evitar el uso de la versión incorporada. Esto último se complica por el hecho de que (al menos algunas versiones) parecen siempre analizar argumentos usando ., pero imprima utilizando la configuración regional actual.

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 pizca.

Le dará el 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 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 algún entorno local (o shell) fallará con otro entorno local. Ejemplo:

$ dash -c 'printf "%2.3fn" 12.3045'
12.305
$  ksh -c 'printf "%2.3fn" 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.2fn" 12,3045'
12,304

Una solución común (e incorrecta) es configurar LC_ALL=C para el comando printf. Pero eso establece la marca decimal en un punto decimal fijo. Para lugares donde una coma (u otro) es el carácter comúnmente utilizado que es un problema.

La solución es averiguar dentro del script para el shell que lo ejecuta cuál es el separador decimal de configuración regional. Eso es bastante simple:

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

Eliminando 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 locale sean automáticamente válidas.


Algunos conceptos básicos.

Debe ser intuitivo cortar el número para formatearlo con el formato %.*e o incluso %.*g de printf. La principal diferencia entre usar %.*e o %.*g así es como cuentan los dígitos. Uno usa el recuento completo, el otro necesita el recuento menos 1:

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

Eso funcionó bien para 4 dígitos significativos.

Una vez que se ha cortado el número de dígitos del número, necesitamos un paso adicional para formatear números con exponentes diferentes a 0 (como se hizo arriba).

$ 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 necesario es el número 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

Como parte integral de la f El formato no tiene límite, de hecho no hay necesidad de declararlo explícitamente y este código (más simple) funciona:

a=$((exp

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"

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.

Segunda prueba.

Para resolver eso, necesitamos limpiar N del exponente y los ceros finales. Entonces podemos obtener la longitud efectiva de dígitos disponible 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"

Sin embargo, eso es usar matemáticas de punto flotante, y "nada es simple en punto flotante": ¿Por qué mis números no se suman?

Pero nada en "punto 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 "%.32gn" 76500,00001e30 76500e30
7,6500000010000000001207515928855e+34
7,6499999999999999997831226199114e+34

Y, además, el comando printf es un componente integrado de muchos caparazones.
Qué printf las impresiones pueden cambiar con el caparazón:

$ 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 de "números significativos" razonablemente buena:

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()bc)         ### find ceilinglog(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

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

Recuerda dar recomendación a esta división si te fue de ayuda.

¡Haz clic para puntuar esta entrada!
(Votos: 2 Promedio: 4.5)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Respuestas a preguntas comunes sobre programacion y tecnología