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つの追加値が得られます。したがって、我々の平均は予想よりわずかに大きい。したがって、この問題を解決するための1つの試みは次のとおりです。 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選択したエディタにファイルをロードし、ヘッダーファイルを含む2つのプリプロセッサディレクティブを含む関数全体を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に丸めることであれば、次のような短い1行操作を実行できます。

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

関連情報