![printf での奇妙な浮動小数点丸め動作](https://rvso.com/image/76426/printf%20%E3%81%A7%E3%81%AE%E5%A5%87%E5%A6%99%E3%81%AA%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E4%B8%B8%E3%82%81%E5%8B%95%E4%BD%9C.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
予想通り、「偶数丸め」、つまり「銀行型丸め」です。
あ関連サイト 回答説明する。
この規則が解決しようとしている問題は、(小数点が1つある数字の場合)
- x.1からx.4までは切り捨てられます。
- x.6 から x.9 までは切り上げられます。
4が下、4が上です。
切り上げのバランスを保つために、x.5を切り上げる必要があります。
- 上一度だけ下次の。
これは、「最も近い「偶数」に丸める」という規則によって行われます。
コードでは:
シュ LC_NUMERIC=C printf '%.0f ' "$value"
awk echo "$value" | awk 'printf( "%s", $1)'
オプション:
数値を丸める方法は全部で 4 つあります。
- すでに説明したバンカーのルール。
- +無限大に向かって丸めます。切り上げます(正の数の場合)
- -無限大に向かって丸めます。切り捨てます(正の数の場合)
- ゼロに向かって丸めます。小数点以下 (正または負) を削除します。
上
「切り上げ( に向かって+infinite
)」が必要な場合は、awk を使用できます。
value=195.5
awk echo "$value" | awk '{ printf("%d", $1 + 0.5) }'
紀元前 echo "scale=0; ($value+0.5)/1" | bc
下
「切り捨て (Toward -infinite
)」が必要な場合は、次を使用できます。
value=195.5
awk 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%%.*}"
awk echo "$value"| awk '{printf ("%d",$0)}'
紀元前 echo "scale=0; ($value)/1" | bc
答え2
これはバグではなく、意図的なものです。
最も近い値に丸めているのです (詳細は後述)。
正確には、.5
どちらの方向にも丸めることができます。学校では切り上げるように言われたかもしれませんが、なぜでしょうか? そうすると、それ以上の桁を調べる必要がなくなるからです。たとえば、3.51 は 4 に切り上げられます。3.5 はどちらの方向にもできますが、最初の桁だけを見て .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
任意のエディタに読み込み、関数全体を、ヘッダーファイルと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正の無限大に向かって丸める
- 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