Expressões Regulares em Javascript: Libere o poder oculto na prática — Parte 3 (Character Sets & Quantifiers)


Fala galera!! Estamos de volta com mais uma parte desta série de expressões regulares no Javascript.

Hoje veremos mais conceitos fundamentais no trabalho com regexes. Ao meu ver, com mais esses dois que veremos hoje, temos o básico essencial para expressar cenários mais reais. Enfim, este artigo é sobre character sets e quantifiers!

Caso ainda não tenha lido as partes anteriores, LEIA para entender tudo que usaremos hoje. Seguem os links abaixo:


Antes de qualquer coisa, preciso deixar claro que tanto character sets ([]) quanto quantifiers ({},*,?,+) são metacharacters. Assim como character classes (\), visto no artigo anterior.

Character Sets

Por mais que eu não esteja tentando tirar uma com a cara de vocês, character sets são grupos de caracteres também. A primeira diferença deles para o character classes (\) é que você tem que explicitamente determinar os grupos usando brackets, colchetes ou []. Isso é bom e ruim. Se por um lado podemos ter maior precisão e customização dos caracteres que fazem parte do grupo, por outro, é uma forma mais verbosa e manual do que já tínhamos aprendido.

Para definir os grupos de caracteres, temos novos recursos e considerações importantes:

  1. A relação lógica entre diferentes caracteres ou ranges dentro de um mesmo set é de alternância (OU): /[abc]/ captura a ou b ou c, por exemplo;
  2. Todos os caracteres contidos nos colchetes irão capturar apenas o primeiro match (por enquanto) que aparecer no texto analisado: /[felino]/ captura apenas 'f' da String 'felino';
  3. Diferentemente de outras engines ou implementações de expressões regulares, no Javascript character classes funcionam dentro de brackets: /[\d]/ captura dígitos, ao invés de \ ou d;
  4. ^ como primeiro caractere, nega o set, senão é lido literalmente;
  5. - entre caracteres monta um range set;

Sets simples

A forma mais simples de começarmos a usar sets é colocando os caracteres de interesse entre colchetes. Se, por exemplo, quisermos capturar pronomes retos da terceira pessoa do singular, usaríamos /\bel[ae]\b/ . Essa expressão dará match no primeiro ele OU ela que encontrar no texto.

Sets negados

Caret, acento circunflexo ou ^ como primeiro caractere dentro dos colchetes nega o character set, capturando quaisquer outros caracteres que não sejam os contidos nos brackets. Colocar um ^em outra posição significa capturá-lo literalmente. Estes devem ser usados com cuidado pois, ao contrario dos sets simples, os negados tendem ser extremamente permissivos. Se, por exemplo, quisermos capturar qualquer caractere que não seja uma vogal num texto, usaríamos /[^AEIOUaeiou]/, o que incluiria não só as consoantes, mas também espaços, caracteres especiais, etc.

Ranged sets

Hyphen, hífen ou - define ranges ou sequências. Os limites precisam estar na ordem correta, isso significa do menor número ao maior ou do primeiro caractere ao último, de acordo com a ordem da tabela Unicode. Se o hífen for colocado no começo ou fim dos brackets, será assumido como literal. Se o range escolhido não estiver ordenado, ocorrerá um SyntaxError: invalid range in character class

Se você está ligeiro, já deve ter imaginado que /[0-9]/ é o mesmo que /\d/ , da mesma forma que /[A-z0-9_]/ equivale a /\w/ . E você está certo 🎉 🎉. Nesses casos, fica evidente que usar character classes é mais legível e conciso que usar sets. Mas se precisássemos capturar algo mais específico como das letras F à H, então a forma mais prática seria /[F-H]/ .

Obs: Agora podemos capturar letras com acentuação latina, definindo o range desse grupo Unicode. De acordo com o complemento latino 1 da tabela Unicode, partindo de U+00C0 até U+00FF temos todos (e até mais) que usamos na língua portuguesa. A expressão para os caracteres acentuados APENAS, é /\u00C0-\u00FF/ , ok?
Sets done! virando a página…

Quantifiers

Quantifiers ou quantificadores são outros metacharacters muito recorrentes no dia-a-dia do desenvolvedor regexer 🤖. Estes servem para controlar a quantidade de vezes que um trecho ou regra da expressão antecedente deverá ter para considerar um match. Isso não é bom só para aumentar legibilidade, através da diminuição de repetições, mas também para alcançarmos um nível de imprevisibilidade mais compatível com o mundo real. Afinal nem sempre sabemos se ou o que está num texto, se tivéssemos essa informação, trabalharíamos apenas com expressões regulares literais. Os quantifiers existentes são:

  • ?: captura de 0 a 1 vez;
  • *: captura de 0 a n vezes;
  • +: captura de 1 a n vezes;
  • {x}: captura x vezes;
  • {x,}: captura de x a n vezes;
  • {x,y}: captura de x a y vezes;

