Comportamento estranho de arredondamento flutuante com printf

Comportamento estranho de arredondamento flutuante com printf

Li algumas respostas neste site e achei o printfarredondamento desejável.

No entanto, quando 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.5se torna 196.

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

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

Responder1

Como esperado, é "arredondado para par" ou "arredondamento do banqueiro".

Aresposta do site relacionadoexplique.

A questão que tal regra está tentando resolver é que (para números com uma casa decimal),

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

São 4 para baixo e 4 para cima.
Para manter o arredondamento equilibrado, precisamos arredondar x,5

  • acimauma vez eabaixonas próximas.

Isto é feito pela regra: «Arredonde para o 'número par' mais próximo».

Em 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 já explicada regra do banqueiro.
  2. Arredonde em direção ao +infinito. Arredondar (para números positivos)
  3. Arredondar para -infinito. Arredondar para baixo (para números positivos)
  4. Arredondar para zero. Remova os decimais (positivos ou negativos).

Acima

Se você precisar de "arredondar (em direção +infinite)", poderá 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ê precisar de "arredondar para baixo (em direção -infinite)", poderá 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 após o ponto).
Também poderíamos 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

Responder2

Não é um bug, é intencional.
Está fazendo uma espécie de rodada para o mais próximo (mais sobre isso depois).
Com exatamente .5podemos arredondar de qualquer maneira. Na escola você provavelmente foi avisado para reunir, mas por quê? Porque você não precisa examinar mais nenhum dígito, por exemplo, 3,51, arredondado para 4; 3,5 poderia ser diferente, mas se olharmos apenas para o primeiro dígito e arredondarmos 0,5 para cima, sempre acertaremos.

Porém, 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 número inteiro, 49 devem ser arredondados para cima, 49 devem ser arredondados para baixo , 1 (0,50) poderia ir para o outro lado. Se sempre arredondarmos, obteremos, em média, números 0,01 grandes demais.

Se estendermos o intervalo para 0 → 9,99, teremos 9 valores extras que são arredondados para cima. Tornando assim a nossa média um pouco maior do que o esperado. Portanto, uma tentativa de consertar isso é: 0,5 rodadas em direção ao empate. Metade das vezes arredonda para cima, metade das vezes arredonda para baixo.

Isso muda a tendência de cima para uniforme. Na maioria dos casos, isso é melhor.

Responder3

Alterar temporariamente os modos de arredondamento não é tão incomum e é possível, bin/printfembora nãopor si sóvocê precisa alterar as fontes.

Você precisa das fontes dos coreutils, usei a versão mais recente disponível hoje que foihttp://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz.

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.cno editor de sua escolha e troque toda a mainfunção pela seguinte função, incluindo ambas as diretivas do pré-processador para incluir os arquivos de cabeçalho math.he fenv.h. A função principal está no final e começa 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 ./configureda seguinte maneira

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

Ele coloca o sufixo -ownem cada subprograma (há muitos), caso você queira instalar todos eles e não tenha certeza se eles cabem no resto do sistema. Os coreutils não são nomeadosessencialutilitários sem motivo!

Mas o mais importante é estar LIBS=-lmna frente da fila. Precisamos da biblioteca matemática e este comando diz ./configurepara adicioná-la à lista de bibliotecas necessárias.

Execute fazer

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ê deseja dispensar para esse trabalho.

Se tudo correu bem você tem o novo printfint 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 depois IN_PRINTF_ROUNDING_MODEsignificam:

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

Você pode instalar o arquivo inteiro (não recomendado) ou apenas copiar o arquivo (renomeá-lo antes é altamente recomendado!) src/printfem um diretório no seu PATHe usar conforme descrito acima.

Responder4

Você pode fazer a seguinte linha curta se o que realmente deseja é 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 exemplo, "6".

PS sobre o problema com "." e/ou "," sendo usados ​​como separadores decimais, aqui está uma solução universal fácil.

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

informação relacionada