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., tryprintf '%.0f\n' 28.5
, which yields28
rather than the expected29
):
awk -v item=30 -v total=70 'BEGIN { printf "%.0f\n", 100 * item / total }'
- Note how
-v
is used to define variables for theawk
script, which allows for a clean separation between the single-quoted and therefore literalawk
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 insideawk
above), so the following may or may not work (and is not worth the trouble, given the simplerawk
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