shell教程
OutOfMemory.CN技术专栏-> shell-> shell教程-> 12.8. 数学计算命令

12.8. 数学计算命令

"Doingthenumbers"factor将一个正数分解为多个素数.bash$factor2741727417:3131937bcBash不能处理浮点运算,并且缺乏特定的一些操作,这些操作都是一些重要的计算功能.

"Doing the numbers"

factor

将一个正数分解为多个素数.

 bash$ factor 27417
 27417: 3 13 19 37
 	      

bc

Bash 不能处理浮点运算, 并且缺乏特定的一些操作,这些操作都是一些重要的计算功能.幸运的是, bc 可以解决这个问题.

bc 不仅仅是个多功能灵活的精确的工具, 而且它还提供许多编程语言才具备的一些方便的功能.

bc 比较类似于 C 语言的语法.

因为它是一个完整的 UNIX 工具, 所以它可以用在管道中, bc 在脚本中也是很常用的.

这里有一个简单的使用 bc 命令的模版可以用来在计算脚本中的变量. 用在命令替换 中.

 	      variable=$(echo "OPTIONS; OPERATIONS" | bc)
 	      


Example 12-42. 按月偿还贷款

#!/bin/bash
# monthlypmt.sh: 计算按月偿还贷款的数量.


#  这份代码是一份修改版本, 原始版本在 "mcalc" (贷款计算)包中,
#+ 这个包的作者是 Jeff Schmidt 和 Mendel Cooper (本书作者).
#   http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz  [15k]

echo
echo "Given the principal, interest rate, and term of a mortgage,"
echo "calculate the monthly payment."

bottom=1.0

echo
echo -n "Enter principal (no commas) "
read principal
echo -n "Enter interest rate (percent) "  # 如果是 12%, 那就键入 "12", 别输入 ".12".
read interest_r
echo -n "Enter term (months) "
read term


 interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # 转换成小数.
                 # "scale" 指定了有效数字的个数.


 interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc)


 top=$(echo "scale=9; $principal*$interest_rate^$term" | bc)

 echo; echo "Please be patient. This may take a while."

 let "months = $term - 1"
# ====================================================================
 for ((x=$months; x > 0; x--))
 do
   bot=$(echo "scale=9; $interest_rate^$x" | bc)
   bottom=$(echo "scale=9; $bottom+$bot" | bc)
#  bottom = $(($bottom + $bot"))
 done
# ====================================================================

# --------------------------------------------------------------------
#  Rick Boivie 给出了一个对上边循环的修改,
#+ 这个修改更加有效率, 将会节省大概 2/3 的时间.

# for ((x=1; x <= $months; x++))
# do
#   bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc)
# done


#  然后他又想出了一个更加有效率的版本,
#+ 将会节省 95% 的时间!

# bottom=`{
#     echo "scale=9; bottom=$bottom; interest_rate=$interest_rate"
#     for ((x=1; x <= $months; x++))
#     do
#          echo 'bottom = bottom * interest_rate + 1'
#     done
#     echo 'bottom'
#     } | bc`       # 在命令替换中嵌入一个 'for 循环'.
# --------------------------------------------------------------------------
#  On the other hand, Frank Wang suggests:
#  bottom=$(echo "scale=9; ($interest_rate^$term-1)/($interest_rate-1)" | bc)

#  因为 . . .
#  在循环后边的算法
#+ 事实上是一个等比数列的求和公式.
#  求和公式是 e0(1-q^n)/(1-q),
#+ e0 是第一个元素 并且 q=e(n+1)/e(n)
#+ 和 n 是元素的数量.
# --------------------------------------------------------------------------


 # let "payment = $top/$bottom"
 payment=$(echo "scale=2; $top/$bottom" | bc)
 # 使用2位有效数字来表示美元和美分.

 echo
 echo "monthly payment = \$$payment"  # 在总和的前边显示美元符号.
 echo


 exit 0


 # 练习:
 #   1) 处理输入允许本金总数中的逗号.
 #   2) 处理输入允许按照百分号和小数点的形式输入利率.
 #   3) 如果你真正想好好编写这个脚本,
 #      那么就扩展这个脚本让它能够打印出完整的分期付款表.


Example 12-43. 数制转换

