Fluent Regex Composer
Publicado por Henrique Lima e arquivado em domain driven design, fluent interface, java, regexNa semana passada, Martin Fowler escreveu um artigo demonstrando as práticas que ele costuma utilizar ao trabalhar com expressões regulares. Neste artigo, o senhor Fowler propõe uma maneira de separar uma expressão regular em partes menores, recompondo tal expressão posteriormente para, entre outra coisas, dar maior legibilidade ao código. Ao acabar de ler o artigo a primeira coisa que veio em minha cabeça foi Fluent Interfaces. Na atualização de seu artigo, Martin Fowler cita este assunto (inclusive expôe sua preferência por não utilizar Fluent Interfaces) e sugere uma alternativa fluente criada por Joshua Flanagan para C#.
Este post não tem como objetivo discutir se utilizar Fluent Interfaces, ou não, é a melhor maneira de resolver problemas relacionados a expressões regulares, mas sim, propor um exercício a fim de criar uma possível API fluente e, ao final do artigo, demonstrar uma pequena porção de código Java que pode solucionar alguns problemas relacionados a expressões regulares utilizando esta abordagem.
O problema
O grande problema de quando trabalhamos com expressões regulares é conhecer todos os recursos disponíveis (metacaracteres, quantificadores gananciosos, possessivos, relutantes, etc) para validarmos determinada String e posteriormente recuperarmos os grupos que nos interessa para aplicarmos a lógica que necessitamos. Além disso, quanto maior for a expressão mais difícil será de interpretá-la, por isso, utilizar a abordagem “Divisão e Conquista” é uma boa prática a ser seguida.
A solução
Criar uma API para abstrair os recursos (metacaracteres, quantificadores, etc) e disponibilizar ao usuário, métodos mais legíveis e simples de escrever. Para chegar a este fim, me propus a analisar as expressões regulares que mais utilizo no dia-a-dia, “mapeá-las” para o português estruturado e, então, escrever uma porção de código que reflita a interpretação do português estruturado o mais fluente possível.
Interpretando Expressões Regulares
Analisando uma expressão regular que tem como objetivo validar se Strings representam uma data em um formato dd/mm/aaaa (em java dd/MM/yyyy), temos:
Expressão Regular: \\d{2}/\\d{2}/\\d{4}
Português estruturado: A String em questão: Inicia com dois dígitos (numéricos), seguido de uma barra, seguido de dois dígitos (numéricos), seguido de uma barra e finaliza com quatro dígitos (númericos).
Código Java:
RegexComposer composer = RegexComposer.getInstance();
composer
.startsWith(exactly(2, digit()))
.followedBy(exactly(1, literal("/")))
.followedBy(exactly(2, digit()))
.followedBy(exactly(1, literal("/")))
.finishedWith(exactly(4, digit()));
Parece bacana, mas não está ao contrário? Porque o quantificador vem antes do dígito no código Java e na expressão regular não? Pois ao interpretarmos a expressão regular em português estruturado, também passamos o quantificador a frente do dígito e é essa a diferença, pois, ao interpretarmos uma expressão como: \\d{2} nós não escrevemos “Esta expressão representa digítos (numéricos) dois“, e sim “Esta expressão representa dois dígitos (numéricos)”.
Este foi o meu principal questionamento quando analisei a API do senhor Joshua Flanagan, pois acredito que ao utilizarmos a abordagem fluente, devemos valorizar a maneira como escreveríamos em uma língua (português/inglês) e não como é a sintaxe de uma determinada linguagem (de programação).
Delimitadores
Uma ocorrência constante (inclusive citado no artigo do Martin Fowler) é a utilização de delimitadores para separarmos os dados em Strings (tokens). São exemplos disto, os arquivos CSV (valores separados por vírgula) e também a data do exemplo anterior (separados por barra “/”). Desta forma, seria conveniente se pudessemos configurar a API para interpretar delimitadores. A seguir, demostro um exemplo de como isto poderia ser feito:
Expressão Regular: \\d{2}/\\d{2}/\\d{4}
Português estruturado: A String em questão: Inicia com dois dígitos (numéricos), seguido de dois dígitos (numéricos), finaliza com quatro dígitos (númericos) e é delimitada por uma barra.
Código Java:
RegexComposer composer = RegexComposer.getInstance();
composer
.startsWith(exactly(2, digit()))
.followedBy(exactly(2, digit()))
.finishedWith(exactly(4, digit()))
.delimitedBy(exactly(1, literal("/")));
Tá começando a melhorar, mas existe um recurso muito importante que deve ser considerado, os agrupamentos.
Agrupamentos
Os agrupamentos em Java são efetuados através inserção da expressão em parênteses, isto possibilita recuperar a String de determinado grupo uma vez que foi validado que a String se encontra no padrão requerido. A seguir demonstro uma maneira que, inicialmente, acreditei ser adequada para este fim.
Imagine que, após validar uma determinada data, você queira recuperar o dia, mês e ano separadamente para efetuar alguma lógica, então, teríamos algo como:
Expressão Regular: (\\d{2})/(\\d{2})/(\\d{4})
Português estruturado: A String em questão: Inicia com dois dígitos (numéricos) agrupados, seguido de dois dígitos (numéricos) agrupados, finaliza com quatro dígitos (númericos) agrupados e é delimitada por uma barra.
Código Java:
RegexComposer composer = RegexComposer.getInstance();
Pattern p = composer
.startsWith(exactly(2, digit()).grouped())
.followedBy(exactly(2, digit()).grouped())
.finishedWith(exactly(4, digit()).grouped())
.delimitedBy(exactly(1, literal("/")))
.compile();
Matcher matcher = p.matcher("05/01/1979");
if(matcher.matches()) {
System.out.println("Dia: " + matcher.group(1));
System.out.println("Mes: " + matcher.group(2));
System.out.println("Ano: " + matcher.group(3));
} else {
System.out.println("Formato invalido");
}
Legal, ao menos à primeira vista, entretanto eu ainda precisaria saber em qual grupo está o dia (matcher.group(1)), o mês (matcher.group(2)) e o ano (matcher.group(3)) e isto não é muito bacana. Creio que o ideal seria criarmos um alias para cada grupo e depois recuperarmos o número do grupo através do alias. Seria algo como .startsWith(exactly(2, digit()).grouped(usingAlias(”dia”))) e depois recuperarmos o grupo usando matcher.group(composer.getAliasGroup(”dia”)). Uma outra questão é que há outra interpretação para expressões agrupadas, por exemplo, “A String em questão: Inicia com um grupo de dois dígitos …”. Isto quer dizer que escreveriamos algo como: .startsWith(groupOf(exactly(2, digit())).usingAlias(”dia”)).
Embora eu considere estas opções adequadas, não escrevi nada disso ainda. No exemplo, a seguir, irei demonstrar um pouco mais de detalhes do que já implementei.
Exemplo prático
Utilizando o exemplo que o Martin Fowler deu em seu artigo, irei validar se a String “score 400 for 2 nights at Minas Tirith Airport” se encontra no padrão correto e ,após isto, recuperar os números 400 e 2.
Código Java:
import br.com.submundojava.regexcomposer.RegexComposer;
import static br.com.submundojava.regexcomposer.quantifier.Quantifiers.*;
import static br.com.submundojava.regexcomposer.value.MetaCharacters.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
/**
*
* @author Henrique Lima
*/
public class Test {
public static void main(String argz[]) {
RegexComposer composer = RegexComposer.getInstance();
Pattern pattern = composer
.startsWith(exactly(1, literal("score")))
.followedBy(oneOrMore(digit()).grouped())
.followedBy(exactly(1, literal("for")))
.followedBy(oneOrMore(digit()).grouped())
.followedBy(exactly(1, literal("night")))
.followedBy(zeroOrOne(literal("s")))
.followedBy(exactly(1, literal("at")))
.delimitedBy(zeroOrMore(whiteSpace()))
.finishedWith(oneOrMore(any()))
.compile();
Matcher matcher = pattern.matcher("score 400 for 2 nights at Minas Tirith Airport");
if (matcher.matches()) {
System.out.println(matcher.group(1));
System.out.println(matcher.group(2));
} else {
System.out.println("Formato inválido");
}
}
}
Muito simples! Veja que não utilizamos nenhum metacaracter, nenhum quantificador, agrupamos os dados necessários e ainda utilizamos um delimitador. Uma pessoa nem precisaria conhecer de expressões regulares para validar a String (embora seja desejável).
Perceba que novas coisas aconteceram e utilizamos import static do Java 5 para disponibilizarmos os métodos necessários à nossa classe. Além disso, utilizamos novos métodos quantificadores (não regex) que são: zeroOrMore(), para zero ou mais ocorrências (análogo a *), oneOrMore(), para uma ou mais ocorrências (análogo a +), zeroOrOne() para zero ou uma ocorrência (análogo a ?) e por final utilizamos o método compile() para obtermos um objeto do tipo java.util.regex.Pattern para podermos validar a String.
Código fonte
Abaixo segue o projeto que criei para efetuar este post.
Para netbeans: download
Para eclipse: download
Considerações Finais
Procurei demonstrar uma maneira fluente de trabalhar com expressões regulares, abstraindo seus recursos e disponibilizando métodos de alto nível. O exemplo que escrevi ainda está muito “verde”, com nomes esquisitos para algumas classes, métodos e pacotes. Na verdade, em alguns casos, nem sei se o inglês está correto.
Além disso, existem muitas limitações, por exemplo, se eu quiser usar um delimitador e ao mesmo tempo validar um dos tokens com um outro RegexComposer, não é possível. Seria desejável poder aninhar RegexComposer’s para este fim. Outro detalhe é que os métodos da classe RegexComposer recebem sempre uma instância de Quantifier e deveriam receber uma instância de uma classe com um nome mais adequado, como Expression ou ExpressionGroup. Detalhes a serem resolvidos.
Aguardo seus comentários e quem quiser entrar em contato, meu twitter é hgflima
Um abraço.
Posts (RSS)