15.8. Comandi per le operazioni matematiche

"Calcoli matematici"

factor

Scompone un intero in fattori primi.

bash$ factor 27417
27417: 3 13 19 37
	      

bc

Bash non è in grado di gestire i calcoli in virgola mobile, quindi non dispone di operatori per alcune importanti funzioni matematiche. Fortunatamente viene in soccorso bc.

Non semplicemente una versatile utility per il calcolo in precisione arbitraria, bc offre molte delle potenzialità di un linguaggio di programmazione.

bc possiede una sintassi vagamente somigliante al C.

Dal momento che si tratta di una utility UNIX molto ben collaudata, e che quindi può essere utilizzata in una pipe, bc risulta molto utile negli script.

Ecco un semplice modello di riferimento per l'uso di bc per calcolare una variabile di uno script. Viene impiegata la sostituzione di comando.

	      variabile=$(echo "OPZIONI; OPERAZIONI" | bc)
	      

Esempio 15-42. Rata mensile di un mutuo

#!/bin/bash
# monthlypmt.sh: Calcola la rata mensile di un mutuo (prestito).


#  Questa è una modifica del codice del pacchetto "mcalc" (mortgage calculator),
#+ di Jeff Schmidt e Mendel Cooper (vostro devotissimo, autore di 
#+ questo documento). 
#  http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz  [15k]

echo
echo "Dato il capitale, il tasso d'interesse e la durata del mutuo,"
echo "calcola la rata di rimborso mensile."

denominatore=1.0

echo
echo -n "Inserisci il capitale (senza i punti di separazione)"
read capitale
echo -n "Inserisci il tasso d'interesse (percentuale)" # Se 12% inserisci "12",
                                                       #+ non ".12".
read t_interesse
echo -n "Inserisci la durata (mesi)"
read durata


 t_interesse=$(echo "scale=9; $t_interesse/100.0" | bc) #  Lo converte 
                                                        #+ in decimale.
                  # "scale" determina il numero delle cifre decimali.


 tasso_interesse=$(echo "scale=9; $t_interesse/12 + 1.0" | bc)


 numeratore=$(echo "scale=9; $capitale*$tasso_interesse^$durata" | bc)

 echo; echo "Siate pazienti. È necessario un po' di tempo."

 let "mesi = $durata - 1"
# ====================================================================
 for ((x=$mesi; x > 0; x--))
 do
    den=$(echo "scale=9; $tasso_interesse^$x" | bc)
    denominatore=$(echo "scale=9; $denominatore+$den" | bc)
    #  denominatore = $(($denominatore + $den"))
 done
# ==================================================================== 

# --------------------------------------------------------------------
#  Rick Boivie ha indicato un'implementazione più efficiente del 
#+ ciclo precedente che riduce di 2/3 il tempo di calcolo.

# for ((x=1; x <= $mesi; x++))
# do
#   denominatore=$(echo "scale=9; $denominatore * $tasso_interesse + 1" | bc)
# done


#  Dopo di che se n'è uscito con un'alternativa ancor più efficiente, una che
#+ abbatte il tempo di esecuzione di circa il 95%!

# denominatore=`{
#  echo "scale=9; denominatore=$denominatore; tasso_interesse=$tasso_interesse"
#  for ((x=1; x <= $mesi; x++))
#  do
#       echo 'denominatore = denominatore * tasso_interesse + 1'
#  done
#  echo 'denominatore'
#  } | bc`       #  Ha inserito il 'ciclo for' all'interno di una 
                 #+ sostituzione di comando.
# --------------------------------------------------------------------------
#  In aggiunta, Frank Wang suggerisce:
#  denominatore=$(echo "scale=9; ($tasso_interesse^$mesi-1)/\
# ($tasso_interesse-1)" | bc)

#  Perché . . .
#  L'algoritmo che segue il ciclo è,
#+ in realtà, la somma di una serie geometrica.
#  La formula è e0(1-q^n)/(1-q),
#+ dove e0 è il primo elemento, mentre q=e(n+1)/e(n)
#+ ed n il numero degli elementi.
# --------------------------------------------------------------------------


 # let "rata = $numeratore/$denominatore"
 rata=$(echo "scale=2; $numeratore/$denominatore" | bc)
 # Vengono usate due cifre decimali per i centesimi di Euro.

 echo
 echo "rata mensile = Euro $rata" 
 echo


 exit 0

 
 # Esercizi:
 #   1) Filtrate l'input per consentire l'inserimento del capitale con i
 #      punti di separazione.
 #   2) Filtrate l'input per consentire l'inserimento del tasso
 #      d'interesse sia in forma percentuale che decimale.
 #   3) Se siete veramente ambiziosi, implementate lo script per visualizzare 
 #      il piano d'ammortamento completo.

