Mini Projeto: Calculadora no Console

[40] Mini Projeto: Calculadora no Console

Chegamos ao fim do primeiro módulo, e o jeito de fechá-lo é construindo: uma calculadora de console com histórico, memória e relatório, montada sobre uma closure que esconde o próprio estado. No caminho aparecem a classe de erro personalizada, o spread dentro de Math.max e o ?? que sabe diferenciar zero de vazio.
Javascript

16 min de leitura

Chegamos ao fim do primeiro módulo. Em nove artigos você percorreu um caminho sólido — da sintaxe básica até conceitos que muitos desenvolvedores levam meses para entender de verdade.

Este artigo tem dois objetivos: primeiro, revisar os conceitos centrais do módulo com exemplos rápidos. Segundo, construir um projeto real que une tudo em um único programa funcional. Projetos são onde o aprendizado se consolida — teoria sem prática esquece rápido.

Vamos revisar e construir.

Revisão rápida — Módulo 1

Variáveis e tipos

const nome = "Ricardo";      // string imutável
let pontos = 0;              // número mutável
const ativo = true;          // boolean
const endereco = null;       // ausência intencional
let profissao;               // undefined

// Template literal
console.log(`${nome} tem ${pontos} pontos.`);

Condicionais

const nota = 7.5;

if (nota >= 9) {
  console.log("Excelente");
} else if (nota >= 6) {
  console.log("Aprovado");
} else {
  console.log("Reprovado");
}

// Ternário
const status = nota >= 6 ? "Aprovado" : "Reprovado";

Laços

// for clássico
for (let i = 0; i < 5; i++) {
  console.log(i);
}

// for...of
const frutas = ["maçã", "banana", "laranja"];
for (const fruta of frutas) {
  console.log(fruta);
}

// while
let tentativas = 3;
while (tentativas > 0) {
  console.log(`Tentativas restantes: ${tentativas}`);
  tentativas--;
}

Funções

// Declaração
function somar(a, b) { return a + b; }

// Expressão
const subtrair = function(a, b) { return a - b; };

// Arrow
const multiplicar = (a, b) => a * b;

// Parâmetros padrão
const saudar = (nome = "visitante") => `Olá, ${nome}!`;

Arrays

const numeros = [1, 2, 3, 4, 5];

const dobrados = numeros.map(n => n * 2);
const pares = numeros.filter(n => n % 2 === 0);
const soma = numeros.reduce((acc, n) => acc + n, 0);

Objetos

const usuario = {
  nome: "Ana",
  idade: 28,
  saudar() {
    return `Olá, sou ${this.nome}!`;
  },
};

const { nome, idade } = usuario; // desestruturação
const copia = { ...usuario };    // spread

Escopo e Closures

function criarContador() {
  let count = 0;
  return {
    incrementar: () => ++count,
    valor: () => count,
  };
}

const contador = criarContador();
contador.incrementar();
contador.incrementar();
console.log(contador.valor()); // 2

Tratamento de erros

function dividir(a, b) {
  if (b === 0) throw new Error("Divisão por zero.");
  return a / b;
}

try {
  console.log(dividir(10, 0));
} catch (erro) {
  console.log(`Erro: ${erro.message}`);
} finally {
  console.log("Operação finalizada.");
}

Mini Projeto — Calculadora no Console

Agora vamos construir uma calculadora completa que aplica tudo do Módulo 1. Ela terá:

  • Operações básicas: soma, subtração, multiplicação, divisão
  • Operações avançadas: potência, resto, raiz quadrada
  • Histórico de operações
  • Validação de entrada com tratamento de erros
  • Memória (guardar e recuperar um valor)
  • Relatório final com estatísticas

Construiremos em partes, do mais simples ao mais complexo.

Parte 1 — Operações básicas

// Erros personalizados
class ErroCalculadora extends Error {
  constructor(mensagem) {
    super(mensagem);
    this.name = "ErroCalculadora";
  }
}

// Validação de entradas
function validarNumeros(...valores) {
  for (const valor of valores) {
    if (typeof valor !== "number" || isNaN(valor)) {
      throw new ErroCalculadora(`"${valor}" não é um número válido.`);
    }
  }
}

