12.8. Команды выполнения математических операций


factor

Разложение целого числа на простые множители.

  1. bash$ factor 27417
  2. 27417: 3 13 19 37
  3.        


bc

Bash не в состоянии выполнять действия над числами с плавающей запятой и не содержит многих важных математических функций. К счастью существует bc.

Универсальная, выполняющая вычисления с произвольной точностью, утилита bc обладает некоторыми возможностями, характерными для языков программирования.

Синтаксис bc немного напоминает язык C.

Поскольку это утилита Unix, то она может достаточно широко использоваться в сценариях на языке командной оболочки, в том числе и в конвейерной обработке данных.

Ниже приводится простой шаблон работы с утилитой bc в сценарии. Здесь используется прием подстановки команд.

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


Пример 12-35. Ежемесячные выплаты по займу

  1. #!/bin/bash
  2. # monthlypmt.sh: Расчет ежемесячных выплат по займу.
  3. #  Это измененный вариант пакета "mcalc" (mortgage calculator),
  4. #+ написанного Jeff Schmidt и Mendel Cooper (ваш покорный слуга).
  5. #   <a href="http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz" title="http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz">http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz</a>  [15k]
  6. echo
  7. echo "Введите сумму займа, процентную ставку и срок займа,"
  8. echo "для расчета суммы ежемесячных выплат."
  9. bottom=1.0
  10. echo
  11. echo -n "Сумма займа (без запятых — с точностью до доллара) "
  12. read principal
  13. echo -n "Процентная ставка (процент) "  # Если 12%, то нужно вводить "12", а не ".12".
  14. read interest_r
  15. echo -n "Срок займа (месяцев) "
  16. read term
  17.  interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # Здесь "scale" — точность вычислений.
  18.  interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc)
  19.  top=$(echo "scale=9; $principal*$interest_rate^$term" | bc)
  20.  echo; echo "Прошу подождать. Вычисления потребуют некоторого времени."
  21.  let "months = $term - 1"
  22. # ====================================================================
  23.  for ((x=$months; x > 0; x--))
  24.  do
  25.    bot=$(echo "scale=9; $interest_rate^$x" | bc)
  26.    bottom=$(echo "scale=9; $bottom+$bot" | bc)
  27. #  bottom = $(($bottom + $bot"))
  28.  done
  29. # --------------------------------------------------------------------
  30. #  Rick Boivie предложил более эффективную реализацию
  31. #+ цикла вычислений, который дает выигрыш по времени на 2/3.
  32. # for ((x=1; x <= $months; x++))
  33. # do
  34. #   bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc)
  35. # done
  36. #  А затем нашел еще более эффективную альтернативу,
  37. #+ которая выполняется в 20 раз быстрее !!!
  38. # bottom=`{
  39. #     echo "scale=9; bottom=$bottom; interest_rate=$interest_rate"
  40. #     for ((x=1; x <= $months; x++))
  41. #     do
  42. #          echo 'bottom = bottom * interest_rate + 1'
  43. #     done
  44. #     echo 'bottom'
  45. #     } | bc`       # Внедрить цикл 'for' в конструкцию подстановки команд.
  46. # ====================================================================
  47.  # let "payment = $top/$bottom"
  48.  payment=$(echo "scale=2; $top/$bottom" | bc)
  49.  # Два знака после запятой, чтобы показать доллары и центы.
  50.  echo
  51.  echo "ежемесячные выплаты = \$$payment"  # Вывести знак "доллара" перед числом.
  52.  echo
  53.  exit 0
  54.  # Упражнения:
  55.  #   1) Добавьте возможность ввода суммы с точностью до цента.
  56.  #   2) Добавьте возможность ввода процентной ставки как в виде процентов, так и в виде десятичного числа — доли целого.
  57.  #   3) Если вы действительно честолюбивы,
  58.  #      добавьте в сценарий вывод полной таблицы помесячных выплат.

