Calculatrice scientifique
Parenthèses, puissances, sinus, racines : une vraie calculatrice qui comprend une expression entière, sans jamais utiliser eval. Le cousin de Tanuki Arcade. Le bon exercice pour écrire son premier analyseur.
- Découper un texte en jetons
- Respecter les priorités avec une descente récursive
- Remplacer eval par une grammaire sûre
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.
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; }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(); }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); }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; }
PIÈGE 1 : Utiliser eval
Mauvais : eval(champ.value). N'importe quel code tapé dans le champ s'exécute, et les messages d'erreur sont incompréhensibles.
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
Mauvais : traiter ^ comme * dans une boucle. 2^3^2 donne 64 au lieu de 512.
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
Mauvais : appliquer le moins après la puissance sans y réfléchir, puis être surpris par -2^2.
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.
- 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.
- 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.
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.
Chargement des commentaires…