Calculating rounded percentage in Shell Script without using "bc"

A POSIX-compliant shell script is only required to support integer arithmetic using the shell language ("only signed long integer arithmetic is required"), so a pure shell solution must emulate floating-point arithmetic[1]:

item=30
total=70

percent=$(( 100 * item / total + (1000 * item / total % 10 >= 5 ? 1 : 0) ))
  • 100 * item / total yields the truncated result of the integer division as a percentage.
  • 1000 * item / total % 10 >= 5 ? 1 : 0 calculates the 1st decimal place, and if it is equal to or greater than 5, adds 1 to the integer result in order to round it up.
  • Note how there's no need to prefix variable references with $ inside an arithmetic expansion $((...)).

If - in contradiction to the premise of the question - use of external utilities is acceptable:


  • awk offers a simple solution, which, however, comes with the caveat that it uses true double-precision binary floating point values and may therefore yield unexpected results in decimal representation - e.g., try printf '%.0f\n' 28.5, which yields 28 rather than the expected 29):
awk -v item=30 -v total=70 'BEGIN { printf "%.0f\n", 100 * item / total }'
  • Note how -v is used to define variables for the awk script, which allows for a clean separation between the single-quoted and therefore literal awk script and any values passed to it from the shell.

  • By contrast, even though bc is a POSIX utility (and can therefore be expected to be present on most Unix-like platforms) and performs arbitrary-precision arithmetic, it invariably truncates the results, so that rounding must be performed by another utility; printf, however, even though it is a POSIX utility in principle, is not required to support floating-point format specifiers (such as used inside awk above), so the following may or may not work (and is not worth the trouble, given the simpler awk solution, and given that precision problems due to floating-point arithmetic are back in the picture):
# !! 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
)"

[1] However, POSIX allows non-integer support "The shell may use a real-floating type instead of signed long as long as it does not affect the results in cases where there is no overflow." In practice, ksh and zsh. support floating-point arithmetic if you request it, but not bash and dash. If you want to be POSIX-compliant (run via /bin/sh), stick with integer arithmetic. Across all shells, integer division works as usual: the quotient is returned, that is the result of the division with any fractional part truncated (removed).


Use AWK (no 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

Taking 2 * the original percent calculation and getting the modulo 2 of that provides the increment for rounding.

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

echo $percent
43

(tested with bash, ash, dash and ksh)

This is a faster implementation than firing off an AWK coprocess:

$ 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