15.8. Commandes mathématiques

« Compter »

factor

Décompose un entier en nombre premiers.

bash$ factor 27417
 27417: 3 13 19 37
 
bc

Bash ne peut traiter les calculs en virgule flottante et n'intègre pas certaines fonctions mathématiques importantes. Heureusement, bc est là pour nous sauver.

bc n'est pas simplement une calculatrice souple à précision arbitraire, elle offre aussi beaucoup de facilités disponibles habituellement dans un langage de programmation.

La syntaxe de bc ressemble vaguement à celle du C.

bc est devenu un outil UNIX assez puissant pour être utilisé via un tube et est manipulable dans des scripts.

Ceci est un simple exemple utilisant bc pour calculer la valeur d'une variable. Il utilise la substitution de commande.

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

Exemple 15.46. Paiement mensuel sur une hypothèque

#!/bin/bash
# monthlypmt.sh : Calcule le paiement mensuel d'une hypothèque.


#  C'est une modification du code du paquetage "mcalc" (mortgage calculator,
#+ c'est-à-dire calcul d'hypothèque), de Jeff Schmidt et Mendel Cooper
#+ (l'auteur du guide ABS).
#   http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz  [15k]

echo
echo "Étant donné le montant principal, le taux d'intérêt et la fin de l'hypothèque,"
echo "calcule le paiement mensuel."

bas=1.0

echo
echo -n "Entrez le montant principal (sans virgule) "
read principal
echo -n "Entrez le taux d'intérêt (pourcentage) "
  # Si 12%, entrez "12" et non pas ".12".
read taux_interet
echo -n "Entrez le nombre de mois "
read nb_mois


 taux_interet=$(echo "scale=9; $taux_interet/100.0" | bc) # Convertit en décimal
                 #             ^^^^^^^^^^^^^^^^^^^ Diviser par 100.
                 # "scale" détermine le nombre de décimales.

 taux_interet_tmp=$(echo "scale=9; $taux_interet/12 + 1.0" | bc)
 

 top=$(echo "scale=9; $principal*$taux_interet_tmp^$nb_mois" | bc)
                 #    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                 #    Formule standard pour calculer un intérêt.

 echo; echo "Merci d'être patient. Ceci peut prendre longtemps."

 let "mois = $nb_mois - 1"
# ==================================================================== 
 for ((x=$mois; x > 0; x--))
 do
   bot=$(echo "scale=9; $taux_interet_tmp^$x" | bc)
   bas=$(echo "scale=9; $bas+$bot" | bc)
#  bas = $(($bas + $bot"))
 done
# ==================================================================== 

# -------------------------------------------------------------------- 
#  Rick Boivie indique une implémentation plus efficace que la boucle
#+ ci-dessus, ce qui réduit le temps de calcul de 2/3.

# for ((x=1; x <= $mois; x++))
# do
#   bas=$(echo "scale=9; $bas * $taux_interet_tmp + 1" | bc)
# done


#  Puis, il est revenu avec une alternative encore plus efficace,
#+ car elle descend le temps d'exécution de 95%!

# bas=`{
#     echo "scale=9; bas=$bas; taux_interet_tmp=$taux_interet_tmp"
#     for ((x=1; x <= $mois; x++))
#     do
#          echo 'bas = bas * taux_interet_tmp + 1'
#     done
#     echo 'bas'
#     } | bc`       # Intègre une 'boucle for' dans la substitution de commande.

# --------------------------------------------------------------------------
#  D'un autre côté, Frank Wang suggère :
#  bottom=$(echo "scale=9; ($interest_rate^$term-1)/($interest_rate-1)" | bc)

#  Car
#  L'algorithme de la boucle est une somme de série géométrique de proportion.
#  La formule de la somme est e0(1-q^n)/(1-q),
#+ où e0 est le premier élément et q=e(n+1)/e(n)
#+ et n est le nombre d'éléments.
# --------------------------------------------------------------------------

 # let "paiement = $top/$bas"
 paiement=$(echo "scale=2; $top/$bas" | bc)
 # Utilise deux décimales pour les dollars et les cents.
 
 echo
 echo "paiement mensuel = \$$paiement"
    # Affiche un signe dollar devant le montant.
 echo


 exit 0

 #  Exercices :
 #    1) Filtrez l'entrée pour permettre la saisie de virgule dans le montant.
 #    2) Filtrez l'entrée pour permettre la saisie du taux d'intérêt en
 #+      pourcentage ou en décimale.
 #    3) Si vous êtes vraiment ambitieux, étendez ce script pour afficher
 #+      les tables d'amortissement complètes.