// Operações — cada uma faz apenas uma coisa
const operacoes = {
  somar: (a, b) => {
    validarNumeros(a, b);
    return a + b;
  },

  subtrair: (a, b) => {
    validarNumeros(a, b);
    return a - b;
  },

  multiplicar: (a, b) => {
    validarNumeros(a, b);
    return a * b;
  },

  dividir: (a, b) => {
    validarNumeros(a, b);
    if (b === 0) throw new ErroCalculadora("Divisão por zero não é permitida.");
    return a / b;
  },

  potencia: (base, expoente) => {
    validarNumeros(base, expoente);
    return base ** expoente;
  },

  resto: (a, b) => {
    validarNumeros(a, b);
    if (b === 0) throw new ErroCalculadora("Divisão por zero não é permitida.");
    return a % b;
  },

  raizQuadrada: (a) => {
    validarNumeros(a);
    if (a < 0) throw new ErroCalculadora("Não é possível calcular raiz de número negativo.");
    return Math.sqrt(a);
  },
};

Parte 2 — A fábrica da calculadora (closures + objetos)

function criarCalculadora() {
  // Estado privado — acessível apenas pelos métodos
  const historico = [];
  let memoria = null;
  let totalOperacoes = 0;
  let totalErros = 0;

  // Registra operação no histórico
  function registrar(expressao, resultado) {
    totalOperacoes++;
    const entrada = {
      numero: totalOperacoes,
      expressao,
      resultado,
      horario: new Date().toLocaleTimeString("pt-BR"),
    };
    historico.push(entrada);
    console.log(`[${entrada.numero}] ${expressao} = ${resultado}`);
    return resultado;
  }

  // Executa uma operação com tratamento de erros
  function executar(operacao, expressao, ...args) {
    try {
      const resultado = operacao(...args);
      return registrar(expressao, resultado);
    } catch (erro) {
      totalErros++;
      console.log(`❌ Erro: ${erro.message}`);
      return null;
    }
  }

  // Interface pública da calculadora
  return {
    somar(a, b) {
      return executar(operacoes.somar, `${a} + ${b}`, a, b);
    },

    subtrair(a, b) {
      return executar(operacoes.subtrair, `${a} - ${b}`, a, b);
    },

    multiplicar(a, b) {
      return executar(operacoes.multiplicar, `${a} × ${b}`, a, b);
    },

    dividir(a, b) {
      return executar(operacoes.dividir, `${a} ÷ ${b}`, a, b);
    },

    potencia(base, exp) {
      return executar(operacoes.potencia, `${base} ^ ${exp}`, base, exp);
    },

    resto(a, b) {
      return executar(operacoes.resto, `${a} % ${b}`, a, b);
    },

    raizQuadrada(a) {
      return executar(operacoes.raizQuadrada, `√${a}`, a);
    },

    // Memória
    salvarMemoria(valor) {
      if (typeof valor !== "number" || isNaN(valor)) {
        console.log("❌ Valor inválido para memória.");
        return;
      }
      memoria = valor;
      console.log(`💾 Memória salva: ${memoria}`);
    },

    recuperarMemoria() {
      if (memoria === null) {
        console.log("⚠️  Memória vazia.");
        return null;
      }
      console.log(`💾 Memória: ${memoria}`);
      return memoria;
    },

    limparMemoria() {
      memoria = null;
      console.log("💾 Memória limpa.");
    },

    // Histórico
    verHistorico() {
      if (historico.length === 0) {
        console.log("Nenhuma operação realizada ainda.");
        return;
      }
      console.log("\n📋 HISTÓRICO DE OPERAÇÕES:");
      console.log("─".repeat(40));
      historico.forEach(({ numero, expressao, resultado, horario }) => {
        console.log(`  [${numero}] ${horario} | ${expressao} = ${resultado}`);
      });
      console.log("─".repeat(40));
    },

    // Relatório final com estatísticas
    relatorio() {
      const resultados = historico.map(h => h.resultado).filter(r => r !== null);

      const maior = resultados.length > 0 ? Math.max(...resultados) : null;
      const menor = resultados.length > 0 ? Math.min(...resultados) : null;
      const media = resultados.length > 0
        ? (resultados.reduce((acc, r) => acc + r, 0) / resultados.length).toFixed(2)
        : null;

      console.log("\n📊 RELATÓRIO DA SESSÃO:");
      console.log("═".repeat(40));
      console.log(`  Total de operações : ${totalOperacoes + totalErros}`);
      console.log(`  Operações com erro : ${totalErros}`);
      console.log(`  Operações válidas  : ${totalOperacoes}`);
      console.log(`  Maior resultado    : ${maior}`);
      console.log(`  Menor resultado    : ${menor}`);
      console.log(`  Média dos resultados: ${media}`);
      console.log(`  Memória atual      : ${memoria ?? "vazia"}`);
      console.log("═".repeat(40));
    },
  };
}