Portanto, agora se quisermos capturar palavras cujo tamanho é desconhecido, não tem problema! Por exemplo, para extrair palavras que contenham car, escrevemos /[A-z]*car[A-z]*/ e capturamos cara, trocar, trocarmos, etc.

Greedy x Lazy

Talvez agora você esteja se perguntando: “Mas para todos os quantifiers que tem limite n, como ele define qual é o tamanho do match? É o mínimo ou máximo possível?”. O comportamento padrão é de sempre pegar o máximo possível de repetições. E por este motivo ele é chamado de greedy ou ganancioso. Para inverter esse comportamento, usamos suas formas lazy ou preguiçosa (*?, +?, {x}?, {x,}?, {x,y}?), que assume o menor número de repetições. Se você não entendeu, veja o cenário a seguir:

'...... .. ..... ....... (. ... ..) .. ... (... .)'

Temos um texto mas não sabemos o conteúdo, queremos pegar o primeiro conteúdo entre parêntesis que também desconhecemos. Logo pensamos em algo como /(.+)/. Acontece que, se tiver (e neste caso há) outros trechos com parêntesis, o nosso match no exemplo acima se torna (. ... ...) .. ... (... .), ao invés de apenas(. ... ..). E isso faz sentido, pois quando usamos .+ queremos pegar tudo, a maior quantidade de vezes possível. Então a nossa resposta ideal até é capturada, mas como a engine continua encontrando caracteres válidos, o match se estende. Invertendo para o comportamento lazy: /(.+?)/, capturamos somente o mínimo necessário.

Adivinhem qual é o greedy…

Praticando

Para obter maior nível de precisão e segurança, monte suas expressões regulares baseando-se na ideia, na regra de negócio que ela precisa cumprir, ao invés de simplesmente testar matches numa massa de dados. Use essa última técnica no final do processo, somente para confirmar o esperado.

Obs:Quebrei os códigos do gist pra tentar ficar mais mobile-friendly. Vejam se ficaram bons.

1) Dado uma String com palavras separadas por vírgulas, contabilize quantas palavras iniciam com consoantes, com o auxilio de expressões regulares.

Exemplo:

"mexer,exceto,ermo,respeito,vicissitude,sublime,prerrogativa,vereda,presteza,proeminente,embuste,cinismo,contencioso,sagaz,contenda,antologia,resiliência,remanescente,perspicaz,insolente,conjuntura,oportunista,obstante,pertinente,depreender,prolixo,algoz,sucinto,simultaneamente,ardiloso,paradigma,pressuposto,pressa,heterogeneidade,complacente,retificar,prescindir,deliberar,persuadir,benignidade,equidade"
// código mágico
31

Solução:

Set negando vogais no início da palavra.

Primeiramente, precisamos de alguma forma de trabalhar com as palavras em contexto separado, só enquanto ainda não vermos conceitos de anchors e flags. Para isso pegamos a String de input e damos um split nas vírgulas, que atua como separador neste caso. Como só precisamos verificar o começo das palavras para filtrar, então nem usamos quantificadores. Colocamos um \b para dar maior garantia de que estamos no começo da palavra (vai ter um jeito ainda mais correto depois). E então criamos um set negado contendo todas as vogais. testarPadrao vai receber o padrão da expressão criada e vai deixar pronta outra função para ser usada como predicate do filter na definição de arrayConsoantesInicio. Por fim, apenas damos o console.log do valor armazenado em qtdePalavrasIniciadasConsoantes.

2) A partir de um Array de Strings de números, melhore a expressão de captura de telefones que tínhamos feito no artigo anterior (/\(11\)9\d\d\d\d-\d\d\d\d/), retornando um Array de objetos com as propriedades String valor e tipo (fixo, móvel, inválido). Desta vez considere qualquer DDD e telefones fixos (8 dígitos, começa com um número de 2 a 6), além de telefones celulares (9 dígitos, começa com 9).
Fonte: http://www.anatel.gov.br/Portal/verificaDocumentos/documento.asp?null&filtro=1&documentoPath=biblioteca/resolucao/1998/anexo_res_86_1998.pdf

Exemplo:

['012345678','(ab)cdef-ghij','01923456789','(01)2345-6789','(ab)9cdef-ghij','1234-5678','(01)9234-5678','(01)1234-5678','abcdefghij','(01)91234-5678']
// código mágico
[
{ valor: '012345678', tipo: 'inválido' },
{ valor: '(ab)cdef-ghij', tipo: 'inválido' },
{ valor: '01923456789', tipo: 'inválido' },
{ valor: '(01)2345-6789', tipo: 'fixo' },
{ valor: '(ab)9cdef-ghij', tipo: 'inválido' },
{ valor: '1234-5678', tipo: 'inválido' },
{ valor: '(01)9234-5678', tipo: 'inválido' },
{ valor: '(01)1234-5678', tipo: 'inválido' },
{ valor: 'abcdefghij', tipo: 'inválido' },
{ valor: '(01)91234-5678', tipo: 'móvel' }
]

