Вычисление округленного процента в Shell Script без использования "bc"

Я пытаюсь вычислить процент некоторых элементов в Shell Script. Я хотел бы округлить значение, то есть, если результат равен 59,5, я должен ожидать 60, а не 59.

item=30
total=70
percent=$((100*$item/$total))

echo $percent

Это дает 42.

Но на самом деле результат составляет 42,8, и я бы округлял его до 43. "bc" делает трюк, есть ли способ без использования "bc"?

Я не уполномочен устанавливать новые пакеты. "dc" и "bc" не присутствуют в моей системе. Это должно быть чисто Shell, не может использовать скрипты perl или python либо

Ответ 1

Используйте AWK (без bash-isms):

item=30
total=70
percent=$(awk "BEGIN { pc=100*${item}/${total}; i=int(pc); print (pc-i<0.5)?i:i+1 }")

echo $percent
43

Ответ 2

Принимая 2 * первоначальный процентный расчет и получая по модулю 2, который обеспечивает прирост округления.

item=30
total=70
percent=$((200*$item/$total % 2 + 100*$item/$total))

echo $percent
43

(проверены с помощью bash, ash, dash и ksh)

Это более быстрая реализация, чем отключение AWK-копроцесса:

$ pa() { for i in 'seq 0 1000'; do pc=$(awk "BEGIN { pc=100*${item}/${total}; i=int(pc); print (pc-i<0.5)?i:i+1 }"); done; }
$ time pa

real    0m24.686s
user    0m0.376s
sys     0m22.828s

$ pb() { for i in 'seq 0 1000'; do pc=$((200*$item/$total % 2 + 100*$item/$total)); done; }
$ time pb

real    0m0.035s
user    0m0.000s
sys     0m0.012s

Ответ 3

Сценарий оболочки, совместимый с POSIX, ограничен целочисленной арифметикой с использованием языка оболочки ("требуется только подписанная длинная целочисленная арифметика"), поэтому чистое решение оболочки должно эмулировать арифметику с плавающей запятой:

item=30
total=70

percent=$(( 100 * item / total + (1000 * item / total % 10 >= 5 ? 1 : 0) ))
  • 100 * item/total дает усеченный результат целочисленного деления в процентах.
  • 1000 * item/total % 10 >= 5? 1: 0 1000 * item/total % 10 >= 5? 1: 0 вычисляет первое десятичное место, а если оно равно или больше 5, добавляет 1 к целочисленному результату, чтобы округлить его.
  • Обратите внимание, что нет необходимости префикс ссылки на переменные с $ внутри арифметического расширения $((...)).

Если - в противоречие с предпосылкой вопроса - приемлемо использование внешних коммунальных услуг:


  • awk предлагает простое решение, которое, однако, связано с предостережением о том, что оно использует истинные двоичные значения с плавающей запятой двойной точности и поэтому может давать неожиданные результаты в десятичном представлении - например, try printf '%.0f\n' 28.5, что дает 28 а не ожидаемый 29):
awk -v item=30 -v total=70 'BEGIN { printf "%.0f\n", 100 * item / total }'
  • Обратите внимание на то, как -v используется для определения переменных для awk скрипта, что позволяет обеспечить чистое разделение между одиночным кадром и, следовательно, литералом awk скриптом и любыми значениями, переданными ему из оболочки.

  • Напротив, несмотря на то, что bc - это утилита POSIX (поэтому ее можно ожидать на большинстве Unix-подобных платформ) и выполняет арифметику произвольной точности, она неизменно обрезает результаты, так что округление должно выполняться другой утилитой; printf, однако, хотя это и есть утилита POSIX в принципе, не требуется поддерживать спецификаторы форматирования с плавающей запятой (например, используемые внутри awk выше), поэтому следующее может работать или не работать (и не стоит проблем, учитывая более простое решение awk и учитывая, что проблемы точности из-за арифметики с плавающей запятой вернулись на рисунке):
# !! This MAY work on your platform, but is NOT POSIX-compliant:
# '-l' tells 'bc' to set the precision to 20 decimal places, 'printf '%.0f\n''
# then performs the rounding to an integer.
item=20 total=70
printf '%.0f\n' "$(bc -l <<EOF
100 * $item / $total
EOF
)"

Ответ 4

Следующее основано на моем втором ответе, но с expr и back-ticks, которые (хотя, возможно, и отвратительны для большинства) могут быть адаптированы для работы даже в самых архаичных оболочках изначально:

item=30
total=70
percent='expr 200 \* $item / $total % 2 + 100 \* $item / $total'

echo $percent
43