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)'


옵션:

숫자를 반올림하는 방법에는 총 4가지가 있습니다.

  1. 은행원의 법칙을 설명했습니다.
  2. +무한대 방향으로 반올림합니다. 반올림(양수의 경우)
  3. -무한대 방향으로 반올림합니다. 내림(양수의 경우)
  4. 0을 향해 반올림합니다. 소수(양수 또는 음수)를 제거합니다.

위로

정말로 "반올림 +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

이는 실수가 아니라 의도적인 것입니다.
일종의 가장 가까운 반올림을 수행합니다(나중에 자세히 설명).
말 그대로 .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개의 추가 값을 얻게 됩니다. 따라서 우리의 평균은 예상보다 약간 큽니다. 따라서 이 문제를 해결하기 위한 한 가지 시도는 다음과 같습니다. 0.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.hfenv.hint 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

멀티코어/멀티프로세서 시스템을 갖고 있다면 시도해 보세요.

make -j4

여기서 숫자(여기서는 "4")는 해당 작업을 위해 기꺼이 절약할 코어 수를 나타내야 합니다.

printf모든 것이 잘 진행되면 새로운 int 를 얻게 됩니다 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의미합니다.

  • 00을 향해 반올림
  • 1가장 가까운 숫자로 반올림(기본값)
  • 2양의 무한대로 반올림
  • 음의 무한대로 반올림

전체 파일을 설치하거나(권장하지 않음) 파일(강력히 권장되기 전에 이름을 바꾸었음)을 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"과 같이 원하는 대로 변경하세요.

PS: 소수 구분 기호로 사용되는 "." 문제에 대한 간단한 일반적인 해결책은 다음과 같습니다.

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

관련 정보