#!/bin/bash
##########################################################################
# 脚本       :	base.sh - 用不同的数值来打印数字 (Bourne Shell)
# 作者       :	Heiner Steven (heiner.steven@odn.de)
# 日期       :	07-03-95
# 类型       :	桌面
# $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
# ==> 上边这行是 RCS ID 信息.
##########################################################################
# 描述
#
# Changes
# 21-03-95 stv	fixed error occuring with 0xb as input (0.2)
##########################################################################

# ==> 在本书中使用这个脚本通过了作者的授权.
# ==> 注释是本书作者添加的.

NOARGS=65
PN=`basename "$0"`			       # 程序名
VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2`  # ==> VER=1.2

Usage () {
    echo "$PN - print number to different bases, $VER (stv '95)
usage: $PN [number ...]

If no number is given, the numbers are read from standard input.
A number may be
    binary (base 2)		starting with 0b (i.e. 0b1100)
    octal (base 8)		starting with 0  (i.e. 014)
    hexadecimal (base 16)	starting with 0x (i.e. 0xc)
    decimal			otherwise (i.e. 12)" >&2
    exit $NOARGS
}   # ==> 打印出用法信息的函数.

Msg () {
    for i   # ==> 省略 [list] .
    do echo "$PN: $i" >&2
    done
}

Fatal () { Msg "$@"; exit 66; }

PrintBases () {
    # 决定数值的数制
    for i      # ==> 省略 [list]...
    do         # ==> 所以是对命令行参数进行操作.
	case "$i" in
	    0b*)		ibase=2;;	# 2进制
	    0x*|[a-f]*|[A-F]*)	ibase=16;;	# 16进制
	    0*)			ibase=8;;	# 8进制
	    [1-9]*)		ibase=10;;	# 10进制
	    *)
		Msg "illegal number $i - ignored"
		continue;;
	esac

	# 去掉前缀, 将16进制数字转换为大写(bc需要大写)
	number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
	# ==>使用":" 作为sed分隔符, 而不使用"/".

	# 将数字转换为10进制
	dec=`echo "ibase=$ibase; $number" | bc`  # ==> 'bc' 是个计算工具.
	case "$dec" in
	    [0-9]*)	;;			 # 数字没问题
	    *)		continue;;		 # 错误: 忽略
	esac

	# 在一行上打印所有的转换后的数字.
	# ==> 'here document' 提供命令列表给'bc'.
	echo `bc <<!
	    obase=16; "hex="; $dec
	    obase=10; "dec="; $dec
	    obase=8;  "oct="; $dec
	    obase=2;  "bin="; $dec
