![printf를 사용한 이상한 부동소수점 반올림 동작](https://rvso.com/image/76426/printf%EB%A5%BC%20%EC%82%AC%EC%9A%A9%ED%95%9C%20%EC%9D%B4%EC%83%81%ED%95%9C%20%EB%B6%80%EB%8F%99%EC%86%8C%EC%88%98%EC%A0%90%20%EB%B0%98%EC%98%AC%EB%A6%BC%20%EB%8F%99%EC%9E%91.png)
이 사이트에서 몇 가지 답변을 읽었으며 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가지가 있습니다.
- 이미 설명된 뱅커의 규칙입니다.
- +무한쪽으로 반올림합니다. 반올림(양수의 경우)
- -무한쪽으로 반올림합니다. 내림(양수의 경우)
- 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.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
멀티코어/멀티프로세서 시스템이 있다면 시도해 보세요.
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").
"." 문제에 관한 추신. 및/또는 ","가 소수 구분 기호로 사용되는 경우 여기에 쉬운 범용 솔루션이 있습니다.
if [[ ${a##*[.,]} -ge "5" ]]; then a=$((${a%[.,]*}+1)); else a=${a%[.,]*}; fi