Parte 3 — Usando a calculadora

const calc = criarCalculadora();

console.log("🔢 CALCULADORA JAVASCRIPT\n");

// Operações básicas
calc.somar(10, 5);          // [1] 10 + 5 = 15
calc.subtrair(20, 8);       // [2] 20 - 8 = 12
calc.multiplicar(6, 7);     // [3] 6 × 7 = 42
calc.dividir(100, 4);       // [4] 100 ÷ 4 = 25

// Operações avançadas
calc.potencia(2, 10);       // [5] 2 ^ 10 = 1024
calc.resto(17, 5);          // [6] 17 % 5 = 2
calc.raizQuadrada(144);     // [7] √144 = 12

// Testando erros
calc.dividir(10, 0);        // ❌ Divisão por zero
calc.raizQuadrada(-9);      // ❌ Raiz de negativo
calc.somar("abc", 5);       // ❌ Valor inválido

// Memória
calc.salvarMemoria(calc.somar(3, 7)); // salva 10
calc.recuperarMemoria();              // 💾 Memória: 10
calc.multiplicar(calc.recuperarMemoria(), 5); // 10 × 5 = 50
calc.limparMemoria();

// Histórico e relatório
calc.verHistorico();
calc.relatorio();

Parte 4 — Saída esperada no console

🔢 CALCULADORA JAVASCRIPT

[1] 10 + 5 = 15
[2] 20 - 8 = 12
[3] 6 × 7 = 42
[4] 100 ÷ 4 = 25
[5] 2 ^ 10 = 1024
[6] 17 % 5 = 2
[7] √144 = 12
❌ Erro: Divisão por zero não é permitida.
❌ Erro: Não é possível calcular raiz de número negativo.
❌ Erro: "abc" não é um número válido.
[8] 3 + 7 = 10
💾 Memória salva: 10
💾 Memória: 10
[9] 10 × 5 = 50
💾 Memória limpa.

📋 HISTÓRICO DE OPERAÇÕES:
────────────────────────────────────────
  [1] 10:32:15 | 10 + 5 = 15
  [2] 10:32:15 | 20 - 8 = 12
  [3] 10:32:15 | 6 × 7 = 42
  [4] 10:32:15 | 100 ÷ 4 = 25
  [5] 10:32:15 | 2 ^ 10 = 1024
  [6] 10:32:15 | 17 % 5 = 2
  [7] 10:32:15 | √144 = 12
  [8] 10:32:15 | 3 + 7 = 10
  [9] 10:32:15 | 10 × 5 = 50
────────────────────────────────────────

📊 RELATÓRIO DA SESSÃO:
════════════════════════════════════════
  Total de operações  : 12
  Operações com erro  : 3
  Operações válidas   : 9
  Maior resultado     : 1024
  Menor resultado     : 2
  Média dos resultados: 132.44
  Memória atual       : vazia
════════════════════════════════════════

O que este projeto exercitou

Conceito Onde apareceu
const e let Estado interno da calculadora
Condicionais Validações e tratamento de casos especiais
Funções e arrow functions Todas as operações e helpers
Objetos e métodos Interface pública e objeto operacoes
Arrays e métodos Histórico, map, filter, reduce
Desestruturação forEach no histórico
Spread operator Math.max(...resultados)
Escopo Estado privado da calculadora
Closures historico, memoria, totalOperacoes privados
Classes de erro ErroCalculadora
try/catch/finally executar()
Template literals Todas as mensagens formatadas

Desafio final do Módulo 1