Exemple 15.47. Conversion de base

#!/bin/bash
################################################################################
# Script shell: base.sh - affiche un nombre en différentes bases (Bourne Shell)
# Auteur      : Heiner Steven (heiner.steven@odn.de)
# Date        : 07-03-95
# Catégorie   : Desktop
# $Id: base.sh,v 1.10 2008-05-10 08:36:11 gleu Exp $
# ==> La ligne ci-dessus est l'information ID de RCS.
################################################################################
# Description
#
# Modifications
# 21-03-95 stv  correction d'une erreur arrivant avec 0xb comme entrée (0.2)
################################################################################

# ==> Utilisé dans le guide ABS avec la permission de l'auteur du script.
# ==> Commentaires ajoutés par l'auteur du guide ABS.

NOARGS=65
PN=`basename "$0"`                                 # Nom du programme
VER=`echo '$Revision: 1.10 $' | cut -d' ' -f2`  # ==> VER=1.6

Usage () {
    echo "$PN - Affiche un nombre en différentes bases, $VER (stv '95)
usage: $PN [nombre ...]

Si aucun nombre n'est donné, les nombres sont lus depuis l'entrée standard.
Un nombre peut être
    binaire (base 2)            commençant avec 0b (i.e. 0b1100)
    octal (base 8)              commençant avec 0  (i.e. 014)
    hexadécimal (base 16)       commençant avec 0x (i.e. 0xc)
    décimal                     autrement (c'est-à-dire 12)" >&2
    exit $NOARGS 
}   # ==> Fonction pour afficher le message d'usage.

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

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

AfficheBases () {
    # Détermine la base du nombre
    for i      # ==> [liste] manquante...
    do         # ==> donc opère avec le(s) argument(s) en ligne de commande.
        case "$i" in
            0b*)                ibase=2;;       # binaire
            0x*|[a-f]*|[A-F]*)  ibase=16;;      # hexadécimal
            0*)                 ibase=8;;       # octal
            [1-9]*)             ibase=10;;      # décimal
            *)
                Msg "nombre illégal $i - ignoré"
                continue;;
        esac

        #  Suppression du préfixe, conversion des nombres hexadécimaux en
        #+ majuscule (bc a besoin de cela)
        number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
        # ==> Utilise ":" comme séparateur sed, plutôt que "/".

        # Conversion des nombres en décimal
        dec=`echo "ibase=$ibase; $number" | bc`  # ==> 'bc' est un utilitaire de
                                                 #+ calcul.
        case "$dec" in
            [0-9]*)     ;;                       # nombre ok
            *)          continue;;               # erreur: ignore
        esac

        # Affiche toutes les conversions sur une ligne.
        # ==> le 'document en ligne' remplit la liste de commandes de '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 ]
# ==>  est une "boucle while" réellement nécessaire
# ==>+ car tous les cas soit sortent de la boucle
# ==>+ soit terminent le script.
# ==> (commentaire de Paulo Marcel Coelho Aragao.)
do
    case "$1" in
        --)     shift; break;;
        -h)     Usage;;                 # ==> Message d'aide.
        -*)     Usage;;
        *)      break;;                 # premier nombre
    esac   #  ==> Plus de vérification d'erreur pour des entrées illégales
           #+ serait utile.
    shift
done

if [ $# -gt 0 ]
then
    AfficheBases "$@"
else                                    #  lit à partir de l'entrée standard
                                        #+ stdin
    while read ligne
    do
        PrintBases $ligne
    done
fi

exit 0

Une autre façon d'utiliser bc est d'utiliser des documents en ligne embarqués dans un bloc de substitution de commandes. Ceci est très intéressant lorsque le script passe un grand nombre d'options et de commandes à bc

variable=`bc >> CHAINE_LIMITE
 options
 instructions
 operations
 CHAINE_LIMITE
 `

 ...or...


 variable=$(bc >> CHAINE_LIMITE
   options
   instructions
   operations
   CHAINE_LIMITE
   )

Exemple 15.48. Appeler bc en utilisant un document en ligne

#!/bin/bash
# Appelle 'bc' en utilisant la substitution de commandes
# en combinaison avec un 'document en ligne'.


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


# La notation $( ... ) fonctionne aussi.
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
)
# Renvoie le sinus de 1,7 radians.
# L'option "-l" appelle la bibliothèque mathématique de 'bc'.
echo $var3       # .991664810