Пример 12-36. Перевод чисел из одной системы счисления в другую

  1. :
  2. ##########################################################################
  3. # Shellscript:  base.sh - вывод чисел в разных системах счисления (Bourne Shell)
  4. # Author     :  Heiner Steven (<a href="mailto:heiner.steven@odn.de">heiner.steven@odn.de</a>)
  5. # Date       :  07-03-95
  6. # Category   :  Desktop
  7. # $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
  8. ##########################################################################
  9. # Description
  10. #
  11. # Changes
  12. # 21-03-95 stv  исправлена ошибка, возникающая при вводе числа 0xb (0.2)
  13. ##########################################################################
  14. # ==> Используется в данном документе с разрешения автора.
  15. # ==> Комментарии добавлены автором документа.
  16. NOARGS=65
  17. PN=`basename "$0"`                             # Имя программы
  18. VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2`  # ==> VER=1.2
  19. Usage () {
  20.     echo "$PN - вывод чисел в различных системах счисления, $VER (stv '95)
  21. Порядок использования: $PN [number ...]
  22. Если число не задано, то производится ввод со stdin.
  23. Число может быть:
  24.    двоичное            должно начинаться с комбинации символов 0b (например 0b1100)
  25.    восьмеричное        должно начинаться с 0  (например 014)
  26.    шестнадцатиричное   должно начинаться с комбинации символов 0x (например 0xc)
  27.    десятичное          в любом другом случае (например 12)" >&2
  28.     exit $NOARGS
  29. }   # ==> Функция вывода сообщения о порядке использования.
  30. Msg () {
  31.     for i   # ==> [список] параметров опущен.
  32.     do echo "$PN: $i" >&2
  33.     done
  34. }
  35. Fatal () { Msg "$@"; exit 66; }
  36. PrintBases () {
  37.     # Определение системы счисления
  38.     for i      # ==> [список] параметров опущен...
  39.     do         # ==> поэтому работает с аргументами командной строки.
  40.         case "$i" in
  41.             0b*)                ibase=2;;       # двоичная
  42.             0x*|[a-f]*|[A-F]*)  ibase=16;;      # шестнадцатиричная
  43.             0*)                 ibase=8;;       # восьмеричная
  44.             [1-9]*)             ibase=10;;      # десятичная
  45.             *)
  46.                 Msg "Ошибка в числе $i - число проигнорировано"
  47.                 continue;;
  48.         esac
  49.         # Удалить префикс и преобразовать шестнадцатиричные цифры в верхний регистр (этого требует bc)
  50.         number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
  51.         # ==> вместо "/", здесь используется символ ":" как разделитель для sed.
  52.         # Преобразование в десятичную систему счисления
  53.         dec=`echo "ibase=$ibase; $number" | bc`  # ==> 'bc' используется как калькулятор.
  54.         case "$dec" in
  55.             [0-9]*)     ;;       # все в порядке
  56.             *)          continue;; # ошибка: игнорировать
  57.         esac
  58.         # Напечатать все преобразования в одну строку.
  59.         # ==> 'вложенный документ' — список команд для 'bc'.
  60.         echo `bc <<!
  61.             obase=16; "hex="; $dec
  62.             obase=10; "dec="; $dec
  63.             obase=8;  "oct="; $dec
  64.             obase=2;  "bin="; $dec
  65. !
  66.     ` | sed -e 's: :    :g'
  67.     done
  68. }
  69. while [ $# -gt 0 ]
  70. do
  71.     case "$1" in
  72.         --)     shift; break;;
  73.         -h)     Usage;;          # ==> Вывод справочного сообщения.
  74.         -*)     Usage;;
  75.         *)      break;;          # первое число
  76.     esac   # ==> Хорошо бы расширить анализ вводимых символов.
  77.     shift
  78. done
  79. if [ $# -gt 0 ]
  80. then
  81.     PrintBases "$@"
  82. else                                    # чтение со stdin
  83.     while read line
  84.     do
  85.         PrintBases $line
  86.     done
  87. fi

Один из вариантов вызова bc — использование вложенного документа, внедряемого в блок с подстановкой команд. Это особенно актуально, когда сценарий должен передать bc значительный по объему список команд и аргументов.

  1. variable=`bc << LIMIT_STRING
  2. options
  3. statements
  4. operations
  5. LIMIT_STRING
  6. `
...или...
  1. variable=$(bc << LIMIT_STRING
  2. options
  3. statements
  4. operations
  5. LIMIT_STRING
  6. )


Пример 12-37. Пример взаимодействия bc со "встроенным документом"

  1. #!/bin/bash
  2. # Комбинирование 'bc' с
  3. # 'вложенным документом'.
  4. var1=`bc << EOF
  5. 18.33 * 19.78
  6. EOF
  7. `
  8. echo $var1       # 362.56
  9. #  запись $( ... ) тоже работает.
  10. v1=23.53
  11. v2=17.881
  12. v3=83.501
  13. v4=171.63
  14. var2=$(bc << EOF
  15. scale = 4
  16. a = ( $v1 + $v2 )
  17. b = ( $v3 * $v4 )
  18. a * b + 15.35
  19. EOF
  20. )
  21. echo $var2       # 593487.8452
  22. var3=$(bc -l << EOF
  23. scale = 9
  24. s ( 1.7 )
  25. EOF
  26. )
  27. # Возвращается значение синуса от 1.7 радиана.
  28. # Ключом "-l" вызывается математическая библиотека 'bc'.
  29. echo $var3       # .991664810
  30. # Попробуем функции...
  31. hyp=             # Объявление глобальной переменной.
  32. hypotenuse ()    # Расчет гипотенузы прямоугольного треугольника.
  33. {
  34. hyp=$(bc -l << EOF
  35. scale = 9
  36. sqrt ( $1 * $1 + $2 * $2 )
  37. EOF
  38. )
  39. # К сожалению, функции Bash не могут возвращать числа с плавающей запятой.
  40. }
  41. hypotenuse 3.68 7.31
  42. echo "гипотенуза = $hyp"    # 8.184039344
  43. exit 0

Пример 12-38. Вычисление числа "пи"

  1. #!/bin/bash
  2. # cannon.sh: Аппроксимация числа "пи".
  3. # Это очень простой вариант реализации метода "Monte Carlo",
  4. #+ математическое моделирование событий реальной жизни,
  5. #+ для эмуляции случайного события используются псевдослучайные числа.
  6. #  Допустим, что мы располагаем картой квадратного участка поверхности со стороной квадрата 10000 единиц.
  7. #  На этом участке, в центре, находится совершенно круглое озеро,
  8. #+ с диаметром в 10000 единиц.
  9. #  Т.е. озеро покрывает почти всю карту, кроме ее углов.
  10. #  (Фактически — это квадрат со вписанным кругом.)
  11. #
  12. #  Пусть по этому участку ведется стрельба железными ядрами из древней пушки
  13. #  Все ядра падают где-то в пределах данного участка,
  14. #+ т.е. либо в озеро, либо на сушу, по углам участка.
  15. #  Поскольку озеро покрывает большую часть участка,
  16. #+ то большинство ядер будет падать в воду.
  17. #  Незначительная часть ядер будет падать на твердую почву.
  18. #
  19. #  Если произвести достаточно большое число неприцельных выстрелов по данному участку,
  20. #+ то отношение попаданий в воду к общему числу выстрелов будет примерно равно
  21. #+ значению PI/4.
  22. #
  23. #  По той простой причине, что стрельба фактически ведется только
  24. #+ по правому верхнему квадранту карты.
  25. #  (Предыдущее описание было несколько упрощено.)
  26. #
  27. #  Теоретически, чем больше будет произведено выстрелов, тем точнее будет результат.
  28. #  Однако, сценарий на языке командной оболочки, в отличие от других языков программирования,
  29. #+ в которых доступны операции с плавающей запятой, имеет некоторые ограничения.
  30. #  К сожалению, это делает вычисления менее точными.
  31. DIMENSION=10000  # Длина стороны квадратного участка поверхности.
  32.                  # Он же — верхний предел для генератора случайных чисел.
  33. MAXSHOTS=1000    # Количество выстрелов.
  34.                  # 10000 выстрелов (или больше) даст лучший результат,
  35.                                                                  # но потребует значительного количества времени.
  36. PMULTIPLIER=4.0  # Масштабирующий коэффициент.
  37. get_random ()
  38. {
  39. SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
  40. RANDOM=$SEED                                  #  Из примера "seeding-random.sh"
  41. let "rnum = $RANDOM % $DIMENSION"             #  Число не более чем 10000.
  42. echo $rnum
  43. }
  44. distance=        # Объявление глобальной переменной.
  45. hypotenuse ()    # Расчет гипотенузы прямоугольного треугольника.
  46. {                # Из примера "alt-bc.sh".
  47. distance=$(bc -l << EOF
  48. scale = 0
  49. sqrt ( $1 * $1 + $2 * $2 )
  50. EOF
  51. )
  52. #  Установка "scale" в ноль приводит к округлению результата "вниз",
  53. #+ это и есть то самое ограничение, накладываемое командной оболочкой.
  54. #  Что, к сожалению, снижает точность аппроксимации.
  55. }
  56. # main() {
  57. # Инициализация переменных.
  58. shots=0
  59. splashes=0
  60. thuds=0
  61. Pi=0
  62. while [ "$shots" -lt  "$MAXSHOTS" ]           # Главный цикл.
  63. do
  64.   xCoord=$(get_random)                        # Получить случайные координаты X и Y.
  65.   yCoord=$(get_random)
  66.   hypotenuse $xCoord $yCoord                  #  Гипотенуза = расстоянию.
  67.   ((shots++))
  68.   printf "#%4d   " $shots
  69.   printf "Xc = %4d  " $xCoord
  70.   printf "Yc = %4d  " $yCoord
  71.   printf "Distance = %5d  " $distance         #  Растояние от
  72.                                               #+ центра озера,
  73.                                               #+ с координатами (0,0).
  74.   if [ "$distance" -le "$DIMENSION" ]
  75.   then
  76.     echo -n "ШЛЕП!  "                         # попадание в озеро
  77.     ((splashes++))
  78.   else
  79.     echo -n "БУХ!    "                        # попадание на твердую почву
  80.     ((thuds++))
  81.   fi
  82.   Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc)
  83.   # Умножение на коэффициент 4.0.
  84.   echo -n "PI ~ $Pi"
  85.   echo
  86. done
  87. echo
  88. echo "После $shots выстрела, примерное значение числа \"пи\" равно $Pi."
  89. # Имеет тенденцию к завышению...
  90. # Вероятно из-за ошибок округления и несовершенства генератора случайных чисел.
  91. echo
  92. # }
  93. exit 0
  94. #  Самое время задуматься над тем, является ли сценарий удобным средством
  95. #+ для выполнения большого количества столь сложных вычислений.
  96. #
  97. #  Тем не менее, этот пример может расцениваться как
  98. #  1) Доказательство возможностей языка командной оболочки.
  99. #  2) Прототип для "обкатки" алгоритма перед тем как перенести
  100. #+    его на высокоуровневые языки программирования компилирующего типа.
dc

Утилита dc (desk calculator) — это калькулятор, использующий "Обратную Польскую Нотацию", и ориентированный на работу со стеком.

Многие стараются избегать испоьзования dc, из-за непривычной формы записи операндов и операций. Однако, dc имеет и своих сторонников.

Пример 12-39. Преобразование чисел из десятичной в шестнадцатиричную систему счисления

  1. #!/bin/bash
  2. # hexconvert.sh: Преобразование чисел из десятичной в шестнадцатиричную систему счисления.
  3. BASE=16     # Шестнадцатиричная.
  4. if [ -z "$1" ]
  5. then
  6.   echo "Порядок использования: $0 number"
  7.   exit $E_NOARGS
  8.   # Необходим аргумент командной строки.
  9. fi
  10. # Упражнение: добавьте проверку корректности аргумента.
  11. hexcvt ()
  12. {
  13. if [ -z "$1" ]
  14. then
  15.   echo 0
  16.   return    # "Return" 0, если функции не был передан аргумент.
  17. fi
  18. echo ""$1" "$BASE" o p" | dc
  19. #                 "o" устанавливает основание системы счисления для вывода.
  20. #                   "p" выводит число, находящееся на вершине стека.
  21. # См. 'man dc'.
  22. return
  23. }
  24. hexcvt "$1"
  25. exit 0

Изучение страниц info dc позволит детальнее разобраться с утилитой. Однако, отряд "гуру", которые могут похвастать своим знанием этой мощной, но весьма запутанной утилиты, весьма немногочислен.

Пример 12-40. Разложение числа на простые множители

  1. #!/bin/bash
  2. # factr.sh: Разложение числа на простые множители
  3. MIN=2       # Не работает с числами меньше 2.
  4. E_NOARGS=65
  5. E_TOOSMALL=66
  6. if [ -z $1 ]
  7. then
  8.   echo "Порядок использования: $0 number"
  9.   exit $E_NOARGS
  10. fi
  11. if [ "$1" -lt "$MIN" ]
  12. then
  13.   echo "Исходное число должно быть больше или равно $MIN."
  14.   exit $E_TOOSMALL
  15. fi
  16. # Упражнение: Добавьте проверку типа числа (не целые числа должны отвергаться).
  17. echo "Простые множители для числа $1:"
  18. # ---------------------------------------------------------------------------------
  19. echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
  20. # ---------------------------------------------------------------------------------
  21. # Автор вышеприведенной строки: Michel Charpentier <charpov@cs.unh.edu>.
  22. # Используется с его разрешения (спасибо).
  23.  exit 0
awk

Еще один способ выполнения математических операций, над числами с плавающей запятой, состоит в создании сценария-обертки, использующего математические функции awk.

Пример 12-41. Расчет гипотенузы прямоугольного треугольника

  1. #!/bin/bash
  2. # hypotenuse.sh: Возвращает "гипотенузу" прямоугольного треугольника.
  3. #               ( корень квадратный от суммы квадратов катетов)
  4. ARGS=2                # В сценарий необходимо передать два катета.
  5. E_BADARGS=65          # Ошибка в аргументах.
  6. if [ $# -ne "$ARGS" ] # Проверка количества аргументов.
  7. then
  8.   echo "Порядок использования: `basename $0` катет_1 катет_2"
  9.   exit $E_BADARGS
  10. fi
  11. AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
  12. #            команды и параметры, передаваемые в awk
  13. echo -n "Гипотенуза прямоугольного треугольника, с катетами $1 и $2, = "
  14. echo $1 $2 | awk "$AWKSCRIPT"
  15. exit 0