:: Enseignements :: ESIPE :: E4INFO :: 2020-2021 :: Java Avancé ::
[LOGO]

A table !


Ce TP a pour but de modéliser la structure de données sous-jacente à un tableur comme LibreOffice Calc ou Microsoft Excel.
Historiquement, un des problèmes des tableurs comme Lotus 1-2-3 ou Microsoft Excel était qu'à l'époque les machines avait relativement peu de mémoire. Or ces logiciels consommaient beaucoup de mémoire si toutes les cases du tableau contenaient des formules de calcul. Pour éviter la consommation excessive de mémoire, les tableurs utilisent une astuce qui permet de réutiliser la même formule pour plusieurs cellules en stockant les références aux autres cases dans la formule de façon relative à la position de la case contenant la formule.
Par exemple, si la case B2 contient la formule C13 + 7, la formule va être stockée comme Shift(11, 1) + 7 avec Shift(11, 1) voulant dire "la case qui se trouve 11 lignes plus bas et 1 colonne plus à droite que celle contenant cette formule".
Lors de l'évaluation de la formule Shift(11, 1) + 7, on a besoin de connaître la case contenant la formule (dans notre exemple, c'est B2) et Shift(11, 1) est résolue comme la case décalée de 11 lignes et de 1 colonne par rapport à B2, donc la case C13.
Si maintenant on stocke la même formule Shift(11, 1) + 7 dans B3, dans ce cas, la formule est évaluée comme la référence "11 lignes plus bas et 1 colonne plus à droite" par rapport à B3 donc C14.
Ainsi, lorsque que l'on copie-colle ou lisse une formule de calcul sur plusieurs lignes (ou colonnes), le logiciel stocke exactement la même formule (le même pointeur) dans les différentes cases.

Pour simplifier le problème nous allons supposer que :
  • Une case est identifiée par une chaîne de caractères comme "A1", "Z350" ou "B5000" par exemple. La lettre indique la valeur de la colonne, le nombre indique la valeur de la ligne.
    Nous allons utiliser un record Cell pour représenter une cellule et un record Shift pour représenter le décalage par rapport à une cellule.
  • On va demander à l'utilisateur de fournir les formules en notation préfixe, donc + B3 3 plutôt que B3 + 3, car on n'est pas dans un TP de compilation.
    De plus, on se limitera aux entiers à virgule flottante (double) et aux opérateurs (+, -, *, /) en plus des références sur les cellules. Donc le résultat d'un calcul est toujours un entier à virgule flottante.
  • Pour éviter les limitations sur le nombre de lignes et de colonnes, les cases sont stockées dans une table de hachage qui associe à une Cell la fonction qu'elle contient.