Solução:

Deixemos claro que se não houvesse limitação quanto ao número do primeiro dígito para telefones fixos, poderíamos fazer uma única expressão para números: /\(\d{2}\)9?\d{4}-\d{4}/. Porém não conseguiríamos determinar o tipo do telefone com o regex. Talvez fosse possível se após a validação vessemos o tamanho do texto. Enfim, voltando ao código… numa pegada de programação funcional, criamos as expressões regulares e objetos que já trazem o texto de classificação. Com criarObjetoClassificado podemos passá-lo para um map do Array de input e resultar no output esperado.

3) Dadas três URLs, extrair com auxilio de expressões regulares apenas os trechos até o nome do host (sem capturar caminho, subcaminho e parâmetros).

Exemplo:

http://www.dominio.com.br/caminho/subcaminho/parametro
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
https://medium.com/@felipemonobe
// código mágico
/*
[
"http://www.dominio.com.br/",
"https://developer.mozilla.org/",
"https://medium.com/"
]
*/

Solução:

Analisemos por partes… O primeiro padrão que detectamos é que todos começam com http, com o s sendo opcional em alguns casos. Logo temos de usar o quantificador ? nele. Usei {2} para ficar mais claro que estamos repetindo as barras. O problema agora é que a partir daqui, o resto parece ser flexível demais. Perderíamos horas tentando descobrir sozinhos quais as regras de nomenclatura de URLs. Seria muito mais prático se pudéssemos considerar que a partir das duas barras, pode vir qualquer coisa, usando .+, contanto que termine em outra barra. Portanto, resultando na expressão /\bhttps?:\/{2}.+\//. Mas se fizermos o teste de fogo, percebemos que a captura traz mais do que o necessário. Culpa do comportamento greedy. Se trocarmos para lazy, resolvemos o problema adicionando apenas um ? após o quantificador: /\bhttps?:\/{2}.+?\//. Como usar map de extração retorna um Array de Arrays, terminamos com um reduce para nivelar em apenas um nível, essa operação é chamada flatten.


Bônus

Desta vez ao invés de falar de ferramentas, vamos falar de mini-games pra passar o tempo aprendendo mais. Finalmente agora acredito que temos conhecimento o suficiente pra fazer sentido passar essas recomendações:

  • Regex Golf: O conceito de code golfing é usado para competições onde se tem que alcançar um objetivo com o menor código-fonte possível. Pense isso com expressões regulares. Isso mesmo! Resolva os desafios com as menores regexes que puder imaginar!! Bom pra aprender a otimizar.
  • Regex Crossword: Já imaginou uma cruzadinha em que as definições das palavras são trocadas por expressões regulares? Essa é a ideia desse site. Você cruza regras para descobrir os campos mais restritivos possíveis e vai preenchendo até completar cada nível. Ótimo para memorizar mecânicas simples e sintaxe.

Glossário do dia

  • character sets: agrupamento de caracteres definidos com uso de brackets;
  • quantifiers: delimitadores de repetição de regras das expressões;
  • ranges: sequências ordenadas de caracteres, baseadas na tabela Unicode;
  • Unicode: padrão de caracteres;
  • greedy: comportamento ganancioso de quantificadores que captura o máximo possível de repetições de uma expressão válida;
  • lazy: comportamento preguiçoso, inverso do ganancioso;
  • predicate: uma função de validação que retorna true ou false;
  • flatten: nivelamento de objetos ou Arrays para um único nível de profundidade;
  • code golfing: prática de tentar resolver um problema com o algoritmo mais conciso possível;

Resumindo

A cada artigo estamos cada vez mais perto da força. Desta vez aprendemos mais dois conceitos e metacaracteres, character sets e quantifiers. Vimos que character sets servem para criar grupos de classes, e seus 3 tipos: normais, negados e ranged. Na sequência vimos que quantifiers servem para controlar repetição e aprendemos sobre seu comportamento greedy e lazy. Exercitamos esses conceitos através de 3 problemas, com suas devidas soluções e explicações. Por fim, descobrimos 2 novos sites de jogos dentro do assunto de expressões regulares. What a day, ladies and gentlemen!

Dúvidas, sugestões, etc mandem aqui nos comentários ou no meu Twitter. Abraços e até a próxima.

Keep training, padawans!

/keepCoding 🤓/