Странное поведение округления чисел с плавающей точкой с помощью printf

Странное поведение округления чисел с плавающей точкой с помощью printf

Я прочитал некоторые ответы на этом сайте и нашел printfокругление желательным.

Однако когда я использовал его на практике, небольшая ошибка привела меня к следующему поведению:

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

Обратите внимание, что округление 196.5становится 196.

Я знаю, что это может быть какая-то тонкая ошибка с плавающей точкой (но ведь это не очень большое число, да?), так что может ли кто-нибудь пролить свет на это?

Обходной путь для этого также весьма приветствуется (потому что я сейчас пытаюсь заставить это работать).

решение1

Как и ожидалось, это «округление к четному» или «банковское округление».

Асвязанный ответ сайтаобъясни это.

Проблема, которую пытается решить это правило, заключается в том, что (для чисел с одним десятичным знаком)

  • Округление от x.1 до x.4 производится в меньшую сторону.
  • Округление производится от x.6 до x.9.

Это 4 вниз и 4 вверх.
Чтобы округление было сбалансированным, нам нужно округлить x.5

  • вверходин раз ивнизследующий.

Это делается по правилу: «Округлить до ближайшего «четного числа»».

В коде:

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


Параметры:

Всего существует четыре возможных способа округления числа:

  1. Уже объясненное правило Банкира.
  2. Округлить в сторону +бесконечности. Округлить вверх (для положительных чисел)
  3. Округление в сторону -бесконечности. Округление вниз (для положительных чисел)
  4. Округлить до нуля. Удалить десятичные знаки (положительные или отрицательные).

Вверх

Если вам действительно нужно «округлить в большую сторону (в сторону +infinite)», то вы можете использовать awk:

value=195.5

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

Вниз

Если вам действительно нужно «округлить в меньшую сторону (в сторону -infinite)», то вы можете использовать:

value=195.5

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

Обрежьте десятичные дроби.

Чтобы удалить десятичные знаки (все, что после точки).
Мы также могли бы напрямую использовать оболочку (работает на большинстве оболочек - это POSIX):

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

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

решение2

Это не ошибка, это сделано намеренно.
Он делает своего рода округление к ближайшему (подробнее об этом позже).
С помощью exact .5мы можем округлить в любую сторону. В школе вам, вероятно, говорили округлять в большую сторону, но почему? Потому что тогда вам не нужно проверять больше цифр, например, 3,51 округляется до 4; 3,5 может округляться в другую сторону, но если мы смотрим только на первую цифру и округляем 0,5 в большую сторону, то мы всегда получаем правильно.

Однако, если мы посмотрим на набор из 2-значных десятичных чисел: 0,00 0,01, 0,02, 0,03 … 0,98, 0,99, мы увидим, что есть 100 значений, 1 — целое число, 49 нужно округлить в большую сторону, 49 нужно округлить в меньшую сторону, 1 ( 0,50 ) может пойти в другую сторону. Если мы всегда округляем в большую сторону, то в среднем получим числа, которые на 0,01 больше.

Если мы расширим диапазон до 0 → 9,99, у нас будет 9 дополнительных значений, которые округляются вверх. Таким образом, наше среднее значение будет немного больше ожидаемого. Так что одна из попыток исправить это: .5 округляет к четному. Половину времени округляет вверх, половину времени округляет вниз.

Это меняет смещение с восходящего на равномерное. В большинстве случаев это лучше.

решение3

Временное изменение режимов округления не является чем-то необычным и возможно, bin/printfхотя и некак таковойвам нужно изменить источники.

Вам нужны исходники coreutils, я использовал последнюю доступную на сегодняшний день версию, которая былаhttp://ftp.gnu.org/gnu/coreutils/coreutils-8.24.tar.xz.

Распаковать в каталог по вашему выбору с помощью

tar xJfv coreutils-8.24.tar.xz

Перейдите в исходный каталог

cd coreutils-8.24

Загрузите файл src/printf.cв редактор по вашему выбору и замените всю mainфункцию следующей функцией, включая обе директивы препроцессора для включения заголовочных файлов math.hи fenv.h. Основная функция находится в конце и начинается int main...и заканчивается в самом конце файла закрывающей скобкой}

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

Выполнить ./configureследующим образом

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

Он добавляет суффикс -ownк каждой подпрограмме (их много) на тот случай, если вы хотите установить их все и не уверены, подходят ли они к остальной части системы. Coreutils не названыосновнойутилиты без причины!

Но самое важное — это LIBS=-lmперед строкой. Нам нужна математическая библиотека и эта команда говорит ./configureдобавить ее в список необходимых библиотек.

Запустить сделать

make

Если у вас многоядерная/многопроцессорная система, попробуйте

make -j4

где число (здесь «4») должно представлять количество ядер, которые вы готовы выделить для этой задачи.

Если все прошло хорошо, у вас есть новый printfint src/printf. Попробуйте:

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

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

Обе команды должны отличаться по выводу. Цифры после IN_PRINTF_ROUNDING_MODEозначают:

  • 0Округление к 0
  • 1Округление до ближайшего числа (по умолчанию)
  • 2Округление в сторону положительной бесконечности
  • 3Округление в сторону отрицательной бесконечности

Вы можете установить его целиком (не рекомендуется) или просто скопировать файл (предварительно переименовав его!) src/printfв каталог на вашем компьютере PATHи использовать его, как описано выше.

решение4

Вы можете сделать следующее короткое действие, если на самом деле вы хотите округлить в меньшую сторону от x.1 до x.4 и в большую сторону от x.5 до x.9.

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

Или измените «5» на что угодно, например, «6».

P.S. Что касается проблемы с использованием «.» и/или «,» в качестве десятичных разделителей, вот простое универсальное решение.

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

Связанный контент