!
    ` | sed -e 's: :	:g'

    done
}

while [ $# -gt 0 ]
# ==>  这里必须使用一个 "while 循环",
# ==>+ 因为所有的 case 都可能退出循环或者
# ==>+ 结束脚本.
# ==> (感谢, Paulo Marcel Coelho Aragao.)
do
    case "$1" in
	--)     shift; break;;
	-h)     Usage;;                 # ==> 帮助信息.
	-*)     Usage;;
         *)     break;;			# 第一个数字
    esac   # ==> 对于非法输入更严格检查是非常有用的.
    shift
done

if [ $# -gt 0 ]
then
    PrintBases "$@"
else					# 从标准输入中读取
    while read line
    do
	PrintBases $line
    done
fi


exit 0
 

调用 bc 的另一种可选的方法就是使用 here document ,并把它嵌入到 命令替换 块中. 当一个脚本需要将一个选项列表和多个命令传递到 bc 中时, 这种方法就显得非常合适.

variable=`bc << LIMIT_STRING
options
statements
operations
LIMIT_STRING
`

...or...


variable=$(bc << LIMIT_STRING
options
statements
operations
LIMIT_STRING
)


Example 12-44. 使用 "here document" 来调用 bc

#!/bin/bash
# 使用命令替换来调用 'bc'
# 并与 'here document' 相结合.


var1=`bc << EOF
18.33 * 19.78
EOF
`
echo $var1       # 362.56


#  $( ... ) 这种标记法也可以.
v1=23.53
v2=17.881
v3=83.501
v4=171.63

var2=$(bc << EOF
scale = 4
a = ( $v1 + $v2 )
b = ( $v3 * $v4 )
a * b + 15.35
EOF
)
echo $var2       # 593487.8452


var3=$(bc -l << EOF
scale = 9
s ( 1.7 )
EOF
)
# 返回弧度为1.7的正弦.
# "-l" 选项将会调用 'bc' 算数库.
echo $var3       # .991664810


# 现在, 在函数中试一下...
hyp=             # 声明全局变量.
hypotenuse ()    # 计算直角三角形的斜边.
{
hyp=$(bc -l << EOF
scale = 9
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
# 不幸的是, 不能从bash 函数中返回浮点值.
}

hypotenuse 3.68 7.31
echo "hypotenuse = $hyp"    # 8.184039344


exit 0


Example 12-45. 计算圆周率

#!/bin/bash
# cannon.sh: 通过开炮来取得近似的圆周率值.

# 这事实上是一个"Monte Carlo"蒙特卡洛模拟的非常简单的实例:
#+ 蒙特卡洛模拟是一种由现实事件抽象出来的数学模型,
#+ 由于要使用随机抽样统计来估算数学函数, 所以使用伪随机数来模拟真正的随机.

#  想象有一个完美的正方形土地, 边长为10000个单位.
#  在这块土地的中间有一个完美的圆形湖,
#+ 这个湖的直径是10000个单位.
#  这块土地的绝大多数面积都是水, 当然只有4个角上有一些土地.
#  (可以把这个湖想象成为使这个正方形的内接圆.)
#
#  我们将使用老式的大炮和铁炮弹
#+ 向这块正方形的土地上开炮.
#  所有的炮弹都会击中这块正方形土地的某个地方.
#+ 或者是打到湖上, 或者是打到4个角的土地上.
#  因为这个湖占据了这个区域大部分地方,
#+ 所以大部分的炮弹都会"扑通"一声落到水里.
#  而只有很少的炮弹会"砰"的一声落到4个
#+ 角的土地上.
#
#  如果我们发出的炮弹足够随机的落到这块正方形区域中的话,
#+ 那么落到水里的炮弹与打出炮弹的总数的比率,
#+ 大概非常接近于 PI/4.
#
#  原因是所有的炮弹事实上都
#+ 打在了这个土地的右上角,
#+ 也就是, 笛卡尔坐标系的第一象限.
#  (之前的解释只是一个简化.)
#
#  理论上来说, 如果打出的炮弹越多, 就越接近这个数字.
#  然而, 对于shell 脚本来说一定会作些让步的,
#+ 因为它肯定不能和那些内建就支持浮点运算的编译语言相比.
#  当然就会降低精度.


DIMENSION=10000  # 这块土地的边长.
                 # 这也是所产生的随机整数的上限.

MAXSHOTS=1000    # 开炮次数.
                 # 10000 或更多次的话, 效果应该更好, 但有点太浪费时间了.
PMULTIPLIER=4.0  # 接近于 PI 的比例因子.

get_random ()
{
SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
RANDOM=$SEED                                  #  来自于 "seeding-random.sh"
                                              #+ 的例子脚本.
let "rnum = $RANDOM % $DIMENSION"             #  范围小于 10000.
echo $rnum
}

distance=        # 声明全局变量.
hypotenuse ()    # 从 "alt-bc.sh" 例子来的,
{                # 计算直角三角形的斜边的函数.
distance=$(bc -l << EOF
scale = 0
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
#  设置 "scale" 为 0 , 好让结果四舍五入为整数值,
#+ 这是这个脚本中必须折中的一个地方.
#  不幸的是, 这将降低模拟的精度.
}


# main() {

# 初始化变量.
shots=0
splashes=0
thuds=0
Pi=0

while [ "$shots" -lt  "$MAXSHOTS" ]           # 主循环.
do

  xCoord=$(get_random)                        # 取得随机的 X 与 Y 坐标.
  yCoord=$(get_random)
  hypotenuse $xCoord $yCoord                  #  直角三角形斜边 =
                                              #+ distance.
  ((shots++))

  printf "#%4d   " $shots
  printf "Xc = %4d  " $xCoord
  printf "Yc = %4d  " $yCoord
  printf "Distance = %5d  " $distance         #  到湖中心的
                                              #+ 距离 --
                                              #  起始坐标点 --
                                              #+  (0,0).

  if [ "$distance" -le "$DIMENSION" ]
  then
    echo -n "SPLASH!  "
    ((splashes++))
  else
    echo -n "THUD!    "
    ((thuds++))
  fi

  Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc)
  # 将比例乘以 4.0.
  echo -n "PI ~ $Pi"
  echo

done

echo
echo "After $shots shots, PI looks like approximately $Pi."
# 如果不太准的话, 那么就提高一下运行的次数. . .
# 可能是由于运行错误和随机数随机程度不高造成的.
echo

# }

exit 0

#  要想知道一个shell脚本到底适不适合作为
#+ 一种需要对复杂和精度都有要求的计算应用的模拟的话.
#
#  一般至少需要两个判断条件.
#  1) 作为一种概念的验证: 来显示它可以做到.
#  2) 在使用真正的编译语言来实现一个算法之前,
#+    使用脚本来测试和验证这个算法.

dc

dc (桌面计算器desk calculator) 工具是面向栈的并且使用 RPN (逆波兰表达式 "Reverse Polish Notation" 又叫"后缀表达式"). 与 bc 命令很相像 , 但是这个工具具备好多只有编程语言才具备的能力.(正常表达式 逆波兰表达式 a+b a,b,+ a+(b-c) a,b,c,-,+ a+(b-c)*d a,d,b,c,-,*,+)

绝大多数人都避免使用这个工具, 因为它需要非直觉的 RPN 输入. 但是, 它却有特定的用途.


Example 12-46. 将10进制数字转换为16进制数字

#!/bin/bash
# hexconvert.sh: 将10进制数字转换为16进制数字

E_NOARGS=65 # 缺命令行参数错误.
BASE=16     # 16进制.

if [ -z "$1" ]
then
  echo "Usage: $0 number"
  exit $E_NOARGS
  # 需要一个命令行参数.
fi
# 练习: 添加命令行参数检查.


hexcvt ()
{
if [ -z "$1" ]
then
  echo 0
  return    # 如果没有参数传递到这个函数中就 "return" 0.
fi

echo ""$1" "$BASE" o p" | dc
#                 "o" 设置输出的基数(数制).
#                   "p" 打印栈顶.
# 察看 dc 的 man 页来了解其他的选项.
return
}

hexcvt "$1"

exit 0

通过仔细学习 dc 命令的 info 页, 可以更深入的理解这个复杂的命令. 但是, 有一些精通 dc巫术 的小组经常会炫耀他们使用这个强大而又晦涩难懂的工具时的一些技巧, 并以此为乐.

 bash$ echo "16i[q]sa[ln0=aln100%Pln100/snlbx]sbA0D68736142snlbxq" | dc"
 Bash
 	      


Example 12-47. 因子分解

#!/bin/bash
# factr.sh: 分解约数

MIN=2       # 如果比这个数小就不行了.
E_NOARGS=65
E_TOOSMALL=66

if [ -z $1 ]
then
  echo "Usage: $0 number"
  exit $E_NOARGS
fi

if [ "$1" -lt "$MIN" ]
then
  echo "Number to factor must be $MIN or greater."
  exit $E_TOOSMALL
fi

# 练习: 添加类型检查 (防止非整型的参数).

echo "Factors of $1:"
# ---------------------------------------------------------------------------------
echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
# ---------------------------------------------------------------------------------
# 上边这行代码是 Michel Charpentier 编写的<charpov@cs.unh.edu>.
# 在此使用经过授权 (thanks).

 exit 0

awk

在脚本中使用浮点运算的另一种方法是使用 awk 内建的数学运算函数, 可以用在shell wrapper中.


Example 12-48. 计算直角三角形的斜边

#!/bin/bash
# hypotenuse.sh: 返回直角三角形的斜边.
#               ( 直角边长的平方和,然后对和取平方根)

ARGS=2                # 需要将2个直角边作为参数传递进来.
E_BADARGS=65          # 错误的参数值.

if [ $# -ne "$ARGS" ] # 测试传递到脚本中的参数值.
then
  echo "Usage: `basename $0` side_1 side_2"
  exit $E_BADARGS
fi


AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
#              命令 / 传递给awk的参数


# 现在, 将参数通过管道传递给awk.
echo -n "Hypotenuse of $1 and $2 = "
echo $1 $2 | awk "$AWKSCRIPT"

exit 0

© 内存溢出 OutOfMemory.CN