Skip to content
← 1001 Widgets
N° 031 · ●●●

Scientific calculator

Parentheses, powers, sines, roots: a real calculator that understands a whole expression, without ever using eval. A cousin of Tanuki Arcade. The right exercise to write your first parser.

JS
What you'll learn
  • Split text into tokens
  • Respect precedence with recursive descent
  • Replace eval with a safe grammar
Why this language

Pourquoi une calculatrice scientifique ? La calculatrice simple calcule au fil des touches. Ici, on tape 2^10 + sqrt(16) * (3 - 1) et il faut respecter les priorités : puissance, puis multiplication, puis addition. C'est le travail d'un analyseur.

La méthode s'appelle la descente récursive. On découpe d'abord le texte en jetons : nombres, opérateurs, noms. Puis une fonction par niveau de priorité appelle la suivante. Les parenthèses relancent tout depuis le haut.

Pourquoi pas eval ? eval exécuterait n'importe quel code tapé dans le champ. Un analyseur ne comprend que ce qu'on lui a appris, et peut expliquer une erreur.

How it works
  1. LES JETONS

    On transforme le texte en une liste : nombres, noms et opérateurs.

    function jetons(s) {
      const t = [];
      let i = 0, m;
      while (i < s.length) {
        if (/\s/.test(s[i])) { i++; continue; }
        if ((m = /^\d+(\.\d+)?/.exec(s.slice(i)))) { t.push({ n: Number(m[0]) }); i += m[0].length; continue; }
        if ((m = /^[a-z]+/i.exec(s.slice(i)))) { t.push({ id: m[0] }); i += m[0].length; continue; }
        if ('+-*/^()'.includes(s[i])) { t.push({ op: s[i++] }); continue; }
        throw new Error('Caractère inattendu : ' + s[i]);
      }
      return t;
    }
  2. LA GRAMMAIRE

    Un niveau de priorité par fonction : expr appelle terme, terme appelle facteur.

    function evaluer(texte) {
      const t = jetons(texte);
      let p = 0;
      const voir = o => t[p] && t[p].op === o;
    
      function expr() {          // + et -
        let v = terme();
        while (voir('+') || voir('-')) v = t[p++].op === '+' ? v + terme() : v - terme();
        return v;
      }
      function terme() {         // * et /
        let v = facteur();
        while (voir('*') || voir('/')) v = t[p++].op === '*' ? v * facteur() : v / facteur();
        return v;
      }
      function facteur() {       // ^ se lit de droite à gauche : 2^3^2 = 2^9
        const base = unaire();
        if (voir('^')) { p++; return base ** facteur(); }
        return base;
      }
      function unaire() {        // le moins devant un nombre
        if (voir('-')) { p++; return -unaire(); }
        return primaire();
      }
  3. LES ÉLÉMENTS DE BASE

    Un nombre, une constante, une fonction ou une expression entre parenthèses.

      function primaire() {
        const k = t[p++];
        if (!k) throw new Error('Expression incomplète');
        if ('n' in k) return k.n;
        if (k.id === 'pi') return Math.PI;
        if (k.id) {
          const f = { sin: Math.sin, cos: Math.cos, sqrt: Math.sqrt, ln: Math.log }[k.id];
          if (!f || !voir('(')) throw new Error('Fonction inconnue : ' + k.id);
          p++; const v = expr(); p++;   // saute ( et )
          return f(v);
        }
        if (k.op === '(') { const v = expr(); p++; return v; }
        throw new Error('Inattendu : ' + k.op);
      }
  4. TOUT LIRE

    S'il reste des jetons après l'expression, c'est une erreur de saisie.

      const r = expr();
      if (p < t.length) throw new Error('Expression mal formée');
      return r;
    }
Pitfalls

PIÈGE 1 : Utiliser eval

Pitfall

Mauvais : eval(champ.value). N'importe quel code tapé dans le champ s'exécute, et les messages d'erreur sont incompréhensibles.

Fix

Correct : un analyseur qui ne connaît que les nombres, les opérateurs et une liste fermée de fonctions.

PIÈGE 2 : La puissance de gauche à droite

Pitfall

Mauvais : traiter ^ comme * dans une boucle. 2^3^2 donne 64 au lieu de 512.

Fix

Correct : facteur s'appelle lui-même à droite du ^, ce qui rend l'opérateur associatif à droite.

PIÈGE 3 : Le moins unaire avant la puissance

Pitfall

Mauvais : appliquer le moins après la puissance sans y réfléchir, puis être surpris par -2^2.

Fix

Correct : choisir une règle, la documenter, et la tester. Ici, le moins s'applique au nombre : -2^2 vaut 4, comme sur beaucoup de calculatrices de poche.

PERSONNALISER
  • La multiplication implicite : 2pi ou 3(4+1), en ajoutant un * entre deux jetons qui se suivent.
  • Des variables : x = 5, puis 2x + 1.
  • Un historique des calculs, à rappeler avec la flèche du haut.
OPTIMISATION
  • Pour tracer des courbes, analysez l'expression une fois en arbre, puis évaluez l'arbre pour chaque x.
  • Arrondissez l'affichage à 12 chiffres significatifs : 0,1 + 0,2 s'affichera 0,3.
VERS REACT

L'analyseur vit dans un fichier pur, sans React, facile à tester.

L'expression est un useState, le résultat se calcule au rendu dans un try et catch.

Les touches du clavier virtuel insèrent à la position du curseur grâce à un useRef sur le champ.

Comments
0/1000

Loading comments…

Audience measurement

With your consent, this site measures its traffic with Google Analytics and the Meta pixel, via Google Tag Manager. Nothing is loaded until you choose, and you can change your mind at any time with the "Cookies" link at the bottom of the page.