Dia 10 — Desafios: sintaxe do zero
Desafio 1: validar e formatar um cadastro
O que o desafio resolve
Um cadastro chega do formulário como texto cru: espaço sobrando nas pontas, maiúscula misturada com minúscula, um campo que o usuário deixou em branco. Antes de gravar isso em qualquer lugar, três perguntas precisam de resposta: o dado está preenchido, está no formato certo, e está limpo o suficiente para comparar depois.
A ordem importa e é o que separa o código que funciona do que dá erro em produção: validar antes de formatar. Se o trim e o toLowerCase vierem primeiro, o dado ruim vira dado bonito e passa pela validação como se estivesse correto.
O que a página valida é o resultado de validar formulário: o navegador tem a validação dele, e a escrita em JavaScript existe para produzir mensagem no lugar certo, no português da aplicação, e não no do sistema operacional.
As quatro peças
O programa se monta com quatro peças independentes, e nenhuma delas sabe da existência das outras.
Formatação, uma função por transformação. Cada uma devolve valor novo e não altera o texto original.
const semEspaco = (texto) => texto.trim(); const minusculo = (texto) => texto.toLowerCase();
Nome de exibição, que junta as duas e cuida das maiúsculas. O split(/\s+/) é o detalhe: com split(' ') cada espaço vira uma palavra vazia, e "bia carvalho" sai como "Bia Carvalho", com os três espaços intactos no meio.
const nomeDeExibicao = (texto) => { const partes = semEspaco(texto).toLowerCase().split(/\s+/); return partes .map((parte) => parte.charAt(0).toUpperCase() + parte.slice(1)) .join(' '); };
Validação, que devolve sempre a mesma forma de objeto — com e sem erro — para quem chama nunca precisar perguntar se o erro veio.
function validar(cadastro) { const erros = []; // cada regra empurra seu problema e segue return { valido: erros.length === 0, erros }; }
Formatação final, o objeto que vai para o banco: já limpo, já minúsculo, idade já convertida para número.
const prontoParaGravar = { nome: nomeDeExibicao(cadastro.nome), email: minusculo(semEspaco(cadastro.email)), idade: Number(semEspaco(cadastro.idade)), cidade: semEspaco(cadastro.cidade), };
Campo obrigatório
Espaço em branco é ausência. Por isso o trim vem antes da comparação, e é '' que decide — não a string crua.
O objeto de cadastro é o que vem do formulário: um objeto com uma chave por campo, o mesmo que a validação recebe e o mesmo que a formatação devolve limpo. O que entra nele é texto; o que sai é número.
for (const campo of ['nome', 'email', 'cidade']) { if (semEspaco(cadastro[campo]) === '') { erros.push(campo + ': campo obrigatorio'); } }
O for sobre a lista de campos evita três blocos idênticos. Quando a regra for diferente para cada campo, o laço não serve e cada um ganha seu if.
O ciclo é o do exercício: a pessoa preenche, o formulário entrega o objeto de cadastro, e a validação roda antes de qualquer gravação. O trim e toLowerCase são o par que devolve o texto limpo.
Cada if empurra o problema na lista e segue em frente: quem cadastrou errou o e-mail e esqueceu a cidade, vê as duas mensagens de uma vez, e não uma por tentativa. O oposto disso é o retorno antecipado — o return que para a função na primeira falha —, que esconde as outras. Ele é a escolha certa quando a regra é "ou o cadastro inteiro passa, ou não passa", e a escolha errada quando a pessoa precisa ver todos os problemas de uma vez.
Formato: e-mail, idade e número
| Checagem | Código | Por quê |
|---|---|---|
um @ só | email.split('@').length === 2 | dois @ não formam e-mail |
| domínio com ponto | partes[1].includes('.') | ana@localhost não é endereço de verdade |
| sem espaço no meio | /\s/.test(email) | espaço é erro de digitação |
| inteiro | Number.isInteger(Number(texto)) | "30abc" não é número |
| na faixa | nota >= 18 && nota <= 120 | regra de negócio |
A checagem de espaço tem uma sutileza que quebra metade das implementações: se o teste rodar sobre o e-mail já em minúsculo, o resultado é idêntico e o if nunca entra. O que interessa é o espaço no meio, e ele só existe no texto original.
const emailCru = semEspaco(cadastro.email); const partes = emailCru.split('@'); if (partes.length !== 2 || !partes[1].includes('.') || /\s/.test(emailCru)) { erros.push('email: invalido'); }
Number('30abc') devolve NaN em vez de erro, e Number('') devolve 0. A diferença entre NaN e 0 é o que separa "campo vazio" de "campo preenchido com lixo", e o Number.isInteger é o que distingue as duas: Number.isInteger(Number('')) é false, mesmo com a comparação numérica passando.
Para número dentro de faixa, a guarda de NaN tem que vir antes da comparação de faixa: comparar com NaN devolve false nos dois lados, e o else if pode ser tomado como se o número fosse pequeno demais.
Os três erros que aparecem em produção
Validar o que já foi formatado. Com o toLowerCase antes da checagem, o teste de espaço nunca acusa, e o e-mail com espaço no meio vai para o banco.
Devolver duas formas diferentes. A função que devolve { valido: false } no erro e a lista de erros no sucesso obriga todo mundo a checar os dois formatos. Devolva sempre o mesmo objeto.
Formatar antes de validar. O custo é maior que o trabalho jogado fora: nomeDeExibicao('') devolve '', nomeDeExibicao de um número devolve NaN misturado no nome, e o erro aparece no JSON.stringify final, longe da origem.
O
alertde validação que o navegador faz sozinho (required,type="email") não substitui a validação em JavaScript: ele protege o envio, não o dado que já chegou, e a mensagem dele está em português do sistema operacional, não no português da sua aplicação. A validação escrita à mão é a que produz mensagem de erro no lugar certo.
Formato de e-mail não se resolve com uma expressão regular só. O que resolve, em código de produção, é uma mensagem de erro boa: o usuário precisa saber o que corrigir, e
"email inválido"sem dizer o que falta é o mesmo campo perdido de novo na próxima tentativa.
Exemplo
'use strict'; // ============================================================ // AULA 1 - Desafio 1: validar e formatar um cadastro // // O programa inteiro: entra um cadastro com espaços e letras // misturadas, valida campo por campo, formata o que passou e // imprime o relatório. Roda do primeiro campo ao último. // ============================================================ // ------------------ 1. ENTRADA ------------------ // O que chegaria do formulário, cru: espaço sobrando nas pontas, // maiúscula misturada com minúscula, um campo vazio. const cadastro = { nome: ' ana souza ', email: ' [email protected] ', idade: '30', cidade: '', }; // ------------------ 2. FORMATO ------------------ // Uma função por transformação. Cada uma devolve um valor novo, e o // texto original continua intacto se algo der errado no meio do caminho. const semEspaco = (texto) => texto.trim(); const minusculo = (texto) => texto.toLowerCase(); // Nome de exibição: `trim`, minúsculas, e a inicial de cada palavra em // maiúscula. O `split(/\s+/)` é o que importa: com `split(' ')` cada // espaço vira uma palavra vazia e o nome sai com três espaços no meio. const nomeDeExibicao = (texto) => { const partes = semEspaco(texto).toLowerCase().split(/\s+/); return partes .map((parte) => parte.charAt(0).toUpperCase() + parte.slice(1)) .join(' '); }; // ------------------ 3. VALIDAÇÃO ------------------ // Cada `if` empurra o problema na lista e segue em frente, e o resto da // função não precisa de `else` nem de nivelamento. // Devolve sempre a mesma forma de objeto: quem chama nunca precisa // perguntar "e se não vier erro?". function validar(cadastro) { const erros = []; // Campo obrigatório. Espaço também é ausência, então o `trim` vem // antes da comparação: é `''` que decide, não a string crua. for (const campo of ['nome', 'email', 'cidade']) { if (semEspaco(cadastro[campo]) === '') { erros.push(campo + ': campo obrigatório'); } } // E-mail: um arroba, e um domínio com ponto depois dele. const emailCru = semEspaco(cadastro.email); if (emailCru !== '') { const partes = emailCru.split('@'); if (partes.length !== 2 || !partes[1].includes('.')) { erros.push('email: precisa ter um @ e um domínio depois dele'); } // Espaço no MEIO do e-mail é erro de digitação; espaço nas pontas // não é, porque o `trim` acima já tirou. Por isso o teste é sobre o // texto original, e não sobre a versão em minúsculas. if (/\s/.test(emailCru)) { erros.push('email: não pode ter espaço'); } } // Idade precisa ser número inteiro dentro da faixa. const idadeTexto = semEspaco(cadastro.idade); const idadeNumero = Number(idadeTexto); if (idadeTexto === '' || !Number.isInteger(idadeNumero)) { erros.push('idade: precisa ser um número inteiro'); } else if (idadeNumero < 18 || idadeNumero > 120) { erros.push('idade: precisa estar entre 18 e 120'); } return { valido: erros.length === 0, erros }; } // ------------------ 4. PROCESSAMENTO ------------------ // Formata DEPOIS de validar. O caminho invertido, formatar e só depois // validar, joga trabalho fora quando o dado está errado. const resultado = validar(cadastro); // ------------------ 5. SAÍDA ------------------ console.log('--- ENTRADA (o que chegou do formulário) ---'); console.log('nome : [' + cadastro.nome + ']'); console.log('email : [' + cadastro.email + ']'); console.log('idade : [' + cadastro.idade + ']'); console.log('cidade: [' + cadastro.cidade + ']'); console.log('\n--- VALIDAÇÃO ---'); if (resultado.valido) { console.log('Cadastro válido.'); } else { console.log('Cadastro recusado. ' + resultado.erros.length + ' problema(s):'); // `forEach` em vez de `for`: aqui não existe índice, só a mensagem. resultado.erros.forEach((erro, i) => console.log(` ${i + 1}. ${erro}`)); } // ------------------ 6. O MESMO CÓDIGO, CADASTRO PREENCHIDO ------------------ const cadastroBom = { nome: ' bia carvalho ', email: ' [email protected] ', idade: ' 22 ', cidade: ' Olinda ', }; const resultadoBom = validar(cadastroBom); console.log('\n--- MESMO CÓDIGO, AGORA COM O CADASTRO PREENCHIDO ---'); console.log('validado:', resultadoBom.valido); if (resultadoBom.valido) { // O que vai para o banco: já limpo, já minúsculo, já número. const prontoParaGravar = { nome: nomeDeExibicao(cadastroBom.nome), email: minusculo(semEspaco(cadastroBom.email)), idade: Number(semEspaco(cadastroBom.idade)), cidade: semEspaco(cadastroBom.cidade), }; console.log('\nnome digitado : [' + cadastroBom.nome + ']'); console.log('nome gravado : [' + prontoParaGravar.nome + ']'); console.log('email digitado : [' + cadastroBom.email + ']'); console.log('email gravado : [' + prontoParaGravar.email + ']'); console.log('idade: string virou ' + typeof prontoParaGravar.idade + ' -> ' + prontoParaGravar.idade); console.log('\nobjeto pronto para o banco:'); console.log(JSON.stringify(prontoParaGravar, null, 2)); } else { console.log(resultadoBom.erros.join('; ')); }
Saída real
--- ENTRADA (o que chegou do formulário) --- nome : [ ana souza ] email : [ [email protected] ] idade : [30] cidade: [] --- VALIDAÇÃO --- Cadastro recusado. 1 problema(s): 1. cidade: campo obrigatório --- MESMO CÓDIGO, AGORA COM O CADASTRO PREENCHIDO --- validado: true nome digitado : [ bia carvalho ] nome gravado : [Bia Carvalho] email digitado : [ [email protected] ] email gravado : [[email protected]] idade: string virou number -> 22 objeto pronto para o banco: { "nome": "Bia Carvalho", "email": "[email protected]", "idade": 22, "cidade": "Olinda" }
Desafio 2: manipular uma lista de notas
O que o desafio resolve
Uma lista de notas chega suja: o mesmo aluno-launchado duas vezes por engano, uma nota digitada fora da escala. O relatório que sai dela tem média, maior, menor, aprovados, reprovados e dois tipos de ordenação. A lista inteira é uma, e cada pergunta é um método de array diferente.
O programa é o índice das aulas de array: cada bloco do relatório usa um método que já foi apresentado, e a ordem das etapas — limpar, medir, ordenar, imprimir — é a ordem em que as perguntas devem ser feitas. O relatorio de notas que sai no fim é só a montagem desses blocos na ordem.
Entrada e limpeza
A lista crua é um array de objetos, que é como o dado real chega: de um formulário, de uma consulta, de um arquivo.
const notas = [ { aluno: 'Ana', materia: 'Matematica', nota: 8.5 }, { aluno: 'Bruno', materia: 'Matematica', nota: 5.0 }, { aluno: 'Feca', materia: 'Matematica', nota: 12.0 }, // fora da escala ];
Antes de calcular qualquer coisa, filter tira o que não presta. Notar que a nota inválida sai pela escala (0 a 10), e não pelo critério de aprovação (6): são duas regras diferentes e elas não podem virar a mesma comparação.
| Regra | Código | O que remove |
|---|---|---|
| dentro da escala | nota >= 0 && nota <= 10 | digitação errada, nota impossível |
| sem repetição | findIndex devolve o índice | o mesmo aluno na mesma matéria duas vezes |
A repetição sai com findIndex dentro de filter, e o truque é comparar o índice devolvido com o índice atual: só a primeira ocorrência passa.
const semRepeticao = validas.filter((registro, indice) => validas.findIndex((outro) => outro.aluno === registro.aluno && outro.materia === registro.materia) === indice);
Set faz o mesmo em uma linha — new Set(chaves).size — e Map resolve quando o registro inteiro é o dado a comparar.
Medir: média, maior e menor
A média tem três partes: extrair os números, somar, dividir pelo quantidade. Cada parte tem seu método. Calcular media é o primeiro bloco do relatório de notas, e o resultado — a estatistica do conjunto — vem antes de qualquer texto pronto.
| Passo | Método | Assinatura | Devolve |
|---|---|---|---|
| extrair | map | notas.map((r) => r.nota) | array só com as notas |
| somar | reduce | reduce((soma, nota) => soma + nota, 0) | um número |
| dividir | — | soma / lista.length | a média |
| maior | Math.max | Math.max(...notas) | o maior valor |
| menor | Math.min | Math.min(...notas) | o menor valor |
O 0 inicial no reduce não é opcional: sem ele, o primeiro elemento é o acumulador, e a soma sai errada quando a lista é vazia ou tem um elemento só.
const soNotas = semRepeticao.map((registro) => registro.nota); const media = soNotas.reduce((soma, nota) => soma + nota, 0) / soNotas.length;
O Math.max(...notas) é o caminho curto, e o ... é obrigatório: sem ele, Math.max recebe um argumento, que é o array, e devolve NaN. A alternativa é reduce((maior, nota) => Math.max(maior, nota)), que funciona com qualquer tamanho de lista e não tem limite de argumentos.
Aprovado, reprovado e ordenação
Aprovado e reprovado são o mesmo filter com o sinal trocado, e é melhor calcular um e negar do que escrever a regra duas vezes.
As tarefas do relatório — quais entraram, quantas são, o maior e o menor valor, o texto final — saem todos daqui. É o mesmo princípio da aula anterior, com outra pergunta por cima.
const aprovados = semRepeticao.filter((r) => r.nota >= NOTA_MINIMA); const reprovados = semRepeticao.filter((r) => r.nota < NOTA_MINIMA);
| Chamada | Efeito |
|---|---|
lista.sort((a, b) => a - b) | número, crescente |
lista.sort((a, b) => b - a) | número, decrescente |
lista.sort((a, b) => a.aluno.localeCompare(b.aluno)) | texto, por ordem alfabética |
lista.sort() | converte para texto e ordena por string |
O sort sem argumento é a pegadinha que aparece em toda prova: "10" < "8" é true, porque a comparação passa a ser caractere a caractere. E o sort mexe na lista original; para preservar, use a cópia antes — [...lista].sort(...).
O localeCompare é o que ordena texto respeitando acentos: "arara".localeCompare("Árvore") põe o á onde o dicionário põe. O < entre strings ignora acento, e a ordem sai errada em qualquer lista com nome português.
Arredondar e a média vazia
toFixed(1) devolve texto, com ponto em vez de vírgula. Serve para imprimir e não para comparar, porque '8.5' > '7' compara caractere a caractere.
const umaCasa = (numero) => Math.round(numero * 10) / 10; // número const notaTexto = (numero) => umaCasa(numero).toFixed(1); // texto, só para imprimir
A divisão por zero não é erro em JavaScript: devolve NaN, e o programa continua rodando como se nada tivesse acontecido. typeof NaN é 'number', o que faz o erro passar despercebido até chegar num gráfico ou numa soma.
| Checagem | Devolve |
|---|---|
Number.isInteger | true se não tem casa decimal |
Number.isFinite | false para NaN, Infinity e -Infinity |
Number.isNaN | true só para NaN |
x === x | false quando x é NaN |
A guarda vem antes da divisão, e não depois.
const mediaSegura = soNotas.length > 0 ? soNotas.reduce((soma, nota) => soma + nota, 0) / soNotas.length : 0;
Reduzir a lista a um número no fim do
reduceé o caminho curto e funciona, mas começa a doer quando o relatório ganha uma segunda métrica. Separar emmape depoisreducecusta uma linha a mais e deixa cada número disponível para a próxima pergunta.
A lista original continua com os sete registros depois de todos os
filteresort. Isso não é sorte:filter,mapesortcom cópia devolvem array novo, e nenhum deles altera a lista de entrada. O único que altera é osortchamado direto na lista, e é por isso que a cópia vem antes dele.
Nota guardada como texto quebra o relatório inteiro sem erro nenhum:
'10' > '9'éfalse, a média sai como concatenação ('0' + '8.5'dá'08.5') e osortordena por caractere. A conversão precisa acontecer na entrada do dado, eNumber.isInteger(Number(valor))é a forma de garantir que ela aconteceu de verdade.
Exemplo
'use strict'; // ============================================================ // AULA 2 - Desafio 2: manipular uma lista de notas // // O relatório inteiro: entra uma lista de notas com uma repetida e // uma inválida, e sai média, maior, menor, aprovados, reprovados e // ordenação. Cada etapa usa um método de array diferente, para o // exemplo servir de índice das aulas de array e de objeto. // ============================================================ // ------------------ 1. ENTRADA ------------------ // A lista crua, como viria de um cadastro ou de uma planilha: // um aluno repetido por engano e uma nota fora da escala. const notas = [ { aluno: 'Ana', materia: 'Matematica', nota: 8.5 }, { aluno: 'Bruno', materia: 'Matematica', nota: 5.0 }, { aluno: 'Carla', materia: 'Matematica', nota: 9.5 }, { aluno: 'Diego', materia: 'Matematica', nota: 7.0 }, { aluno: 'Elis', materia: 'Matematica', nota: 10.0 }, { aluno: 'Ana', materia: 'Matematica', nota: 8.5 }, // repetido { aluno: 'Feca', materia: 'Matematica', nota: 12.0 }, // fora da escala ]; const NOTA_MINIMA = 6; // abaixo disso, reprovado const NOTA_MAXIMA = 10; // teto da escala const ESCALA_MINIMA = 0; // piso da escala: abaixo disso é digitado errado console.log('--- ENTRADA ---'); console.log('registros recebidos:', notas.length); // ------------------ 2. LIMPEZA ------------------ // `filter` tira o que não presta antes de calcular qualquer coisa. const validas = notas.filter((registro) => registro.nota >= ESCALA_MINIMA && registro.nota <= NOTA_MAXIMA); // A repetição do mesmo aluno na mesma matéria sai aqui. const semRepeticao = validas.filter((registro, indice) => validas.findIndex((outro) => outro.aluno === registro.aluno && outro.materia === registro.materia) === indice); console.log('depois de tirar a nota fora da escala:', validas.length); console.log('depois de tirar a repetição:', semRepeticao.length); // ------------------ 3. PROCESSAMENTO ------------------ // Várias respostas, uma pergunta só: como a nota se distribui. const soNotas = semRepeticao.map((registro) => registro.nota); const media = soNotas.reduce((soma, nota) => soma + nota, 0) / soNotas.length; const maior = Math.max(...soNotas); const menor = Math.min(...soNotas); const aprovados = semRepeticao.filter((registro) => registro.nota >= NOTA_MINIMA); const reprovados = semRepeticao.filter((registro) => registro.nota < NOTA_MINIMA); // O `sort` MEXE na lista que recebe. A cópia com `[...lista]` é o que // deixa o original intacto. Com `a - b` é número; com // `a.localeCompare(b)` é texto. Passando só `a`, o JavaScript ordena // como texto e "10" vem antes de "8". const porNota = [...semRepeticao].sort((a, b) => b.nota - a.nota); const porNome = [...semRepeticao].sort((a, b) => a.aluno.localeCompare(b.aluno)); // Uma casa decimal, arredondada no lugar certo. // `toFixed` devolve TEXTO. Para somar e comparar, o número arredondado // é este; a string serve só para imprimir alinhado na coluna. const umaCasa = (numero) => Math.round(numero * 10) / 10; const notaTexto = (numero) => umaCasa(numero).toFixed(1); // ------------------ 4. SAÍDA ------------------ console.log('\n--- RELATÓRIO ---'); console.log('alunos considerados :', semRepeticao.length); console.log('média da turma :', notaTexto(media)); console.log('maior nota :', notaTexto(maior)); console.log('menor nota :', notaTexto(menor)); console.log('aprovados (>= ' + NOTA_MINIMA + ') :', aprovados.length); console.log('reprovados (< ' + NOTA_MINIMA + ') :', reprovados.length); console.log('\naprovados:'); aprovados.forEach((registro) => { console.log(` ${registro.aluno.padEnd(7)} ${notaTexto(registro.nota)}`); }); console.log('\nreprovados:'); reprovados.forEach((registro) => { console.log(` ${registro.aluno.padEnd(7)} ${notaTexto(registro.nota)}`); }); console.log('\nda maior nota para a menor:'); porNota.forEach((registro, i) => { console.log(` ${i + 1}. ${registro.aluno} - ${notaTexto(registro.nota)}`); }); console.log('\nem ordem alfabética:'); porNome.forEach((registro) => { console.log(` ${registro.aluno}`); }); // A prova de que o original não foi mexido: 7 registros, não 5. console.log('\na lista original continua com', notas.length, 'registros'); // ------------------ 5. O MESMO CÓDIGO COM LISTA VAZIA ------------------ // Lista vazia é o caso que quebra a média. Não há exceção: dividir por // zero em JavaScript devolve `NaN`, e o programa continua rodando como // se nada tivesse acontecido. Por isso a guarda vem antes da divisão. console.log('\n--- O QUE ACONTECE COM LISTA VAZIA ---'); const nenhuma = []; const mediaVazia = nenhuma.reduce((soma, nota) => soma + nota, 0) / nenhuma.length; console.log('média da lista vazia:', mediaVazia); console.log('tipo:', typeof mediaVazia, '| Number.isFinite:', Number.isFinite(mediaVazia)); // A guarda que evita o `NaN`: sem lista, a média é zero. const mediaSegura = nenhuma.length > 0 ? nenhuma.reduce((soma, nota) => soma + nota, 0) / nenhuma.length : 0; console.log('com a guarda antes da divisão:', mediaSegura);
Saída real
--- ENTRADA --- registros recebidos: 7 depois de tirar a nota fora da escala: 6 depois de tirar a repetição: 5 --- RELATÓRIO --- alunos considerados : 5 média da turma : 8.0 maior nota : 10.0 menor nota : 5.0 aprovados (>= 6) : 4 reprovados (< 6) : 1 aprovados: Ana 8.5 Carla 9.5 Diego 7.0 Elis 10.0 reprovados: Bruno 5.0 da maior nota para a menor: 1. Elis - 10.0 2. Carla - 9.5 3. Ana - 8.5 4. Diego - 7.0 5. Bruno - 5.0 em ordem alfabética: Ana Bruno Carla Diego Elis a lista original continua com 7 registros --- O QUE ACONTECE COM LISTA VAZIA --- média da lista vazia: NaN tipo: number | Number.isFinite: false com a guarda antes da divisão: 0