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

正如所料,它是“四舍五入”,或者“银行家四舍五入”。

A相关网站解答解释一下。

该规则试图解决的问题是(对于小数点后一位的数字),

  • 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

修剪小数。

删除小数(点后面的任何内容)。
我们还可以直接使用 shell(适用于大多数 shell - 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. main 函数位于文件末尾,以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意思是:

  • 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”。

PS关于“.”的问题和/或“,”用作小数分隔符,这是一个简单的通用解决方案。

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

相关内容