Expanda a calculadora com pelo menos duas das funcionalidades abaixo:

  1. Método desfazer() que remove a última operação do histórico
  2. Método filtrarHistorico(operador) que retorna apenas operações de um tipo (ex: só somas)
  3. Método exportarHistorico() que retorna o histórico como string formatada
  4. Suporte a cálculos encadeados: calc.somar(5, 3).multiplicar(2) retornando 16
  5. Método limpar() que zera todo o estado da calculadora
Ver solução — as cinco funcionalidades, incluindo o encadeamento
// O enunciado pede pelo menos duas. Aqui estão as cinco — e a quarta
// (encadeamento) é a que muda o desenho da calculadora, então vem por
// último, depois que as outras estiverem no lugar.

// As operações da Parte 1, resumidas para o bloco rodar sozinho.
const operacoes = {
  somar: (a, b) => a + b,
  subtrair: (a, b) => a - b,
  multiplicar: (a, b) => a * b,
  dividir: (a, b) => {
    if (b === 0) throw new Error("divisão por zero");
    return a / b;
  },
};

function criarCalculadora() {
  const historico = [];
  let totalOperacoes = 0;
  let totalErros = 0;

  function registrar(expressao, resultado, operador) {
    totalOperacoes++;
    // `operador` é novo: sem ele, filtrarHistorico teria de adivinhar o
    // tipo da operação lendo a string da expressão com regex.
    historico.push({
      numero: totalOperacoes,
      expressao,
      resultado,
      operador,
      horario: new Date().toLocaleTimeString("pt-BR"),
    });
    return resultado;
  }

  function executar(operacao, expressao, operador, ...args) {
    try {
      return registrar(expressao, operacao(...args), operador);
    } catch (erro) {
      totalErros++;
      console.log(`❌ Erro: ${erro.message}`);
      return NaN; // NaN propaga pela cadeia; null quebraria a conta seguinte
    }
  }

  const calc = {
    // ------------------------------------------------------------
    // 1 — desfazer()
    // ------------------------------------------------------------
    desfazer() {
      if (historico.length === 0) {
        console.log("⚠️  Nada para desfazer.");
        return null;
      }
      const removida = historico.pop();
      // O contador volta junto: sem isso o próximo item viraria [4]
      // logo depois de [2] ter sido desfeito, e o histórico mentiria.
      totalOperacoes--;
      console.log(`↩️  Desfeito: ${removida.expressao} = ${removida.resultado}`);
      return removida;
    },

    // ------------------------------------------------------------
    // 2 — filtrarHistorico(operador)
    // ------------------------------------------------------------
    filtrarHistorico(operador) {
      // Devolve cópia: se devolvesse o array interno, quem chamou
      // poderia dar push nele e furar o encapsulamento da closure.
      return historico.filter((item) => item.operador === operador);
    },

    // ------------------------------------------------------------
    // 3 — exportarHistorico()
    // ------------------------------------------------------------
    exportarHistorico() {
      if (historico.length === 0) return "Histórico vazio.";

      const linhas = historico.map(
        (item) =>
          `[${String(item.numero).padStart(2, "0")}] ${item.horario}  ` +
          `${item.expressao} = ${item.resultado}`
      );

      return [
        "═".repeat(46),
        "  HISTÓRICO DA CALCULADORA",
        "═".repeat(46),
        ...linhas,
        "─".repeat(46),
        `  ${historico.length} operação(ões) · ${totalErros} erro(s)`,
      ].join("\n");
    },

    // ------------------------------------------------------------
    // 5 — limpar()
    // ------------------------------------------------------------
    limpar() {
      // historico.length = 0 esvazia o MESMO array. Trocar por
      // `historico = []` seria erro: a variável é const, e mesmo que
      // não fosse, quem já tivesse a referência antiga veria o array
      // velho.
      historico.length = 0;
      totalOperacoes = 0;
      totalErros = 0;
      console.log("🧹 Calculadora zerada.");
      return calc;
    },

    verHistorico() {
      return [...historico];
    },
  };

  // ------------------------------------------------------------
  // 4 — cálculos encadeados
  // ------------------------------------------------------------
  // O truque: os métodos param de devolver número e passam a devolver
  // um objeto que CARREGA o número e sabe continuar a conta. Como ele
  // implementa valueOf, continua funcionando em contexto numérico.
  const NOMES = ["somar", "subtrair", "multiplicar", "dividir"];
  const SIMBOLO = { somar: "+", subtrair: "-", multiplicar: "×", dividir: "÷" };

  function encadeavel(valor) {
    const alvo = {
      valor: () => valor,
      valueOf: () => valor,
      toString: () => String(valor),
    };

    for (const nome of NOMES) {
      // Na cadeia, o resultado anterior é o primeiro operando.
      alvo[nome] = (b) => calc[nome](valor, b);
    }

    return alvo;
  }

  for (const nome of NOMES) {
    calc[nome] = (a, b) =>
      encadeavel(
        executar(operacoes[nome], `${a} ${SIMBOLO[nome]} ${b}`, nome, a, b)
      );
  }

  return calc;
}

