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

相關內容