Esempio 15-43. Conversione di base

#!/bin/bash
################################################################################
# Shellscript:  base.sh - visualizza un numero in basi differenti (Bourne Shell) 
# Autore     :  Heiner Steven (heiner.steven@odn.de)
# Data       :  07-03-95
# Categoria  :  Desktop
# $Id        :  base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
# ==> La riga precedente rappresenta l'ID RCS.
################################################################################
# Descrizione
#
# Changes
# 21-03-95 stv        fixed error occuring with 0xb as input (0.2)
################################################################################

# ==> Utilizzato in questo documento con il permesso dell'autore dello script.
# ==> Commenti aggiunti dall'autore del libro.

NOARG=65
NP=`basename "$0"`                               # Nome del programma
VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2`    # ==> VER=1.2

Utilizzo () {
    echo "$NP - visualizza un numero in basi diverse, $VER (stv '95)
utilizzo: $NP [numero ...]

Se non viene fornito alcun numero, questi vengono letti dallo standard input. 
Un numero può essere
    binario (base 2)                inizia con 0b (es. 0b1100)
    ottale (base 8)                 inizia con 0  (es. 014)
    esadecimale (base 16)           inizia con 0x (es. 0xc)
    decimale                        negli altri casi (es. 12)" >&2
    exit $NOARG
}   # ==> Funzione per la visualizzazione del messaggio di utilizzo.

Msg () {
    for i   # ==> manca in [lista].
    do echo "$NP: $i" >&2
    done
}

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

VisualizzaBasi () {
    # Determina la base del numero
    for i      # ==> manca in [lista] ...
    do         # ==> perciò opera sugli argomenti forniti da riga di comando.
      case "$i" in
            0b*)                ibase=2;;        # binario
            0x*|[a-f]*|[A-F]*)  ibase=16;;       # esadecimale
            0*)                 ibase=8;;        # ottale
            [1-9]*)             ibase=10;;       # decimale
            *)
                 Msg "$i numero non valido - ignorato"
                 continue;;
      esac
      #  Toglie il prefisso, converte le cifre esadecimali in caratteri
      #+ maiuscoli (è richiesto da bc)
      numero=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
      # ==> Si usano i ":" come separatori per sed, al posto della "/".

      # Converte il numero in decimale
      dec=`echo "ibase=$ibase; $numero" | bc`
      # ==> 'bc' è l'utility di calcolo.
      case "$dec" in
      [0-9]*)        ;;                        # numero ok
      *)             continue;;                # errore: ignora
      esac

      # Visualizza tutte le conversioni su un'unica riga.
      # ==> 'here document' fornisce una lista di comandi a 'bc'.
      echo `bc <<!
           obase=16; "esa="; $dec
           obase=10; "dec="; $dec
           obase=8;  "ott="; $dec
           obase=2;  "bin="; $dec
