TP Java CCI

Ce TP est à destination des étudiants qui sont en avance par rapport à l'avancement du TP. Ce TP va permettre de réaliser une calculatrice, tout d'abord en notation polonaise inversée, puis en notation classique.

Créer un projet d'application Java “Calculatrice” et un package “calculatrice” dans ce projet.

Avant le cours sur les exceptions, on pourra déclencher une erreur via l'instruction suivante:

throw new RuntimeException("Un message explicatif");

Expressions

Créer un package “calculatrice.expression” et créer dedans une classe abstraite “Expression”. Dans cette classe, ajouter une méthode abstraite ayant la signature suivante:

public abstract double eval();

Étendre cette classe avec des classes “Constante”, “Oppose” et “BinOp”. Cette dernière possédera un champ de type char nommé op qui indiquera quel est l'opérateur utilisé. On considérera que les valeurs possibles pour op sont '+', '-', '*' et '/'. Créer les constructeurs correspondants et implémenter les méthodes eval().

Redéfinir la méthode toString() afin de renvoyer une chaîne de caractères représentant l'expression.

Ajouter à la classe Expression des méthodes statiques permettant de construire des expressions:

public static Expression constante(double val)
public static Expression moins(Expression e)
public static Expression moins(Expression e1, Expression e2)
public static Expression plus(Expression e1, Expression e2)
public static Expression mult(Expression e1, Expression e2)
public static Expression div(Expression e1, Expression e2)

Passer ensuite les constructeurs de Constante, Oppose et BinOp en protected afin qu'ils ne soient pas directement utilisables en dehors un package ou lors d'un héritage.

Tests pour les expressions

Afin de tester nos expressions, on va utiliser la bibliothèque JUnit. Celle-ci est intégrée à Netbeans. Se placer sur le “répertoire” Test Packages et cliquer droit → New → Other → JUnit → JUnit Test → Next. Nommer votre test “ExpressionTest”, indiquer “calculatrice.expression” comme package et décocher les trois cases. Cliquer sur Finish. Si une question est posée sur la version de JUnit, choisir la 3.x.

Un test en JUnit est une classe qui étend junit.framework.TestCase et qui possède des méthodes ayant un résultat void, pas d'arguments et dont le nom commence par “test”. La classe TestCase met à votre disposition des méthodes assertxxx() qui permettent de faire une vérification et de déclencher une erreur si la vérification échoue. Voici un exemple de test:

    public void testConstante() {
        // Vérifie que l'expression renvoyée ne vaut pas null
        assertNotNull(Expression.constante(3));
        Expression trois = Expression.constante(3);
        // Vérifie que le premier double est égal au second, avec une marge d'erreur de 0.0        
        assertEquals(3.0,trois.eval(),0.0);
        assertEquals(-3,Expression.constante(-3.0).eval(),0.0);
        assertEquals(0,Expression.constante(0).eval(),0);
        // Vérifie que l'expression booléenne passée en argument s'évalue à vrai
        assertTrue(3.0 == trois.eval());
        // Vérifie que les chaînes de caractères sont les mêmes
        assertEquals("3",trois.toString());
    }

Ajouter le test ci-dessus puis lancer l'éxécution des tests via Run → Test Project. Le test échoue. Double-cliquer sur l'échec et corriger le test. Relancer pour vérifier que le test se passe bien.

Ajouter des tests pour les autres expressions (testOppose(), testProduit(), testSomme(), etc).

Rédéfinir la méthode public boolean equals(Object o) pour les différentes Expressions et ajouter les instructions nécessaires dans les tests pour vérifier quelle a le comportement attendu.

"Parsing" d'expression

On va a présent créer une classe pour convertir des chaînes de caractères en expression. Afin de gagner un peu en abstract, on va définir une interface exprimant nos besoins:

package calculatrice.parsing;
 
import calculatrice.expression.Expression;
 
public interface ExpressionParser {
 
    public Expression parse(String input);
 
}

Rmq: le package est ici calculatrice.parsing.

Découpage

On souhaite à présent implémenter cette interface. Pour cela, il convient tout d'abord de découper la chaîne initiale en éléments représentant des bouts d'expression, typiquement les nombres et les opérateur. Pour cela, on va utiliser la classe suivante:

package calculatrice.parsing;
 
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
 * @author ecoquery
 */
public class ExpressionTokenizer {
 
    public static final byte DOUBLE = 0;
    public static final byte OP = 1;
    public static final byte END = 2;
    public static final byte NONE = 3;
    private Matcher spaces;
    private Matcher doubles;
    private Matcher ops;
    private String toParse;
    private int endOffset;
    private int offset = 0;
    private double doubleValue;
    private char op;
 