// ---------------------------------------------------------------
// Usando
// ---------------------------------------------------------------
const calc = criarCalculadora();

console.log(calc.somar(5, 3).multiplicar(2).valor()); // 16
console.log(calc.somar(10, 5).subtrair(3).dividir(2).valor()); // 6

calc.dividir(1, 0); // ❌ Erro: divisão por zero
console.log(calc.somar(2, 2).valor()); // 4 — a calculadora sobrevive ao erro

console.log(calc.filtrarHistorico("somar").map((h) => h.expressao));
// [ '5 + 3', '10 + 5', '2 + 2' ]

calc.desfazer(); // ↩️  Desfeito: 2 + 2 = 4

console.log(calc.exportarHistorico());

calc.limpar();
console.log(calc.exportarHistorico()); // Histórico vazio.

// ---------------------------------------------------------------
// O detalhe que morde: console.log não chama valueOf
// ---------------------------------------------------------------
const cadeia = criarCalculadora().somar(5, 3);

console.log(cadeia);            // { valor: [Function], valueOf: ..., ... }
console.log(cadeia + 0);        // 8  — contexto numérico converte
console.log(`${cadeia}`);       // 8  — template literal chama toString
console.log(Number(cadeia));    // 8
console.log(cadeia.valor());    // 8  — explícito, e é o que se deve usar

Encadear é devolver um objeto onde antes havia um número — e objeto impresso é objeto, não valor. valueOf resolve conta e comparação, toString resolve template literal, mas console.log não chama nenhum dos dois. Por isso a cadeia termina em .valor(): explícito é melhor que mágico.

A calculadora é pequena de propósito: cabe num arquivo e, ainda assim, obriga a decidir o que fazer quando o usuário digita algo que não é número, onde termina o cálculo e começa a exibição, e como registrar o que já foi feito. É o primeiro código da série em que o difícil deixa de ser a sintaxe e passa a ser escolher onde cada responsabilidade mora — e é essa escolha, não o conhecimento de map ou de try, que separa um script que funciona de um que se consegue manter.

Fontes e Referências

Exercícios

Exercício 1

O que cada uma das três linhas marcadas imprime — e por quê a segunda calculadora não sabe nada da primeira?

const calc = criarCalculadora();
calc.somar(10, 5);

console.log(calc.historico);      // A
console.log(calc.verHistorico()); // B

const outra = criarCalculadora();
outra.relatorio();                // C
Ver resposta

✓ Resposta: A imprime undefined. O array historico vive dentro de criarCalculadora e o objeto devolvido nunca o expõe — não há erro de acesso, simplesmente não existe propriedade com esse nome. B imprime a tabela do histórico e, logo abaixo, undefined: verHistorico() escreve no console mas não tem return, então o console.log de fora recebe o vazio. C mostra um relatório zerado. Cada chamada a criarCalculadora() cria uma closure nova, com seu próprio historico, sua própria memoria e seus próprios contadores — não há estado global sendo compartilhado. É essa combinação, variáveis presas na closure e um objeto que só devolve métodos, que dá encapsulamento sem class e sem convenção de nome com underscore.

Exercício 2

Depois deste trecho, quais são os três primeiros números do relatório? E por que Operações válidas não pode ser calculado como totalOperacoes - totalErros?

const calc = criarCalculadora();
calc.somar(10, 5);
calc.dividir(10, 0);   // erro
calc.raizQuadrada(-9); // erro
calc.multiplicar(6, 7);
calc.relatorio();
Ver resposta