La tableur est représenté par la classe Calc dans le package fr.umlv.newxl. Elle possède les méthodes suivantes :
  • set qui prend en paramètre une référence sur une cellule et une formule et stocke la formule dans la cellule indiquée.
  • eval qui prend en paramètre une référence sur une cellule et évalue la formule de celle-ci.
  • setAll qui prend en paramètre une zone contiguë de cellules (définie à partir d'une cellule, d'une direction et d'un nombre de cellules) et une formule et se comporte comme si la formule était copiée/collée de cellules en cellules.

Exercice 1 - Calc

Les tests JUnit 5 pour toutes les questions du TP sont dans la classe CalcTest.java.

  1. Dans une classe Calc, on définit deux records Cell et Shift qui correspondent respectivement aux coordonnées (row et column) d'une cellule et au décalage (dx et dy) par rapport à une cellule.
      public class Calc {
        public record Shift(int dx, int dy) { }
        public record Cell(int row, int column) { }
      }
         

    Dans un premier temps, faites en sorte que l'on ne puisse pas créer de Cell invalide.
    Puis, créer une méthode toString qui affiche la Cell sous la forme A12, Z45, etc..., si le numéro de colonne est entre 0 et 25 (oui on commence à 0) et sous la forme "Cell(row,column)" sinon.
    Vérifier que les tests marqués "Q1" passent.

  2. On va avoir besoin de convertir une chaîne de caractères comme A12, Z45 (où le nom de colonne est une lettre) en Cell pour pouvoir parser la formule. Pour cela, on va créer une méthode parse dans Cell qui prend une formule (c'est à dire une chaîne de caractères) et si cette dernière correspond à un nom de cellule, renvoie la Cell correspondante.
    Comme une formule, même valide, peut être autre chose qu'un nom de cellule, la méthode doit pouvoir indiquer que la chaîne de caractères n'est pas une référence sur une cellule. Quelle doit donc être la signature de la méthode parse ?
    Écrire la méthode parse dans Cell. Vérifier que les tests marqués "Q2" passent.

  3. La formule rentrée par un utilisateur va contenir des cellules (Cell) qu'il va falloir transformer en décalage par rapport à une cellule (Shift). Et lors de l'évaluation d'une formule, il va falloir faire l'opération inverse.
    Pour cela, écrire dans la classe Cell deux méthodes
    • relativize qui prend une Cell en paramètre et renvoie un Shift qui est le décalage de la cellule courante par rapport à celle prise en paramètre.
      Par exemple, si la cellule courante est Cell(5, 29), son décalage par rapport à Cell(1, 23) est Shift(4, 6).
    • resolve qui prend un Shift en paramètre, et renvoie la cellule obtenue par décalage de la cellule courante avec ce Shift.
      Par exemple, si la cellule courante est Cell(3, 30), avec un Shift(dx = 1, dy = -3), on obtient la cellule Cell(4, 27).
    Vérifier que les tests marqués "Q3" passent.

  4. Maintenant, à partir d'une formule contenue dans une cellule, on souhaite fabriquer le calcul correspondant, pour pouvoir évaluer la formule ensuite. Pour cela, on va déclarer une interface Computation qui permet de décrire le calcul que l'on doit faire pour évaluer un cellule. On rappelle qu'une formule doit pouvoir contenir une référence relative à une autre cellule. Quelle est la nature de cette interface ? Quelle est la signature de sa méthode abstraite ? Déclarer l' interface Computation qui permet de représenter un tel calcul.
    Discuter de la visibilité de Computation.

    Pour évaluer une formule (en notation préfixe), vous savez qu'il est possible de la transformer en un arbre d'expression que l'on peut ensuite évaluer récursivement. On pourrait donc écrire des classes Value, Add, Sub, ... représentant respectivement les constantes, l'opération +, l'opération -, etc... ayant toutes pour interface commune Computation et permettant de construire de tels arbres. Mais en réalité, puisque l'on sait manipuler un calcul directement, on n'a pas vraiment besoin de construire des arbres, on peut se contenter de conserver sa trace dans l'arbre d'appel des calcul (la bonne nouvelle, c'est que ça ne change rien au principe l'algo).
    Comment doit-on faire pour représenter les constantes et les opérateurs (+, -, * , /) comme des Computation, sans définir de nouvelles classes (ou records) ?

    Pour simplifier un peu, pour l'instant, on ne va pas s'occuper des références sur les cellules, ni des opérateurs -, * et /. Écrire dans Calc une méthode parse qui prend en paramètre une cellule (celle contenant la formule) et une formule sous forme de chaîne de caractères composée de constantes et d'opérateurs + séparés par des espaces, en notation préfixe (par exemple "+ 2 + 4 4") et transforme celle-ci en (arbre de) Computation.

    Note : voici un lien sur StackOverflow pour vous aider avec l'algo, si vous ne vous souvenez pas comment faire pour parser des expressions en notation préfixe : https://stackoverflow.com/questions/5307218/prefix-notation-parsing-in-python.

    Note 2 : dans le cas où la formule contient des valeurs qui ne sont ni l'opérateur +, ni une constante ou dans la cas où l'expression en paramètre n'est pas un arbre (par exemple "+ + 2 2"), on renverra une runtime exception pour indiquer que la formule n'est pas valide.
    Vérifier que les tests marqués "Q4" passent.

  5. Pour gérer les autres opérateurs que +, on se propose de créer une Map qui associe à la chaîne de caractères correspondant à cet opérateur, une fonction à deux arguments qui effectue l'opération correspondante.
        private static final ??? OPERATOR_MAP =
          Map.of("+", (a, b) -> a + b, "-", (a, b) -> a - b, "*", (a, b) -> a * b, "/", (a, b) -> a / b);
         

    Dans un premier temps, quel est le type de ??? pour que le code marche ?
    Modifier le code de la méthode parse écrite à la question précédente pour gérer les 4 opérateurs.
    Vérifier que les tests marqués "Q5" passent.

  6. Dans un premier temps, écrire dans Calc
    • une méthode set qui prend en paramètre une référence sur une cellule et une formule et assigne la formule (en fait la Computation) à la case correspondante et
    • une méthode eval qui prend en paramètre une cellule et renvoie soit la valeur de la formule de celle-ci, soit que la cellule n'a pas de valeur.
    • Indice : quelle est la structure de données qui permet naturellement d'associer un objet à une clé unique ?

    Puis ajouter à parse la gestion des références aux cellules sachant que la Computation calculée doit stocker un décalage, un Shift, (en utilisant la méthode relativize) relatif à la cellule prise en paramètre de la méthode parse. Le contenu de la cellule cible pourra alors être retrouvé grâce à la méthode resolve.
    Dans le cas où la cellule contient une référence sur une cellule vide, au lieu de planter, on considérera que la valeur d'une cellule non existante est 0.0.
    Vérifier que les tests marqués "Q6" passent.

  7. Enfin, on souhaite écrire une méthode setAll qui prend en paramètre une référence sur une cellule, une direction (cf SpanDirection ci-dessous), une longueur et une chaîne de caractères correspondant à une formule et simule un utilisateur qui sélectionne un ensemble de cellules (définies part une cellule de début, une direction, et le nombre de cellule sélectionnées) et leur applique la même formule (pour l'utilisateur, ça correspond à un copier/coller).
    Par exemple, avec le tableur suivant :
           | A | B | C | D | E |
        ------------------------
         0 |   |   |   |   |   |
         1 | 0 | 1 | 2 | 3 | 4 |
         2 |   |   |   |   |   |
      
    Si on appelle setAll(c0, SpanDirection.EAST, 4, "A1"), celà correspond à mettre la valeur de A1 dans C0, la valeurs de B1 dans D0, la valeur de C1 dans E0 et la valeur de E1 dans F0. Ce qui correspond aux valeurs suivantes dans le tableur :
           | A | B | C | D | E | F |
        ----------------------------
         0 |   |   | 0 | 1 | 2 | 3 | 
         1 | 0 | 1 | 2 | 3 | 4 |   |
         2 |   |   |   |   |   |   |
      

    Écrire la méthode setAll.
    Vérifier que les tests marqués "Q7" passent.
      public enum SpanDirection {
        NORTH, EAST, SOUTH, WEST
      }
         

  8. ( Optionnel ) Pour les plus balèzes, on souhaite détecter les cycles, par exemple, si la case A3 contient une référence sur B4 qui contient elle-même une référence sur A3, on a un problème lors de l'évaluation.
    Quel est le problème ?
    Comment faire pour détecter que l'on a un cycle ?
    Implanter votre solution sachant que en plus, les tests doivent continuer à tous fonctionner donc il faut que Computation reste backward compatible.
    Vérifier que les tests marqués "Q8" passent.