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

Tutorial com passos estruturados

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.isFinite em calculateResult.
  • Números decimais são aceitos e calculados corretamente — appendDecimal bloqueia pontos duplicados.
  • A interface funciona em telas pequenas — media query da parte 3.
  • Todos os botões podem ser acionados via teclado — listener de keydown acima.

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.

>>>> 😊 Veja aqui o código completo do projeto no meu GitHub

Comentários

Não foi possível carregar os comentários no momento.

Deixe seu comentário