✓ Resposta: Total de operações 4, operações com erro 2, operações válidas 2. A chave está em quem incrementa o quê: totalOperacoes++ mora dentro de registrar(), que só é chamado quando a operação deu certo — nas duas falhas o fluxo desviou para o catch e só totalErros subiu. Logo totalOperacoes vale 2, e a subtração daria 0: estaria descontando erros de uma contagem que nunca os incluiu. O total precisa ser totalOperacoes + totalErros, e as válidas são o próprio totalOperacoes. E o contador não pode simplesmente passar a subir também nas falhas, porque é ele que numera as linhas do histórico — furaria a sequência, saltando de [2] para [5] sem que existissem [3] e [4].

Exercício 3

Rode as duas primeiras linhas mentalmente. O que sai? E o que o reduce da terceira linha faria sem o , 0 no fim?

const resultados = []; // nenhuma operação ainda

console.log(Math.max(...resultados));
console.log(Math.min(...resultados));
console.log(resultados.reduce((acc, r) => acc + r, 0));
Ver resposta

✓ Resposta: Sai -Infinity e depois Infinity. Com o spread de um array vazio, Math.max() é chamado sem argumento nenhum e devolve o elemento neutro do máximo, que é -Infinity; Math.min(), pela mesma lógica, devolve Infinity. É exatamente por isso que relatorio() testa resultados.length > 0 antes: sem a guarda, uma calculadora recém-criada anunciaria "Maior resultado: -Infinity / Menor resultado: Infinity" — invertido e absurdo. Já o reduce imprime 0 e não precisa de guarda nenhuma, porque o , 0 é o valor inicial. Retire-o e o reduce sobre array vazio lança TypeError: Reduce of empty array with no initial value.

Exercício 4

A memória guarda o valor zero. As duas versões da linha do relatório imprimem a mesma coisa?

calc.salvarMemoria(0);

// dentro de relatorio(), duas escritas possíveis:
console.log(`  Memória atual: ${memoria ?? "vazia"}`);
console.log(`  Memória atual: ${memoria || "vazia"}`);
Ver resposta

✓ Resposta: Não. A primeira imprime 0; a segunda imprime vazia — e mente. O ?? só recorre ao valor da direita quando o da esquerda é null ou undefined, isto é, quando de fato não há valor. O || recorre a ele diante de qualquer coisa falsy, e 0 é falsy — assim como "", NaN e false. Zero na memória é um valor legítimo, salvo de propósito: trocar o ?? pelo || transformaria "guardei zero" em "não guardei nada". Os dois só coincidem quando memoria é realmente null — que é o estado inicial e o estado depois de limparMemoria(), e é o caso que costuma passar em qualquer teste rápido, escondendo o bug.

Exercício 5

Quantas mensagens de erro aparecem no console, e o que a segunda reclama? Repare que só houve uma divisão por zero.

const calc = criarCalculadora();

const resultado = calc.dividir(10, 0);
calc.multiplicar(resultado, 5);
Ver resposta

✓ Resposta: Aparecem duas. A primeira é ❌ Erro: Divisão por zero não é permitida. — o executar() capturou, contou o erro e devolveu null. Esse null segue vivo em resultado e entra na operação seguinte: validarNumeros(null, 5) avalia typeof null, que é "object" e não "number", e lança a segunda mensagem, ❌ Erro: "null" não é um número válido. O programa não quebra em momento algum — a falha apenas se degrada em outra falha, um pouco mais confusa que a original. É essa a razão de a solução do desafio final devolver NaN em vez de null: NaN é do tipo number, atravessa a cadeia sem disparar um segundo erro de validação, ainda que contamine o resultado. Nenhuma das duas escolhas é obviamente melhor; o que não pode é ser escolha acidental, porque o valor devolvido por uma operação que falhou continua circulando pelo programa.

Comentários

Mais em Javascript

WebSockets e Comunicação em Tempo Real
WebSockets e Comunicação em Tempo Real

HTTP é uma conversa em que só o cliente pode falar primeiro. O WebSocket abre…

Performance em aplicações web
Performance em aplicações web

É fácil otimizar a coisa errada, e por isso a ordem importa: medir antes. O…

O que é Programação Assíncrona? O Event Loop Explicado
O que é Programação Assíncrona? O Event Loop Explicado

Uma linguagem que faz uma coisa de cada vez não deveria conseguir esperar três…