Expressões Regulares em Javascript: Libere o poder oculto na prática — Parte 2 (Metacharacters)
Foi mal a demora pessoal, tive que lançar o artigo sobre Chrome headless antes da continuação dessa série para aproveitar a oportunidade de lançamento.
Continuando… No capítulo anterior entendemos quando surgiram, o que são e pra que servem expressões regulares. Vimos prós e contras, exemplos e demonstramos a forma mais básica de começar a usá-las. Mas ficou um gostinho de quero mais. Se você perdeu a primeira parte, pode acessar aqui.
Hoje vamos dar um level up no nosso conhecimento de regexes.
Agenda:
Metacharacters
Para começarmos a ganhar mais produtividade e utilidade no trabalho com expressões regulares, vamos conhecer os famosos metacharacters. Com este nome intuitivo já dá pra imaginar do que se tratam. São caracteres especiais que representam outros caracteres dentro das nossas expressões. Logo, eles não devem ser interpretados pelo seu valor literal. Devemos entender cada um para decifrar o que está acontecendo na expressão. Há um total de 12 metacharacters, demonstrados a seguir:
\
: barra invertida → backslash;^
: circunflexo → caret;$
: cifrão → dollar sign;.
: ponto final → dot;|
: barra vertical → pipe;?
: ponto de interrogação → question mark;*
: asterisco → star;+
: sinal de mais → plus sign;()
: parêntesis → parenthesis;[]
: colchetes → brackets;{}
: chaves → curly braces;
Cada um deles exerce uma função e são usados de formas diferentes. Vamos abordar todos durante a série. Mas vejamos a barra invertida agora…
A barra invertida ou backslash (\
) serve para representar classes de caracteres que, nada mais são que agrupamentos de caracteres classificados por tipo. Ao ser usado em conjunto com algumas letras predefinidas (exceto .
), esse metacharacter expande o seu valor para uma gama de possibilidades. Vejamos algumas dessas combinações e suas representações para entendermos melhor:
.
: representa TODOS caracteres possíveis, exceto quebras de linha (\n
,\r
,\u2028
,\u2029
);\d
: representa qualquer dígito de 0 a 9;\w
: representa caracteres de palavra, ou seja, alfa-numéricos (letras não acentuadas e números) e underlines (_
);\b
: representa limite de palavra;\s
: representa espaçamentos (espaços (\s
), tabulações(\t
) e quebras de linha (\n
,\r
,\u2028
,\u2029
));
Abaixo temos as versões reversas dos metacharacters acima. Reversos representam o oposto das suas versões “originais” e são menos restritivos pois englobam mais caracteres. Por exemplo, com a expressão /\D/
teremos matches com tudo que não é dígito (letras, limites de palavras, espaçamentos e pontuações).
\D
;\W
;\B
;\S
;
*Outros metacharacters menos recorrentes podem ser vistos aqui.
**Para usar a barra invertida, o ponto ou qualquer metacaractere com seus valores literais, preceda com uma barra invertida, por exemplo:
\\
e\.
.
Alguns pontos importantes para não deixar passar despercebido:
Quando falamos de alfa-numéricos, além dos números e underline, tratamos apenas de letras maíusculas e minúsculas (case-sensitive por padrão) usadas na língua inglesa. Logo, temos de fora do grupo acentos e caracteres especiais essenciais para a língua portuguesa, espanhol, etc. Para considerá-los, devemos adicioná-los manualmente na expressão. Um por um, ou através de ranges (veremos em outro artigo).
Limites de palavra (word boundary) são um caso especial que não representam caracteres reais, e sim limites entre palavras. Mais especificamente de agrupamentos de metacharacters \w
. Exemplificando:
Para comprovarmos que limites de palavra não representam caracteres, vamos aprender uma nova função de objetos String (atentos que desta vez é uma string passando uma função com argumento RegExp, inverso das anteriores). A função .match(padrao)
é executada a partir do texto a ser analisado e recebe um único argumento padrao
que é a nossa regex. Caso existam matches, o retorno é um objeto Array contendo as substrings dos matches na ordem de ocorrência no texto, senão é null
. Por hora nossas regexes vão sempre dar match apenas na primeira substring válida, pois não estamos usando a flag global.
Primeiramente, como podemos ver acima, eu menti para vocês. Parcialmente. O retorno da função é um… Array com propriedades 😨!? Para casos de um único match, temos essa diferença. Isso é um problema, pois as vezes precisaremos ter que prever ambos os casos, implicando em mais código. Além disso, para vários matches, não recebemos os índices das substrings na String original.
Voltando ao cenário exposto, comprovamos a função dos limites de palavras pois temos o match de uma substring. Porém, no resultado não recebemos nada além da letra A
, justamente por que os\b
não passam de validações na sua expressão.
Ferramentas
Antes de começar a praticar alguns exercícios, passarei alguns sites que podemos usar daqui para frente. Eles irão nos auxiliar para pensar apenas na estrutura das expressões regulares e trazem agilidade nos testes.
Testes + Cheatsheets
Já usei ambos os sites, são muito bons. Ultimamente tenho optado pelo Regex101 por causa do diferencial da opção de selecionar a implementação de regexes do Javascript e por ser mais organizado no geral. O cheatsheet dele também está mais completo. Do Regexr eu gosto do fato de já vir com um texto pra testes e que permite jogar cada exemplo das referências para a área de testes.
Roots
- NodeJS no terminal / Dev tools do Chrome ou Firefox;
Cheatsheets
Praticando
Aprendemos novos conceitos importantes de expressões regulares. Podemos usá-los para resolver novas situações. Vamos praticar um pouco do visto até agora com alguns enunciados de problemas. Como ainda temos muitos assuntos para abordar, é difícil simular cenários mais próximos do mundo real. Mas chegaremos lá, só confia.
PS: Não há uma única resposta para cada questão, porém há as mais indicadas (precisas), de acordo com as necessidades evidenciadas.
Exercícios
1) Você tem uma lista de convidados enorme. Nela estão organizados cada convidado com dados de nome e o telefone celular. Nenhuma linha se repete e há apenas números de telefone residencial e móvel. Você precisa do número de telefone do convidado “Brendan Eich” mas se esqueceu. Monte uma expressão regular capaz de extrair nome e número, considerando a formatação da lista abaixo e sabendo que o DDD é 11 e números de telefones celulares têm 9 dígitos e começam com 9.
Exemplo:
...
Brenda Sioux, (11)2222-2222
Brenda Sioux, (11)92222-2222
Brendan Eich, (11)3333-3333
Brendan Eich, (11)90000-0000
Felipe Monobe, (11)4444-4444
Felipe Monobe, (11)94444-4444
...
Soluções:
O regex acima funciona, neste caso. Ele assume muitos pressupostos perigosos que devemos evitar. Basicamente as regras que ele usa para encontrar a linha correta é:
- O primeiro nome ter uma letra “n” na sétima posição;
- A linha inteira conter um total de 28 caracteres (considerando tudo: pontuação, espaços, caracteres, etc);
Fica claro que ele só funciona pois delimitamos no exemplo poucas linhas, mas temos de estar cientes que, na verdade, poderiam haver 300, 400, n linhas e então essas condições da expressão não seriam difíceis de se repetir. Nada garantiria que seria o primeiro match seria o nosso target. Lembra que devemos tentar ser precisos pra não ter que revisitar o mesmo regex, a fim de remover ou acrescentar matches? Com essa solução, isso inevitavelmente aconteceria.
Estamos caminhando para uma solução melhor. Usamos o metacharacter \w
para pegar apenas alfa-numéricos, demos os espaços, e colocamos a máscara do telefone. Vejamos as regras da expressão:
- O primeiro nome é composto de 7 letras ou números;
- O segundo nome é composto de 4 letras ou números e seguido de vírgula;
- O telefone tem 9 letras ou números separados por um hífen, após o quinto alfa-numérico. E precedidos de uma máscara de DDD com dois números ou letras contidos nela, por exemplo:
(00)12345–6789
;
Perceberam como nesta solução já conseguimos delimitar muito mais o cenário de extração? Não só definimos onde e quando estamos trabalhando com letras, números e pontuação, como a quantidade deles, com o uso de espaços literais. Montamos a máscara e temos algo mais propício a um número de telefone. Os problemas são que ainda podemos extrair outras pessoas e possibilitamos números no nome e letras no telefone. Não conheço nenhum desses casos 😆 (na verdade telefone com letra até existe, mas é um ou outro, não misturado).
Acho que desta solução para frente já podemos considerar segura. As regras são:
- O primeiro nome deve ser “Brendan” e não deve ser antecedido por caracteres de palavra;
- O segundo nome deve ser “Eich” e acabar com vírgula;
- O telefone deve ter DDD 11, com máscara. E começar com o dígito 9, seguido de outros 8 dígitos, separados por um hífen, após o quinto dígito. Não deve ser seguido de caracteres de palavra;
Dá pra melhorar? Sim, principalmente com os conceitos que ainda veremos. Essa expressão considera o fato de já termos o nome da pessoa de interesse, prevê o DDD do telefone e o padrão do tipo celular. Além disso, por segurança, usamos espaços visualmente pontuados (não corre perigo de passar despercebidos) e delimitamos para tudo isso não estar entre palavras (ainda não garante a unicidade da linha);
2) Você tem datas de viagens que fez no passado anotadas em um aplicativo de lembretes. Você percebe que estão faltando as fotos da sua viagem para a cidade de “Yu”. Mas só lembra do nome da cidade e a lista é enorme para procurar manualmente. Escreva uma expressão regular que te retorne a data desta viagem. Lembre-se que não é possível usar o nome do país e nem a vírgula como parte de sua expressão. Não é preciso verificar a validade da data.
Exemplo:
...
22/02/2005 YUCATÁN, Mexico
16/08/2006 YUMA, United States
14/04/2005 AYU, Russia
05/02/2015 YUTZ, France
26/12/2003 YU, China
27/03/2011 YUNCOS, Spain
19/01/2017 YUSUFELI, Turkey
09/07/2013 YUWARINGI, India
...
Solução:
Bem, essa é a melhor resposta que podemos dar hoje, mas se você prestou bem atenção, temos um grande porém. Consideramos um grande pressuposto com as datas, pois consideramos válidos quaisquer números que estejam na máscara 00/00/0000
, independente de serem datas válidas, de fato. Quando tivermos aprendido mais uns truques, isso não será mais uma dor de cabeça, ok? Esse problema seria mais fácil se os nomes não fossem tão parecidos. Sem o uso da vírgula para separar, só nos sobra o \b
para determinar o fim da palavra. Esse exercício parece mais fácil que o anterior, pois já vimos que podemos colocar limites de palavras pra enfatizar a segurança no match. Mas neste caso, a presença dele no final é imprescindível.
3) Considerando um objeto JSON com propriedades desconhecidas de tipo String, faça um algoritmo que converta todas as propriedades numéricas para inteiro e retorne o novo objeto. Propriedades de CPF e RG devem ser mantidas como Strings. Resolva com auxilio de expressões regulares, Objects, Arrays e Strings. Não é preciso verificar a validade do CPF e RG.
Exemplo:
const input = {
nome: 'Felipe Monobe',
rg: '11.222.333-X',
cpf: '111.222.333/44',
idade: '42',
nacionalidade: 'brasileiro',
sexo: 'masculino',
altura: '180',
peso: '70',
observacoes: 'não fumante'
}
Solução:
O código acima poderia ser encurtado algumas linhas (metade talvez) caso não usasse uma abordagem mais funcional de resolução.
Eu escolhi fazer assim por que o paradigma funcional é altamente modular e traz várias vantagens como maior legibilidade, reusabilidade, testabilidade, previsibilidade, etc. Procurem mais sobre caso não conheçam.
O foco desse exercício é em como usar uma regex , com o que aprendemos até agora, em conjunto com outras funções num cenário mais corriqueiro. O padrão definido é extremamente simples: /\d/
. Criamos as funções:
extrairValor
: acessa uma propriedade dinâmica de um objeto;testarPadraoEmCaracteres
: quebra uma String e percorre os caracteres testando um padrão*;testarNumerico
: cria uma nova versão da função acima, que testa por números;converterParaInteiroBase10
: acho que já é bem intuitivo;gerarReduzirPropriedadesEmObjeto
: gera uma função que recebe os argumentos que receberemos da funçãoreduce
e os processa usando as funções argumentostestar
eprocessar
;reduzirPropriedadesEmObjeto
: verifica cada se os caracteres de cada propriedade é dígito, se for, converte para inteiro;criarNovoObjeto
: executa oreduce
nas propriedades do objetoinput
;- Usar
!Array.prototype.some()
no lugar deArray.prototype.every
tem o mesmo objetivo, mas melhor performance, pois garantimos que logo que encontremos um valor que não satisfaça o predicate (função de teste), paramos de iterar.
Glossário do dia
- Metacharacters: caracteres que tem representação não-literal;
- Classe de caractere: agrupamentos de caractere por tipo;
- Alfa-numéricos: letras, números e underlines (
_
); - Reverse metacharacter: versões negadas dos metacharacters;
- Case sensitivity: determina se haverá diferenciação para letras maiúsculas e minúsculas;
- Word boundary: limite entre palavras, não representa nenhum caractere do texto, apenas valida;
Resumindo
Resumo de 2k+ palavras: Aprendemos sobre metacharacters, vimos quais classes de palavras conseguimos representar com a barra invertida, demonstramos exemplos, descobrimos novas ferramentas para ajudar na criação das nossas expressões, praticamos com 3 exercícios e entendemos com a resolução e explicação de cada um deles. Cobrimos bastante coisa dessa vez. Nos próximos a tendência é diminuir ainda mais a teoria, e aumentar a prática, afinal é o nosso foco, não é mesmo? Valeu galera! Nos vemos na parte 3.
/keepCoding 🤓/