Extraño comportamiento de redondeo flotante con printf

Extraño comportamiento de redondeo flotante con printf

Leí algunas respuestas en este sitio y encontré printfdeseable el redondeo.

Sin embargo, cuando lo usé en la práctica, un error sutil me llevó al siguiente comportamiento:

$ echo 197.5 | xargs printf '%.0f'
198
$ echo 196.5 | xargs printf '%.0f'
196
$ echo 195.5 | xargs printf '%.0f'
196

Observe que el redondeo 196.5se vuelve 196.

Sé que esto puede ser algún error sutil de punto flotante (pero no es un número muy grande, ¿eh?), Entonces, ¿alguien puede arrojar algo de luz sobre esto?

También es muy bienvenida una solución alternativa para esto (porque estoy intentando poner esto a funcionar ahora).

Respuesta1

Es como se esperaba, es "redondeo a par" o "redondeo bancario".

Arespuesta del sitio relacionadoexplícalo.

El problema que dicha regla intenta resolver es que (para números con un decimal),

  • x.1 hasta x.4 se redondean hacia abajo.
  • x.6 hasta x.9 se redondean hacia arriba.

Son 4 abajo y 4 arriba.
Para mantener el redondeo equilibrado, necesitamos redondear el x.5

  • arribauna vez yabajoel siguiente.

Esto se hace mediante la regla: « Redondear al 'número par' más cercano ».

En codigo:

LC_NUMERIC=C printf '%.0f ' "$value"
echo "$value" | awk 'printf( "%s", $1)'


Opciones:

En total, existen cuatro formas posibles de redondear un número:

  1. La regla del banquero ya explicada.
  2. Redondea hacia +infinito. Redondear hacia arriba (para números positivos)
  3. Redondea hacia -infinito. Redondear hacia abajo (para números positivos)
  4. Redondear hacia cero. Elimina los decimales (ya sean positivos o negativos).

Arriba

Si necesita "redondear hacia arriba (hacia +infinite)", puede usar awk:

value=195.5

echo "$value" | awk '{ printf("%d", $1 + 0.5) }'
echo "scale=0; ($value+0.5)/1" | bc

Abajo

Si necesita "redondear hacia abajo (hacia -infinite)", puede utilizar:

value=195.5

echo "$value" | awk '{ printf("%d", $1 - 0.5) }'
echo "scale=0; ($value-0.5)/1" | bc

Recorta decimales.

Para eliminar los decimales (cualquier cosa después del punto).
También podríamos usar directamente el shell (funciona en la mayoría de los shells, es POSIX):

value="127.54"    ### Works also for negative numbers.

echo "${value%%.*}"
echo "$value"| awk '{printf ("%d",$0)}'
echo "scale=0; ($value)/1" | bc

Respuesta2

No es un error, es intencional.
Está haciendo una especie de ronda al más cercano (más sobre esto más adelante).
Con exactamente .5podemos redondear en cualquier sentido. En la escuela probablemente te dijeron que reunieras, pero ¿por qué? Porque entonces no tendrá que examinar más dígitos, por ejemplo, 3,51 redondea a 4; 3,5 podría ir por el otro lado, pero si solo miramos el primer dígito y redondeamos 0,5 hacia arriba, siempre lo haremos bien.

Sin embargo, si miramos el conjunto de decimales de 2 dígitos: 0,00 0,01, 0,02, 0,03… 0,98, 0,99, veremos que hay 100 valores, 1 es un número entero, 49 se deben redondear hacia arriba, 49 se deben redondear hacia abajo. , 1 (0,50) podría ir en otra dirección. Si siempre redondeamos hacia arriba, obtenemos en promedio números que son 0,01 demasiado grandes.

Si ampliamos el rango a 0 → 9,99, tenemos 9 valores adicionales que se redondean hacia arriba. Por lo tanto, nuestro promedio es un poco mayor de lo esperado. Así que un intento de solucionar este problema es: 0,5 rondas para igualar. La mitad del tiempo se redondea hacia arriba, la mitad del tiempo se redondea hacia abajo.

Esto cambia el sesgo de hacia arriba a incluso. En la mayoría de los casos esto es mejor.

Respuesta3

Cambiar temporalmente los modos de redondeo no es tan inusual y es posible, bin/printfaunque noper senecesitas cambiar las fuentes.

Necesitas las fuentes de coreutils, utilicé la última versión disponible hoy que erahttp://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz.

Descomprímalo en un directorio de su elección con

tar xJfv coreutils-8.24.tar.xz

Cambie al directorio fuente

cd coreutils-8.24

Cargue el archivo src/printf.cen el editor de su elección e intercambie toda la mainfunción con la siguiente función, incluidas las dos directivas del preprocesador para incluir los archivos de encabezado math.hy fenv.h. La función principal está al final, comienza int main...y termina al final del archivo con el corchete de cierre.}

