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을 향해 반올림합니다. 소수(양수 또는 음수)를 제거합니다.

위로

"( 쪽으로) 반올림"이 필요한 경우 +infiniteawk를 사용할 수 있습니다.

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.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

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

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을 의미합니다.

  • 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").

"." 문제에 관한 추신. 및/또는 ","가 소수 구분 기호로 사용되는 경우 여기에 쉬운 범용 솔루션이 있습니다.

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

관련 정보