    protected ExpressionTokenizer(String ops, String toParse) {
        this.spaces = Pattern.compile("\\s").matcher(toParse);
        this.doubles = Pattern.compile("[0-9]+(\\.[0-9]+)?").matcher(toParse);
        this.ops = Pattern.compile("[" + ops + "]").matcher(toParse);
        endOffset = toParse.length();
    }
 
    public ExpressionTokenizer(boolean withParen, String toParse) {
        this(withParen ? "+\\-*/()" : "+\\-*/", toParse);
    }
 
    public ExpressionTokenizer(String toParse) {
        this(true, toParse);
    }
 
    private void moveTo(int newOffset) {
        offset = newOffset;
        try {
            spaces.region(offset, spaces.regionEnd());
            doubles.region(offset, doubles.regionEnd());
            ops.region(offset, ops.regionEnd());
        } catch (IndexOutOfBoundsException e) {
            offset = endOffset;
        }
    }
 
    public byte next() {
        if (offset >= endOffset) {
            return END;
        } else if (spaces.lookingAt()) {
            moveTo(spaces.end());
            return next();
        } else if (doubles.lookingAt()) {
            String matched = doubles.group();
            moveTo(doubles.end());
            try {
                doubleValue = Double.parseDouble(matched);
                return DOUBLE;
            } catch (NumberFormatException e) {
                return NONE;
            }
        } else if (ops.lookingAt()) {
            op = ops.group().charAt(0);
            moveTo(ops.end());
            return OP;
        } else {
            moveTo(offset+1);
            return NONE;
        }
    }
 
    public double getDouble() {
        return doubleValue;
    }
 
    public char getOp() {
        return op;
    }
 
    public static void main(String[] args) {
        // Exemple d'utilisation
        String expr = "1+2(3 24.2(-* + 5.0";
        ExpressionTokenizer et = new ExpressionTokenizer(expr);
        byte result;
        do {
            result = et.next();
            switch (result) {
                case ExpressionTokenizer.DOUBLE:
                    System.out.println(et.getDouble());
                    break;
                case ExpressionTokenizer.OP:
                    System.out.println(et.getOp());
                    break;
                case ExpressionTokenizer.END:
                    System.out.println("Fin atteinte");
                    break;
                case ExpressionTokenizer.NONE:
                    System.out.println("Erreur");
                    break;
                default:
                    throw new RuntimeException("Valeur illégale");
            }
        } while (result != ExpressionTokenizer.END);
    }
}

Notation polonaise

Dans un premier temps, on va supposer que les expressions sont écrites avec la notation polonaise: dans cette notation, les opérateurs sont indiqués avant leurs arguments, par exemple / + 1 + 2 * 3 4 5 correspond à (1 + (2 + (3 * 4))) / 5. On supposera ici que l'opérateur - ne prendra qu'un seul argument. Il est possible de reconstituer l'expression via l'algorithme récursif suivant:

  • si on lit un nombre → créer une constante
  • si on lit un - → lire une expression et l'utiliser pour construire une expression opposé
  • si on lit un opérateur binaire → lire deux expressions et les utiliser pour construire l'opération voulue.

Implémenter l'interface ExpressionParser via une classe NotationPolonaise qui utilisera un ExpressionTokenizer pour décomposer la chaîne de caractères en morceaux puis utilisera l'algorithme décrit ci-dessus pour en extraire une expression. On pourra renvoyer la valeur null en cas d'erreur dans l'expression.

Créer une classe JUnit de test pour votre classe.

Programme

Créer une classe calculatrice.Main possédant une méthode main. Dans le corps de cette méthode, on créera un BufferedReader pour lire sur l'entrée standard1) des lignes. On transformera chaque ligne en Expression via le parser NotationPolonaise et on affichera le résultat de l'évaluation en question.

Notation standard

Créer une nouvelle implémentation d'ExpressionParser qui cette fois ci lit des expressions écrites de manière standard. On peut réaliser cela via plusieurs méthodes mutuellement récursives:

  • lireSomme → lireProduit puis si le symbole suivant est + ou - lireProduit à nouveau et renvoyer la somme ou la différence correspondant, sinon renvoyer la première expression
  • lireProduit → lireBaseExpr, puis si le symbole suivant est * ou / lireBaseExpr à nouveau et renvoyer la somme ou la différence correspondant, sinon renvoyer la première expression
  • lireBaseExpr →
    • si le premier symbole est - → lireBaseExpr et renvoyer un opposé construit à partir de la sous-expression que l'on vient de lire
    • si le premier symbole est ( → lireSomme, puis vérifier que le symbole suivant est ) et renvoyer la sous-expression obtenue
    • sinon on a une constante

On peut remarquer que cet alogorithme ne gère pas les enchaînements de + et -, ni de * et /.

Ecrire des tests pour cette nouvelle classe et changer le programme pour utiliser cette notation.

1)
i.e. au clavier