在不使用 Shell 脚本的情况下计算舍入百分比"bc"



我正在尝试计算Shell脚本中某些项目的百分比。我想四舍五入这个值,也就是说,如果结果是59.5,我应该期望60,而不是59。

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

这给出了42。

但实际上,结果是42.8,我想四舍五入到43。"bc"起作用了,有没有办法不用"bc"?

我无权安装任何新软件包。我的系统中不存在"dc"one_answers"bc"。它应该是纯Shell,不能使用perl或python脚本。

使用AWK(无bash-ism(:

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,为舍入提供增量。

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

POSIX兼容的shell脚本只需要支持使用shell语言整数算术("只需要有符号的长整数算术"(,因此纯shell解决方案必须模拟浮点算术[1]:

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计算小数点后的第一位,如果它等于或大于5,则将整数结果加1以将其四舍五入
  • 请注意,在算术扩展$((...))中,没有需要用$作为变量引用的前缀

如果与问题的前提相矛盾,使用外部实用程序是可以接受的:


  • awk提供了一个简单解决方案,但它附带了警告,即它使用真正的双精度二进制浮点值,因此可能会在十进制表示中产生意外结果-例如,尝试printf '%.0fn' 28.5,它会产生28,而不是预期的29(:
awk -v item=30 -v total=70 'BEGIN { printf "%.0fn", 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 '%.0fn'`
# then performs the rounding to an integer.
item=20 total=70
printf '%.0fn' "$(bc -l <<EOF
100 * $item / $total
EOF
)"

[1]然而,POSIX允许非整数支持"shell可以使用真正的浮点类型,而不是有符号的long,只要它在没有溢出的情况下不影响结果。"在实践中,kshzsh。如果请求,则支持浮点运算,但不支持bashdash。如果您想兼容POSIX(通过/bin/sh运行(,请坚持使用整数运算。在所有shell中,整数除法照常工作:返回,这是截断(删除(任何小数部分的除法结果

以下是基于我的第二个答案,但有了expr反勾——这(虽然可能让大多数人讨厌(可以适应于即使是最古老的外壳:

item=30
total=70
percent=`expr 200 * $item / $total % 2 + 100 * $item / $total`
echo $percent
43

这里有一个没有模2和纯bash的答案,它适用于负百分比:

echo "$(( 200*item/total - 100*item/total ))"

我们可以定义一个通用的整数除法,四舍五入到最接近的整数,然后应用它

function rdiv {
   echo $((  2 * "$1"/"$2" - "$1"/"$2" ))
}
rdiv "$((100 * $item))" "$total"

通常,要使用float到int的转换来计算最近的整数,可以使用,比如在Java:n = (int) (2 * x) - (int) x;

解释(基于二进制扩展(:

fbf(x)x的小数部分的第一位,保持x的符号。设int(x)为整数部分,x向0截断,再次保持x的符号。四舍五入到最接近的整数是

round(x) = fbf(x) + int(x).

例如,如果是x = 100*-30/70 = -42.857,则是fbf(x) = -1int(x) = -42

我们现在可以理解Michael Back的第二个答案,它基于模2,因为:

fbf(x) = int(2*x) % 2 

我们可以在这里理解答案,因为:

fbf(x) = int(2*x) - 2*(int(x))

查看这个公式的一个简单方法是将2的乘积视为将二进制表示向左移动。当我们首先移位x,然后进行截断时,我们保留了fbf(x)位,如果我们首先进行截断,然后进行移位,则会丢失该位。

关键是,根据Posix和C标准,shell向0进行"舍入",但我们希望向最接近的整数进行舍入。我们只需要找到一个技巧来做到这一点,而不需要模2。

由于对正百分比的自然限制(几乎涵盖所有应用程序(,我们有一个简单得多的解决方案:

echo $(( ($item*1000/$total+5)/10 ))

它使用了shell对0的自动截断,而不是像Michael Back的第二个答案那样使用显式的模2,我已经投票支持了。

顺便说一句,这对很多人来说可能是显而易见的,但一开始我并不清楚,在评估这段代码时向0的截断在POSIX中是固定的,它说它必须遵守整数算术的C标准

算术运算符和控制流关键字应实现为相当于引用的ISO C标准部分中的标准,如选择ISO C标准运算符和控制流关键字。——看见https://pubs.opengroup.org/onlinepubs/9699919799/

有关C标准,请参见https://mc-stan.org/docs/2_21/functions-reference/int-arithmetic.html或第6.3.1.4节http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf.第一个引用是针对C++的,但它当然定义了与第二个引用相同的整数算术,第二个参考引用C99 ISO C标准。

请注意,对正百分比的限制并不意味着我们不能有一个百分比,比如说80%,即减少20%。负百分比对应于超过100%的下降。当然,这是可能发生的,但不是在典型的应用程序中。

根据C标准,为了覆盖负百分比,我们必须测试中间值item*1000/$total是否为负,在这种情况下,减去5,而不是加5,但我们失去了简单性。

最新更新