# Maintenant, essayez-la dans une fonction...
hypotenuse ()    # Calculez l'hypoténuse d'un triangle à angle droit.
{                # c = sqrt( a^2 + b^2 )
hyp=$(bc -l << EOF
scale = 9
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
# Can't directly return floating point values from a Bash function.
# But, can echo-and-capture:
echo "$hyp"
}

hyp=$(hypotenuse 3.68 7.31)
echo "hypoténuse = $hyp"    # 8.184039344


exit 0

Exemple 15.49. Calculer PI

#!/bin/bash
# cannon.sh: Approximation de PI en tirant des balles de canon.

# C'est une très simple instance de la simulation "Monte Carlo" : un modèle
#+ mathématique d'un événement réel, en utilisant des nombres pseudo-aléatoires
#+ pour émuler la chance.

#  Considérez un terrain parfaitement carré, de 10000 unités par côté.
#  Ce terrain comprend un lac parfaitement circulaire en son centre d'un
#+ diamètre de 10000 unités.
#  Ce terrain ne comprend pratiquement que de l'eau mais aussi un peu de
#+ terre dans ses quatre coins.
#  (pensez-y comme un carré comprenant un cercle.)
#
#  Nous tirons des balles de canon à partir d'un vieux canon situé sur un des côtés
#+ du terrain.
#  Tous les tirs créent des impacts quelque part sur le carré, soit dans le
#+ lac soit dans un des coins secs.
#  Comme le lac prend la majorité de l'espace disponible, la
#+ plupart des tirs va tomber dans l'eau.
#  Seuls quelques tirs tomberont sur un sol rigide compris dans les quatre coins
#+ du carré.
#
#  Si nous prenons assez de tirs non visés et au hasard,
#+ alors le ratio des coups dans l'eau par rapport au nombre total sera
#+ approximativement de PI/4.
#
#  La raison de ceci est que le canon ne tire réellement que dans la partie
#+ haute à droite du carré, premier quadrant des coordonnées cartésiennes.
#  (La précédente explication était une simplification.)
#
#  Théoriquement, plus de tirs sont réalisés, plus cela correspondra.
#  Néanmoins, un script shell, contrairement à un langage compilé avec un
#+ support des calculs à virgule flottante, nécessite quelques compromis.
#  Ceci tend à rendre la simulation moins précise.


DIMENSION=10000  # Longueur de chaque côté.
                 # Initialise aussi le nombre d'entiers générés au hasard.

NB_TIRS_MAX=1000 # Tire ce nombre de fois.
                 # 10000 ou plus serait mieux mais prendrait bien plus de temps.
PMULTIPLIEUR=4.0 # Facteur d'échelle pour l'approximation de PI.

M_PI=3.141592654 # Valeur réelle de PI, dans un but de comparaison.

au_hasard ()
{
RECHERCHE=$(head -n 1 /dev/urandom | od -N 1 | awk '{ print $2 }')
HASARD=$RECHERCHE                    # Du script d'exemple "seeding-random.sh"
let "rnum = $HASARD % $DIMENSION"    # Echelle plus petite que 10000.
echo $rnum
}

distance=        # Déclaration de la variable globale.
hypotenuse ()    # Calcule de l'hypoténuse d'un triangle à angle droit.
{                # A partir de l'exemple "alt-bc.sh".
distance=$(bc -l << EOF
scale = 0
sqrt ( $1 * $1 + $2 * $2 )
EOF
)
#  Initiale "scale" à zéro fait que le résultat sera une valeur entière, un
#+ compris nécessaire dans ce script.
#  Ceci diminue l'exactitude de la simulation.
}


# main() {

# Initialisation des variables.
tirs=0
dans_l_eau=0
sur_terre=0
Pi=0
error=0

while [ "$tirs" -lt  "$NB_TIRS_MAX" ]        # Boucle principale.
do

  xCoord=$(au_hasard)                        # Obtenir les coordonnées X et Y au
                                             # hasard.
  yCoord=$(au_hasard)
  hypotenuse $xCoord $yCoord                 #  Hypoténuse du triangle rectangle =
                                             #+ distance.
  ((tirs++))

  printf "#%4d   " $tirs
  printf "Xc = %4d  " $xCoord
  printf "Yc = %4d  " $yCoord
  printf "Distance = %5d  " $distance         #  Distance à partir du centre
                                              #+ du lac --
                                              #  l'"origine" --
                                              #+ coordonnées (0,0).

  if [ "$distance" -le "$DIMENSION" ]
  then
    echo -n "Dans l'eau !  "
    ((dans_l_eau++))
  else
    echo -n "Sur terre !   "
    ((sur_terre++))
  fi

  Pi=$(echo "scale=9; $PMULTIPLIEUR*$dans_l_eau/$tirs" | bc)
  # Multipliez le ratio par 4.0.
  echo -n "PI ~ $Pi"
  echo

done

echo
echo "Après $tirs tirs, PI ressemble approximativement à $Pi"
# Tend à être supérieur.
# Probablement dû aux erreurs d'arrondi et au hasard perfectible de $RANDOM.
error=$(echo "scale=9; $Pi - $M_PI" | bc)
echo "Deviation de la valeur mathématique de PI =      $error"
echo

# }

exit 0

#  On peut se demander si un script shell est approprié pour une application
#+ aussi complexe et aussi intensive en calcul.
#
#  Il existe au moins deux justifications.
#  1) La preuve du concept: pour montrer que cela est possible.
#  2) Pour réaliser un prototype et tester les algorithmes avant de le réécrire
#+    dans un langage compilé de haut niveau.

dc

L'utilitaire dc (desk calculator) utilise l'empilement et la « notation polonaise inversée » (RPN). Comme bc, il possède les bases d'un langage de programmation.

La plupart des gens évitent dc, parce qu'il nécessite de saisir les entrées en RPN, ce qui n'est pas très intuitif. Toutefois, cette commande garde son utilité.

Exemple 15.50. Convertir une valeur décimale en hexadécimal

#!/bin/bash
# hexconvert.sh : Convertit un nombre décimal en hexadécimal.

E_SANSARGS=65 # Arguments manquants sur la ligne de commande.
BASE=16       # Hexadécimal.

if [ -z "$1" ]
then
  echo "Usage: $0 nombre"
  exit $E_SANSARGS
  # A besoin d'un argument en ligne de commande.
fi
# Exercice : ajouter une vérification de la validité de l'argument.


hexcvt ()
{
if [ -z "$1" ]
then
  echo 0
  return    # "Renvoie" 0 si aucun argument n'est passé à la fonction.
fi

echo ""$1" "$BASE" o p" | dc
#                   "o" demande une sortie en base numérique.
#                   "p" Affiche le haut de la pile.
# Voir 'man dc' pour plus d'options.
return
}

hexcvt "$1"

exit 0

L'étude de la page info de la commande dc est un moyen pénible de prendre conscience de sa complexité. Il semble cependant qu'une poignée de connaisseurs de dc se délectent de pouvoir exiber leur maîtrise de cet outil puissant mais mystérieux.

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

Exemple 15.51. Factorisation

#!/bin/bash
# factr.sh : Factorise un nombre

MIN=2       # Ne fonctionnera pas pour des nombres plus petits que celui-ci.
E_SANSARGS=65
E_TROPPETIT=66

if [ -z $1 ]
then
  echo "Usage: $0 nombre"
  exit $E_SANSARGS
fi

if [ "$1" -lt "$MIN" ]
then
  echo "Le nombre à factoriser doit être supérieur ou égal à $MIN."
  exit $E_TROPPETIT
fi  

#  Exercice : Ajouter une vérification du type (pour rejeter les arguments non
#+ entiers).

echo "Les facteurs de $1 :"
# -------------------------------------------------------------------------------
echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2"|dc
# -------------------------------------------------------------------------------
# La ligne de code ci-dessus a été écrite par Michel Charpentier &lt;charpov@cs.unh.edu&gt;.
# Utilisé dans le guide ABS avec sa permission (merci).

 exit 0

awk

Une autre façon d'utiliser les nombres à virgule flottante est l'utilisation des fonctions internes de la commande awk dans un emballage shell .

Exemple 15.52. Calculer l'hypoténuse d'un triangle

#!/bin/bash
# hypotenuse.sh : Renvoie l'"hypoténuse" d'un triangle à angle droit,
#                (racine carrée de la somme des carrés des côtés)

ARGS=2                # Le script a besoin des côtés du triangle.
E_MAUVAISARGS=65          # Mauvais nombre d'arguments.

if [ $# -ne "$ARGS" ] # Teste le nombre d'arguments du script.
then
  echo "Usage: `basename $0` cote_1 cote_2"
  exit $E_MAUVAISARGS
fi


SCRIPTAWK=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
#            commande(s) / paramètres passés à awk


# Maintenant, envoyez les paramètres à awk via un tube.
    echo -n "Hypoténuse de $1 et $2 = "
    echo $1 $2 | awk "$SCRIPTAWK"
#   ^^^^^^^^^^^^
# echo et pipe sont un moyen facile de passer des paramètres shell.

exit 0

# Exercice : Ré-écrivez ce script en utilisant 'bc' à la place de awk.
#            Quelle méthode est la plus intuitive ?