!
      ` | sed -e 's: :        :g'

    done
}

while [ $# -gt 0 ]
# ==>  "Ciclo while" che qui si rivela veramente necessario
# ==>+ poiché in ogni caso, o si esce dal ciclo
# ==>+ oppure lo script termina.
# ==> (Grazie, Paulo Marcel Coelho Aragao.)
do
    case "$1" in
            --)        shift; break;;
            -h)        Utilizzo;;            # ==> Messaggio di aiuto.
            -*)        Utilizzo;;
             *)        break;;               # primo numero
    esac   # ==> Sarebbe utile un'ulteriore verifica d'errore per un input
           #+    non consentito.
    shift
done

if [ $# -gt 0 ]
then
    VisualizzaBasi "$@"
else                                         # legge dallo stdin
    while read riga
    do
        VisualizzaBasi $riga
    done
fi


exit 0

Un metodo alternativo per invocare bc comprende l'uso di un here document inserito in un blocco di sostituzione di comando. Questo risulta particolarmente appropriato quando uno script ha la necessità di passare un elenco di opzioni e comandi a bc.

variabile=`bc << STRINGA_LIMITE
opzioni
enunciati
operazioni
STRINGA_LIMITE
`

...oppure...


variabile=$(bc << STRINGA_LIMITE
opzioni
enunciati
operazioni
STRINGA_LIMITE
)

Esempio 15-44. Invocare bc usando un here document

#!/bin/bash
# Invocare 'bc' usando la sostituzione di comando
# in abbinamento con un 'here document'.


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


#  $( ... ) anche questa notazione va bene.
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
)
# Restituisce il seno di 1.7 radianti.
# L'opzione "-l" richiama la libreria matematica di 'bc'.
echo $var3       # .991664810


# Ora proviamolo in una funzione...
ip=              # Dichiarazione di variabile globale.
ipotenusa ()     # Calcola l'ipotenusa di un triangolo rettangolo.
{
ip=$(bc -l << EOF
scale = 9
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
#  Sfortunatamente, non si può avere un valore di ritorno in virgola mobile 
#+ da una funzione Bash.
}

ipotenusa 3.68 7.31
echo "ipotenusa = $ip"     # 8.184039344


exit 0

Esempio 15-45. Calcolo del pi greco

#!/bin/bash
# cannon.sh: Approssimare il PI a cannonate.

# È un esempio molto semplice di una simulazione "Monte Carlo":
#+ un modello matematico di un evento reale, utilizzando i numeri 
#+ pseudocasuali per simulare la probabilità dell'urna.

#  Consideriamo un appezzamento di terreno perfettamente quadrato, di 10000 
#+ unità di lato.
#  Questo terreno ha, al centro, un lago perfettamente circolare con un 
#+ diametro di 10000 unità.
#  L'appezzamento è praticamente tutta acqua, tranne per il terreno ai
#+ quattro angoli (Immaginatelo come un quadrato con inscritto un cerchio).
#
#  Spariamo delle palle con un vecchio cannone sul terreno quadrato. Tutti i
#+ proiettili cadranno in qualche parte dell'appezzamento, o nel lago o negli 
#+ angoli emersi.
#  Poiché il lago occupa la maggior parte dell'area, la maggior 
#+ parte dei proiettili CADRA' nell'acqua.
#  Solo pochi COLPIRANNO il terreno ai quattro angoli del quadrato.
#
#  Se le cannonate sparate saranno sufficientemente casuali, senza aver 
#+ mirato, allora il rapporto tra le palle CADUTE IN ACQUA ed il totale degli 
#+ spari approssimerà il valore di PI/4.
#
#  La spiegazione sta nel fatto che il cannone spara solo al quadrante superiore
#+ destro del quadrato, vale a dire, il 1 Quadrante del piano di assi
#+ cartesiani. (La precedente spiegazione era una semplificazione.)
#
#  Teoricamente, più alto è il numero delle cannonate, maggiore
#+ sarà l'approssimazione.
#  Tuttavia, uno script di shell, in confronto ad un linguaggio compilato che 
#+ dispone delle funzione matematiche in virgola mobile, richiede un po' di 
#+ compromessi.
#  Naturalmente, questo fatto tende a diminuire la precisione della 
#+ simulazione.


DIMENSIONE=10000 #  Lunghezza dei lati dell'appezzamento di terreno.
                 #  Imposta anche il valore massimo degli interi
                 #+ casuali generati.

MAXSPARI=1000    #  Numero delle cannonate.
                 #  Sarebbe stato meglio 10000 o più, ma avrebbe
                 #+ richiesto troppo tempo.
                 #
PMULTIPL=4.0     # Fattore di scala per approssimare PI.

genera_casuale ()
{
SEME=$(head -n 1 /dev/urandom | od -N 1 | awk '{ print $2 }')
RANDOM=$SEME                           #  Dallo script di esempio
                                       #+ "seeding-random.sh".
let "rnum = $RANDOM % $DIMENSIONE"     #  Intervallo inferiore a 10000.
echo $rnum
}

distanza=        # Dichiarazione di variabile globale.
ipotenusa ()     # Calcola l'ipotenusa di un triangolo rettangolo.
{                # Dall'esempio "alt-bc.sh".
distanza=$(bc -l <<EOF
scale = 0
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
#  Impostando "scale" a zero il risultato viene troncato (vengono eliminati i 
#+ decimali), un compromesso necessario in questo script.
#  Purtroppo. questo diminuisce la precisione della simulazione.
}


# main() {

# Inizializzazione variabili.
spari=0
splash=0
terra=0
Pi=0

while [ "$spari" -lt  "$MAXSPARI" ]       #  Ciclo principale.
do

  xCoord=$(genera_casuale)                #  Determina le
                                          #+ coordinate casuali X e Y.
  yCoord=$(genera_casuale)
  ipotenusa $xCoord $yCoord               #  Ipotenusa del triangolo
                                          #+ rettangolo = distanza.
  ((spari++))

  printf "#%4d   " $spari
  printf "Xc = %4d  " $xCoord
  printf "Yc = %4d  " $yCoord
  printf "Distanza = %5d  " $distanza     #  Distanza dal centro del lago -
                                          #+ "origine" degli assi - 
                                          #+ coordinate 0,0.

  if [ "$distanza" -le "$DIMENSIONE" ]
  then
      echo -n "SPLASH!  "
      ((splash++))
  else
      echo -n "TERRENO! "
      ((terra++))
  fi

  Pi=$(echo "scale=9; $PMULTIPL*$splash/$spari" | bc)
  # Moltiplica il rapporto per 4.0.
  echo -n "PI ~ $Pi"
  echo

done

echo
echo "Dopo $spari cannonate, $Pi sembra approssimare PI."
#  Tende ad essere un po' più alto . . .
#  Probabilmente a causa degli arrotondamenti e dell'imperfetta casualità di
#+ $RANDOM.
echo

# }

exit 0

#  Qualcuno potrebbe ben chiedersi se uno script di shell sia appropriato per 
#+ un'applicazione così complessa e ad alto impiego di risorse qual'è una
#+ simulazione.
#
#  Esistono almeno due giustificazioni.
#  1) Come prova concettuale: per dimostrare che può essere fatto.
#  2) Per prototipizzare e verificare gli algoritmi prima della
#+    riscrittura in un linguaggio compilato di alto livello.
dc

L'utility dc (desk calculator) è orientata allo stack e usa la RPN ("Reverse Polish Notation" - notazione polacca inversa). Come bc, possiede molta della potenza di un linguaggio di programmazione.

La maggior parte delle persone evita dc perché richiede un input RPN non intuitivo. Viene, comunque, ancora utilizzata.

Esempio 15-46. Convertire un numero decimale in esadecimale

#!/bin/bash
# hexconvert.sh: Converte un numero decimale in esadecimale.

E_ERR_ARG=65 # Argomento da riga di comando mancante
BASE=16      # Esadecimale.

if [ -z "$1" ]
then
  echo "Utilizzo: $0 numero"
  exit $E_ERR_ARG
  # È necessario un argomento da riga di comando.
fi
# Esercizio: aggiungete un'ulteriore verifica di validità dell'argomento.


esacvt ()
{
if [ -z "$1" ]
then
  echo 0
  return     #  "Restituisce" 0 se non è stato passato nessun argomento alla
             #+ funzione.
fi

echo ""$1" "$BASE" o p" | dc
#                 "o" imposta la radice (base numerica) dell'output.
#                   "p" visualizza la parte alta dello stack.
# Vedi 'man dc' per le altre opzioni.
return
}

esacvt "$1"

exit 0

Lo studio della pagina info di dc fornisce alcuni chiarimenti sulle sue difficoltà. Sembra esserci, comunque, un piccolo, selezionato gruppo di maghi del dc che si deliziano nel mettere in mostra la loro maestria nell'uso di questa potente, ma arcana, utility.

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

Esempio 15-47. Fattorizzazione

#!/bin/bash
# factr.sh: Fattorizza un numero

MIN=2       # Non funzionerà con con un numero inferiore a questo.
E_ERR_ARG=65
E_INFERIORE=66

if [ -z $1 ]
then
  echo "Utilizzo: $0 numero"
  exit $E_ERR_ARG
fi

if [ "$1" -lt "$MIN" ]
then
  echo "Il numero da fattorizzare deve essere $MIN o maggiore."
  exit $E_INFERIORE
fi

#  Esercizio: Aggiungete una verifica di tipo (per rifiutare un argomento 
#+ diverso da un intero).

echo "Fattori primi di $1:"
# -----------------------------------------------------------------------------
echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2"\
| dc
# -----------------------------------------------------------------------------
# La precedente riga di codice è stata scritta da Michel Charpentier
# <charpov@cs.unh.edu>.
# Usata con il permesso dell'autore (grazie).

exit 0
awk

Un altro modo ancora per eseguire calcoli in virgola mobile in uno script, è l'impiego delle funzioni matematiche built-in di awk in uno shell wrapper.

Esempio 15-48. Calcolo dell'ipotenusa di un triangolo

#!/bin/bash
# hypotenuse.sh: Calcola l'"ipotenusa" di un triangolo rettangolo.
#                (radice quadrata della somma dei quadrati dei cateti)

ARG=2                #  Lo script ha bisogno che gli vengano passati i cateti
                     #+ del triangolo.
E_ERR_ARG=65         #  Numero di argomenti errato.

if [ $# -ne "$ARG" ] # Verifica il numero degli argomenti.
then
  echo "Utilizzo: `basename $0` cateto_1 cateto_2"
  exit $E_ERR_ARG
fi


SCRIPTAWK=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
#            comando/i / parametri passati ad awk


# Ora passiamo, per mezzo di una pipe, i parametri a awk.
    echo -n "Ipotenusa di $1 e $2 = "
    echo $1 $2 | awk "$SCRIPTAWK"
#   ^^^^^^^^^^^^
# echo-e-pipe: un modo facile per passare dei parametri di shell ad awk.

exit 0

# Esercizio: riscrivete lo script  usando 'bc' al posto di awk.
#            Qual'è il metodo più intuitivo?