#include <math.h>
#include <fenv.h>
int
main (int argc, char **argv)
{
  char *format;
  char *rounding_env;
  int args_used;
  int rounding_mode;

  initialize_main (&argc, &argv);
  set_program_name (argv[0]);
  setlocale (LC_ALL, "");
  bindtextdomain (PACKAGE, LOCALEDIR);
  textdomain (PACKAGE);

  atexit (close_stdout);

  exit_status = EXIT_SUCCESS;

  posixly_correct = (getenv ("POSIXLY_CORRECT") != NULL);
  // accept rounding modes from an environment variable
  if ((rounding_env = getenv ("BIN_PRINTF_ROUNDING_MODE")) != NULL)
    {
      rounding_mode = atoi(rounding_env);
      switch (rounding_mode)
        {
        case 0:
          if (fesetround(FE_TOWARDZERO) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTowardZero failed"));
              return EXIT_FAILURE;
            }
          break;
       case 1:
          if (fesetround(FE_TONEAREST) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTiesToEven failed"));
              return EXIT_FAILURE;
            }
          break;
       case 2:
          if (fesetround(FE_UPWARD) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTowardPositive failed"));
              return EXIT_FAILURE;
            }
          break;
       case 3:
          if (fesetround(FE_DOWNWARD) != 0)
            {
              error (0, 0, _("setting rounding mode to roundTowardNegative failed"));
              return EXIT_FAILURE;
            }
          break;
       default:
         error (0, 0, _("setting rounding mode failed for unknown reason"));
         return EXIT_FAILURE;
      }
    }
  /* We directly parse options, rather than use parse_long_options, in
     order to avoid accepting abbreviations.  */
  if (argc == 2)
    {
      if (STREQ (argv[1], "--help"))
        usage (EXIT_SUCCESS);

      if (STREQ (argv[1], "--version"))
        {
          version_etc (stdout, PROGRAM_NAME, PACKAGE_NAME, Version, AUTHORS,
                       (char *) NULL);
          return EXIT_SUCCESS;
        }
    }

  /* The above handles --help and --version.
     Since there is no other invocation of getopt, handle '--' here.  */
  if (1 < argc && STREQ (argv[1], "--"))
    {
      --argc;
      ++argv;
    }

  if (argc <= 1)
    {
      error (0, 0, _("missing operand"));
      usage (EXIT_FAILURE);
    }

  format = argv[1];
  argc -= 2;
  argv += 2;

  do
    {
      args_used = print_formatted (format, argc, argv);
      argc -= args_used;
      argv += args_used;
    }
  while (args_used > 0 && argc > 0);

  if (argc > 0)
    error (0, 0,
           _("warning: ignoring excess arguments, starting with %s"),
           quote (argv[0]));

  return exit_status;
}

Ejecute ./configurede la siguiente manera

LIBS=-lm ./configure --program-suffix=-own

Pone el sufijo -ownen cada subprograma (hay muchos) en caso de que desee instalarlos todos y no esté seguro de si encajan con el resto del sistema. Los coreutils no tienen nombre.centroutilidades sin motivo!

Pero lo más importante es estar LIBS=-lmal frente de la fila. Necesitamos la biblioteca matemática y este comando indica ./configureque la agreguemos a la lista de bibliotecas necesarias.

ejecutar hacer

make

Si tiene un sistema multinúcleo/multiprocesador, pruebe

make -j4

donde el número (aquí "4") debe representar la cantidad de núcleos que está dispuesto a reservar para ese trabajo.

Si todo ha ido bien tienes el nuevo printfint src/printf. Pruébalo:

BIN_PRINTF_ROUNDING_MODE=1 ./src/printf '%.0f\n' 196.5

BIN_PRINTF_ROUNDING_MODE=2 ./src/printf '%.0f\n' 196.5

Ambos comandos deberían diferir en el resultado. Los números después IN_PRINTF_ROUNDING_MODEsignifican:

  • 0Redondeando hacia 0
  • 1Redondeo hacia el número más cercano (predeterminado)
  • 2Redondeando hacia el infinito positivo
  • 3Redondeando hacia el infinito negativo

Puede instalar el archivo completo (no recomendado) o simplemente copiar el archivo (¡es muy recomendable cambiarle el nombre antes!) src/printfen un directorio de su PATHy usarlo como se describe anteriormente.

Respuesta4

Puede hacer la siguiente línea corta si lo que realmente desea es redondear hacia abajo de x.1 a x.4 y redondear hacia arriba de x.5 a x.9.

if [[ ${a#*.} -ge "5" ]]; then a=$((${a%.*}+1)); else a=${a%.*}; fi

O cambie "5" por lo que desee, por ejemplo, "6".

PD con respecto al problema con "." y/o "," al usarse como separador(es) decimal(es), aquí hay una solución universal fácil.

if [[ ${a##*[.,]} -ge "5" ]]; then a=$((${a%[.,]*}+1)); else a=${a%[.,]*}; fi

información relacionada