web-dev-qa-db-ja.com

printfによる奇妙な浮動小数点の丸め動作

このサイトでいくつかの回答を読んだところ、printf丸めが望ましいことがわかりました。

ただし、実際に使用すると、微妙なバグにより次のような動作が発生しました。

$ echo 197.5 | xargs printf '%.0f'
198
$ echo 196.5 | xargs printf '%.0f'
196
$ echo 195.5 | xargs printf '%.0f'
196

196.5196に丸められることに注意してください。

これは微妙な浮動小数点のバグである可能性があることを知っています(しかし、これはそれほど大きな数ではありませんね)。誰かがこれに光を当てることはできますか?

これの回避策も大いに歓迎されます(私はこれを今動かせるようにしているので)。

7
Hai Zhang

さすが「丸め」「バンカー丸め」です。

A 関連サイトの回答 説明してください。

このようなルールが解決しようとしている問題は、(小数点以下1桁の数値の場合)、

  • x.1からx.4までは切り捨てられます。
  • x.6からx.9までは切り上げられます。

それは4ダウンと4アップです。
丸めのバランスを保つために、x.5を丸める必要があります

  • up1回、down次へ。

これは、「最も近い「偶数」に丸める」というルールで行われます。

コードで:

sh _LC_NUMERIC=C printf '%.0f ' "$value"_
awkecho "$value" | awk 'printf( "%s", $1)'


オプション:

数値を丸めるには、合計4つの方法があります。

  1. すでにバンカーのルールを説明しました。
  2. +無限に向かって丸めます。切り上げ(正の数の場合)
  3. -無限に向かって丸めます。切り捨て(正数の場合)
  4. ゼロに向かって丸めます。小数(正または負)を削除します。

アップ

「切り上げ(_+infinite_)へ」が必要な場合は、awkを使用できます。

_value=195.5
_

awkecho "$value" | awk '{ printf("%d", $1 + 0.5) }'
bcecho "scale=0; ($value+0.5)/1" | bc

ダウン

「切り捨て(_-infinite_に向けて)」が必要な場合は、以下を使用できます。

_value=195.5
_

awkecho "$value" | awk '{ printf("%d", $1 - 0.5) }'
bcecho "scale=0; ($value-0.5)/1" | bc

小数を切り捨てます。

小数(ドットの後のもの)を削除します。
シェルを直接使用することもできます(ほとんどのシェルで動作します-POSIXです):

_value="127.54"    ### Works also for negative numbers.
_

シェル _echo "${value%%.*}"_
awkecho "$value"| awk '{printf ("%d",$0)}'
bcecho "scale=0; ($value)/1" | bc

15
user79743

これはバグではなく、意図的なものです。
これは、最も近いものへのラウンドのタイプを実行しています(これについては後で詳しく説明します)。
正確に.5どちらの方法でも丸めることができます。学校であなたはおそらく切り上げるように言われましたが、なぜですか?あなたはそれからそれ以上の数字を調べる必要がないからです。 3.51は4に切り上げます。 3.5の方がいいかもしれませんが、1桁目だけを見て.5を切り上げれば、常に正しいことがわかります。

ただし、2桁の10進数のセット(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つの試みは次のとおりです。切り上げの半分の時間、切り捨ての半分の時間。

これにより、バイアスが上向きから均等に変わります。ほとんどの場合、これはより良い方法です。

4
ctrl-alt-delor

丸めモードを一時的に変更することはそれほど珍しいことではなく、-per seではありませんが、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は、理由もなく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の後の数字は、以下を意味します。

  • 0への丸め
  • 1最も近い数値に丸める(デフォルト)
  • 2正の無限大への丸め
  • 3負の無限大への丸め

全体をインストールするか(推奨しません)、ファイルをコピーするか(前に名前を変更することを強くお勧めします!)src/printfPATHのディレクトリに保存し、上記のように使用します。

1
deamentiaemundi

X.1からx.4に切り捨ててx.5からx.9に切り上げる場合、次の短い1つのライナーを使用できます。

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

0
user209952