Estranho comportamento de arredondamento de float com printf

6

Li algumas respostas neste site e descobri que o arredondamento printf é desejável.

No entanto, quando eu usei na prática, um bug sutil me levou ao seguinte comportamento:

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

Observe que o arredondamento 196.5 se torna 196 .

Eu sei que isso pode ser algum bug sutil de ponto flutuante (mas isso não é um número muito grande, hein?), então alguém pode lançar alguma luz sobre isso?

Uma solução para isso também é muito bem-vinda (porque estou tentando colocar isso para funcionar agora).

    
por Dreaming in Code 01.11.2015 / 16:27

4 respostas

13

É como esperado, é "round to even" ou "arredondamento de Banker".

Uma resposta do site relacionado explica isso.

O problema que essa regra está tentando resolver é que (para números com um decimal),

  • x.1 até x.4 são arredondados para baixo.
  • x.6 até x.9 são arredondados.

Isso é 4 para baixo e 4 para cima.
Para manter o arredondamento em equilíbrio, precisamos arredondar o x.5

  • up uma vez e down a próxima.

Isso é feito pela regra: "Arredondar para o número par mais próximo" ».

no código:

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

Opções:

No total, existem quatro maneiras possíveis de arredondar um número:

  1. A regra do banqueiro já explicada.
  2. Arredondar para + infinito. Arredondar (para números positivos)
  3. Arredondar para -infinito. Arredondado (para números positivos)
  4. Arredondar para zero. Remova os decimais (positivos ou negativos).

Para cima

Se você precisa "arredondar para cima (em direção a +infinite )", então você pode usar o awk:

value=195.5

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

Abaixo

Se você precisa "arredondar para baixo (em direção a -infinite )", você pode usar:

value=195.5

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

Corte decimais.

Para remover os decimais (qualquer coisa depois do ponto).
Nós também podemos usar diretamente o shell (funciona na maioria dos shells - é POSIX):

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

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

    
por 01.11.2015 / 16:37
4

Não é um bug, é intencional.
Ele está fazendo um tipo de rodada para mais próximo (mais sobre isso mais tarde). Com exatamente .5 , podemos arredondar de qualquer forma. Na escola, onde você provavelmente disse para completar, mas por quê? Porque você não precisa examinar mais nenhum dígito, por exemplo 3,51 arredonda para 4; 3.5 poderia ir em direção ao éter, mas se nós olharmos apenas para o primeiro dígito e arredondarmos 0,5 para cima, então nós sempre acertamos.

No entanto, se olharmos para o conjunto de decimais de 2 dígitos: 0,00 0,01, 0,02, 0,03… 0,98, 0,99, veremos que existem 100 valores, 1 é um inteiro, 49 tem que ser arredondado, 49 tem que ser ser arredondado para baixo, 1 (0,50) poderia ir caminho de éter. Se nós sempre arredondarmos, então nós obtemos números médios que são 0,01 grandes demais.

Se estendermos o intervalo para 0 → 9,99, temos um valor 9 extra que é arredondado para cima. Assim, tornando a nossa média um pouco maior do que o esperado. Então, uma tentativa de consertar isso é: .5 voltas para o mesmo. Metade do tempo é arredondado, metade do tempo que termina.

Isso altera o preconceito de cima para o mesmo. Na maioria dos casos, isso é melhor.

    
por 01.11.2015 / 16:46
1

Alterar temporariamente os modos de arredondamento não é tão incomum e é possível com bin/printf , embora não seja per se que você precise alterar as fontes.

Você precisa das fontes dos coreutils, eu usei a versão mais recente disponível hoje, que era link .

Descompacte em um diretório de sua escolha com

tar xJfv coreutils-8.24.tar.xz

Mude para o diretório de origem

cd coreutils-8.24

Carregue o arquivo src/printf.c no editor de sua escolha e troque a função main inteira com a seguinte função, incluindo as duas diretivas de pré-processador, para incluir os arquivos de cabeçalho math.h e fenv.h . A função principal está no final e começa em int main... e termina no final do arquivo com o colchete de fechamento }

#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;
}

Execute ./configure da seguinte forma

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

Ele coloca o sufixo -own em cada subprograma (há muito) apenas no caso de você querer instalá-los todos e não tiver certeza se eles se encaixam com o resto do sistema. Os coreutils não são nomeados utilitários core sem um motivo!

Mas o mais importante é o LIBS=-lm na frente da linha. Precisamos da biblioteca matemática e esse comando informa ./configure para incluí-la na lista de bibliotecas necessárias.

Executar make

make

Se você tiver um sistema multicore / multiprocessador, tente

make -j4

onde o número (aqui "4") deve representar o número de núcleos que você está disposto a poupar para esse trabalho.

Se tudo correu bem, você tem o novo printf int src/printf . Experimente:

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

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

Ambos os comandos devem diferir na saída. Os números após IN_PRINTF_ROUNDING_MODE significam:

  • 0 Arredondamento para 0
  • 1 Arredondamento para o número mais próximo (padrão)
  • 2 Arredondamento em direção ao infinito positivo
  • 3 Arredondamento em direção ao infinito negativo

Você pode instalar o todo (não recomendado) ou apenas copiar o arquivo (renomear antes é altamente recomendado!) src/printf em um diretório no seu PATH e usar como descrito acima.

    
por 02.11.2015 / 00:01
0

Você pode fazer o seguinte short one liner se o que você realmente quer é arredondar para x.1 para x.4 e arredondar para x.5 para x.9.

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

Ou altere "5" para o que quiser, por ex. "6".

P.S. sobre o assunto com "." e / ou "," sendo usado como separadores decimais, aqui está uma solução universal fácil.

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

    
por 12.01.2017 / 00:57