Я прочитал некоторые ответы на этом сайте и нашел 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"
awk echo "$value" | awk 'printf( "%s", $1)'
Параметры:
Всего существует четыре возможных способа округления числа:
- Уже объясненное правило Банкира.
- Округлить в сторону +бесконечности. Округлить вверх (для положительных чисел)
- Округление в сторону -бесконечности. Округление вниз (для положительных чисел)
- Округлить до нуля. Удалить десятичные знаки (положительные или отрицательные).
Вверх
Если вам действительно нужно «округлить в большую сторону (в сторону +infinite
)», то вы можете использовать awk:
value=195.5
awk echo "$value" | awk '{ printf("%d", $1 + 0.5) }'
До нашей эры echo "scale=0; ($value+0.5)/1" | bc
Вниз
Если вам действительно нужно «округлить в меньшую сторону (в сторону -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
Это не ошибка, это сделано намеренно.
Он делает своего рода округление к ближайшему (подробнее об этом позже).
С помощью exact .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 дополнительных значений, которые округляются вверх. Таким образом, наше среднее значение будет немного больше ожидаемого. Так что одна из попыток исправить это: .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 -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Округление к 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».
P.S. Что касается проблемы с использованием «.» и/или «,» в качестве десятичных разделителей, вот простое универсальное решение.
if [[ ${a##*[.,]} -ge "5" ]]; then a=$((${a%[.,]*}+1)); else a=${a%[.,]*}; fi