Projeto Calculadora - Parte 4: Implementando a Lógica com JavaScript

Série “Projeto Calculadora” — 4 partes, do planejamento ao código funcionando: 1. Requisitos e funcionalidades · 2. Estrutura HTML · 3. Estilização com CSS · 4. Lógica com JavaScript (você está aqui)
Introdução
Com o HTML da parte 2 e o CSS da parte 3 prontos, chegou a hora de implementar a lógica em JavaScript. Vamos revisitar os casos de borda que definimos na parte 1 — divisão por zero, operadores em sequência, múltiplos pontos decimais — e ver exatamente onde cada um é tratado no código.
Passo 1: Referenciando o Display
Como definimos na parte 2, o display agora é uma <div id="display" role="status">, não um <input>. Por isso manipulamos textContent em vez de .value:
const display = document.getElementById('display');
let expression = '0';
function updateDisplay() {
display.textContent = expression;
}
A variável expression guarda o estado atual da calculadora como string (ex.: "12 + 5"), e updateDisplay() sincroniza essa string com o que aparece na tela — incluindo o anúncio automático para leitores de tela, graças ao aria-live="polite" que já está no HTML.
Passo 2: Adicionando Números ao Display
function appendNumber(number) {
if (expression === '0') {
expression = number;
} else {
expression += number;
}
updateDisplay();
}
Diferente de simplesmente concatenar o número (display.value += number), verificamos primeiro se a expressão atual é '0' — o valor inicial. Sem essa checagem, clicar em “5” logo ao abrir a calculadora resultaria em "05" em vez de "5".
Edge case da parte 1 — múltiplos pontos decimais (1.2.3): para bloquear um segundo ponto no mesmo número, verificamos se o último “token” numérico da expressão já contém um ponto antes de aceitar outro:
function appendDecimal() {
const lastNumber = expression.split(/[+\-*/]/).pop().trim();
if (!lastNumber.includes('.')) {
expression += expression === '0' ? '0.' : '.';
updateDisplay();
}
}
Associe essa função a um botão de ponto decimal (.) na sua interface, seguindo o mesmo padrão de onclick="appendDecimal()" dos demais botões.
Passo 3: Adicionando Operadores ao Display
function appendOperator(operator) {
const trimmed = expression.trim();
const lastChar = trimmed.slice(-1);
if ('+-*/'.includes(lastChar)) {
// Edge case: operadores em sequência (ex.: "5 + + 3")
// substitui o operador anterior pelo mais recente, em vez de acumular os dois
expression = trimmed.slice(0, -1) + operator + ' ';
} else {
expression = trimmed + ' ' + operator + ' ';
}
updateDisplay();
}
Isso resolve diretamente o segundo caso de borda da parte 1: se o usuário pressionar dois operadores seguidos, o mais recente vence, em vez de gerar uma expressão inválida como "5 + + 3".
Passo 4: Limpando o Display
function clearDisplay() {
expression = '0';
updateDisplay();
}
Repare que resetamos expression para '0' (o estado inicial), e não para uma string vazia — assim o display sempre mostra algo, igual a uma calculadora física.
Passo 5: Calculando o Resultado
function calculateResult() {
try {
const result = parseExpression(expression);
if (!Number.isFinite(result)) {
// Cobre divisão por zero (5/0 = Infinity, -5/0 = -Infinity, 0/0 = NaN)
throw new Error('Resultado inválido');
}
expression = String(result);
} catch (error) {
expression = 'Erro';
}
updateDisplay();
}
Aqui resolvemos o primeiro caso de borda da parte 1: em JavaScript, 5 / 0 não lança um erro — o resultado é Infinity. Sem a checagem Number.isFinite(result), a calculadora exibiria literalmente “Infinity” na tela em vez de uma mensagem amigável.
Edge case — pressionar ”=” sem operador (42 =): como parseExpression simplesmente faz parseFloat do único token que existir, "42" retorna 42 sem erro — esse caso já funciona sem código adicional.
Passo 6: Analisando a Expressão
A função parseExpression processa a expressão matemática e calcula o resultado, respeitando a precedência de operadores (multiplicação e divisão antes de adição e subtração):
function parseExpression(expression) {
// Remove espaços em branco
expression = expression.replace(/\s+/g, '');
// Verifica se a expressão contém apenas números e operadores válidos
if (!/^-?[0-9.]+([+\-*/][0-9.]+)*$/.test(expression)) {
throw new Error('Expressão inválida');
}
// Divide a expressão em operadores e operandos
let tokens = expression.split(/([+\-*/])/);
// Multiplicação e divisão primeiro
for (let i = 0; i < tokens.length; i++) {
if (tokens[i] === '*' || tokens[i] === '/') {
const operand1 = parseFloat(tokens[i - 1]);
const operand2 = parseFloat(tokens[i + 1]);
const result = tokens[i] === '*' ? operand1 * operand2 : operand1 / operand2;
tokens.splice(i - 1, 3, result); // Substitui a operação pelo resultado
i--; // Reajusta o índice após modificar o array
}
}
// Adição e subtração
let result = parseFloat(tokens[0]);
for (let i = 1; i < tokens.length; i += 2) {
const operator = tokens[i];
const nextOperand = parseFloat(tokens[i + 1]);
result = operator === '+' ? result + nextOperand : result - nextOperand;
}
return result;
}
O que mudou em relação a uma implementação ingênua: a expressão regular de validação (/^-?[0-9.]+([+\-*/][0-9.]+)*$/) rejeita expressões malformadas antes de tentar calculá-las — por exemplo, "5 + " (operador sem segundo operando) cai no catch de calculateResult e exibe “Erro”, em vez de retornar NaN silenciosamente.
Limitação conhecida: este parser não suporta parênteses ((2 + 3) * 4) nem múltiplas casas de precedência além de + - * /. Para uma calculadora de aprendizado isso é aceitável — para um projeto de produção, a abordagem recomendada é usar um parser de expressões dedicado (shunting-yard algorithm) em vez de eval(), que traz riscos de segurança ao executar código arbitrário digitado pelo usuário.
Passo 7: Suporte a Teclado
Um dos requisitos não funcionais da parte 1 era acessibilidade via teclado. Os botões já são focáveis e acionáveis com Enter/Espaço por serem elementos <button> nativos, mas também vale digitar diretamente:
document.addEventListener('keydown', (event) => {
if (event.key >= '0' && event.key <= '9') {
appendNumber(event.key);
} else if ('+-*/'.includes(event.key)) {
appendOperator(event.key);
} else if (event.key === '.') {
appendDecimal();
} else if (event.key === 'Enter' || event.key === '=') {
event.preventDefault();
calculateResult();
} else if (event.key === 'Escape' || event.key.toLowerCase() === 'c') {
clearDisplay();
}
});
Checklist de aceite — revisitando a parte 1
Voltando à checklist definida na parte 1:
- Soma, subtração, multiplicação e divisão funcionam corretamente —
parseExpression. - O botão “C” limpa o display e reseta o estado interno —
clearDisplay. - Divisão por zero não trava a página — checagem
Number.isFiniteemcalculateResult. - Números decimais são aceitos e calculados corretamente —
appendDecimalbloqueia pontos duplicados. - A interface funciona em telas pequenas — media query da parte 3.
- Todos os botões podem ser acionados via teclado — listener de
keydownacima.
Conclusão
Com este tutorial, construímos uma calculadora funcional em JavaScript, cobrindo não só o caminho feliz, mas os casos de borda que definimos lá na parte 1: divisão por zero, operadores em sequência e pontos decimais duplicados. Isso é o que separa um exercício de “hello world” de um projeto que resiste ao uso real.
Esperamos que esta série — da especificação de requisitos à estrutura HTML, passando pela estilização em CSS e terminando na lógica em JavaScript — tenha ajudado a aprimorar suas habilidades de desenvolvimento web de ponta a ponta.
Comentários
Não foi possível carregar os comentários no momento.
Deixe seu comentário