segunda-feira, 16 de junho de 2003

Tutorial: Validação de formato de horas usando expressões regulares

Há pouco tempo, tive um pequeno problema para resolver: verificar se horas digitadas pelos usuários, no formato HH:MM:SS, eram válidas. A princípio, não parece ser uma tarefa complexa, mas, dependendo da abordagem utilizada, podemos ter pouco ou muito trabalho.


Pare um momento e reflita: como fazer tal validação ? Usando JavaScript, poderíamos pensar em usar o método .split() para decompormos os elementos da string digitada e verificar se estas partes compõem um conjunto válido. Talvez o código acabasse, no final, parecido com o código abaixo:

<script>
function isHoraValidaSplit(strHora)
{
 // Concatenamos dois "dois pontos" para o caso da string
 // passada não contê-los, pois esperamos um array com,
 // no mínimo, 3 posições: horas, minutos e segundos.
 var arrElementos  = ( strHora + "::" ).split( ":" );
 var bolHorasOk    = consistirIntervalo( arrElementos[0], 0, 23 );
 var bolMinutosOk  = consistirIntervalo( arrElementos[1], 0, 59 );
 var bolSegundosOk = consistirIntervalo( arrElementos[2], 0, 59 );
 return( bolHorasOk && bolMinutosOk && bolSegundosOk );
 // Quando uma function aparece dentro de outra function,
 // tem sua visibilidade reduzida. Assim, consistirIntervalo()
 // só poderá ser acessada dentro de isHoraValidaSplit().
 function consistirIntervalo( strValor, intMenor, intMaior )
 {
  var bolRetorno = true;
  // Valor informado precisa ser numérico...
  if( isNaN( strValor ) || strValor == "" ){
   bolRetorno = false;
  }
  else
  {
   // ... e estar contido no intervalo
   var intValor = parseInt( strValor, 10 )
   if( intValor < intMenor || intValor > intMaior ){
    bolRetorno = false;
   }
  }
  return( bolRetorno );
 }
}
</script>

Para sabermos se o código acima funciona, utilizamos uma massa de dados de teste. Note que temos inconsistências em diversos pontos da hora, para podermos explorar cada ponto do algoritmo. Temos horas com apenas os minutos inválidos, apenas os segundos válidos, testamos os limites inferior e superior. Uma boa massa de testes pode ajudá-lo a identificar diversos problemas latentes do seu código, antes que o usuário final descubra esses erros para você :)

<script>
// Massa para testes da função isHoraValida()
var strMensagem = "Método Split:\n--------------------\n"
var arrTestes = new Array(
  "12:34:56", "65:43:21", "02:68:03", "21:43:65", "00:00:00", "23:59:59",
  "24:00:00", "teste123", "11:77:88", "55:66:11", "33:00:99", "99:99:99" )
for( var i=0; i < arrTestes.length; i++ ){
 strMensagem +=
  "\"" + arrTestes[i] + "\" = " +
  isHoraValidaSplit(arrTestes[i]) + "\n";
}
alert( strMensagem )
</script>

"Ok, a função funciona. Então, o que há de errado com ela ?"


Na verdade, não há nada de errado. Minha intenção é de apresentar outro forma de fazer a consistência de horas, usando expressões regulares, lembra ? Vamos começar analisado uma hora válida: "19:48:26". Mas o que torna "19:48:26" uma hora válida ?


Comecemos com simplicidades lógicas: uma hora válida possui dois números, um sinal de dois-pontos, mais dois números, outro sinal de dois-pontos e mais dois números. Apesar de um passo óbvio, já começamos a isolar um padrão, que é justamente do que trata as expressões regulares: a pesquisa de padrões em strings.


Podemos, então, começar a reescrever a função de validação, assim:

<script>
function isHoraValidaRegExp(strHora){
 var re = /^\d\d:\d\d:\d\d$/
 return re.test( strHora );
}
</script>

Como o leitor mais atento deve ter percebido, "\d" é a indicação de dígito numérico. A cada dois deles, temos o sinal de dois-pontos. O caracter "^" indica início da string a ser testada e o "$" indica seu término. Isso consiste a "máscara básica" de uma hora válida.


"Perae! Mas este código está chamando de 'válida' horas descaradamente inválidas!"


Calma, jovem Jedi. Acredite na Força.


Precisamos consistir agora o intervalo de horas, minutos e segundos. Como consistir segundos é a mesma coisa que consistir minutos, vamos continuar por aqui.


O que é um "minuto válido" ? São os minutos entre "00" e "59", inclusive. Então precisamos incrementar a expressão regular, pois não são quaisquer dois números válidos, tal como está codificado hoje:

<script>
function isHoraValidaRegExp(strHora){
 var re = /^\d\d:[0-5]\d:[0-5]\d$/
 return re.test( strHora );
}
</script>

É isso mesmo que você está pensando: os colchetes indicam um conjunto de valores possíveis, e o traço indica um ubtervalo, no nosso caso, os números compreendidos entre "0" e "5". O caracter seguinte permanece sendo o "\d", pois unidades de "0" a "9" são permitidas.


"Não olhe agora, mas ainda está consistindo errado!"


Sim, eu sei. Ainda precisamos consistir as horas. E, aproveitando o embalo, o que é uma "hora válida" ? São as horas compreendidas entre "00" e "23", inclusive. Assim, teremos que adaptar a linha de raciocínio que adotamos no passo anterior. Ficaria assim:

<script>
function isHoraValidaRegExp(strHora){
 var re = /^[01]\d:[0-5]\d:[0-5]\d$/
 return re.test( strHora );
}
</script>

Assim resolvemos o problema das horas compreendidas entre "00" e "19". Precisamos tratar a exceção. Como proceder ?


Pensando mais um pouco: as horas precisam estar contidas em "00" e "19" ou entre "20" e "23". Significa que há dois padrões que necessitamos avaliar. Note que eles são mutuamente exclusivos, ou seja, só pode ocorrer um de cada vez. O código ficaria assim:

<script>
function isHoraValidaRegExp(strHora){
 var re = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
 return re.test( strHora );
}
</script>

Cuidado! Note bem que alteramos o código que vinha antes do primeiro "dois-pontos". Mantivemos a expressão anterior, que validava horas até as 19 horas, acrescentamos um pipe ("|") que representa o "OU" e acrescentamos a condição que faltava "2, seguido de um número entre 0 e 3". Colocamos tudo isso dentro de uma indicação de padrão não-capturável, representado por "(?:)". Os detalhes desta indicação fico devendo para outro artigo.


Para podermos comparar ambas implementações, preparei um teste de performance, que segue abaixo:

<script>
// Método original
function isHoraValidaSplit(strHora)
{
 // Concatenamos dois "dois pontos" para o caso da string
 // passada não contê-los, pois esperamos um array com,
 // no mínimo, 3 posições: horas, minutos e segundos.
 var arrElementos  = ( strHora + "::" ).split( ":" );
 var bolHorasOk    = consistirIntervalo( arrElementos[0], 0, 23 );
 var bolMinutosOk  = consistirIntervalo( arrElementos[1], 0, 59 );
 var bolSegundosOk = consistirIntervalo( arrElementos[2], 0, 59 );
 return( bolHorasOk && bolMinutosOk && bolSegundosOk );
 // Quando uma function aparece dentro de outra function,
 // tem sua visibilidade reduzida. Assim, consistirIntervalo()
 // só poderá ser acessada dentro de isHoraValidaSplit().
 function consistirIntervalo( strValor, intMenor, intMaior )
 {
  var bolRetorno = true;
  // Valor informado precisa ser numérico...
  if( isNaN( strValor ) || strValor == "" ){
   bolRetorno = false;
  }
  else
  {
   // ... e estar contido no intervalo
   var intValor = parseInt( strValor, 10 )
   if( intValor < intMenor || intValor > intMaior ){
    bolRetorno = false;
   }
  }
  return( bolRetorno );
 }
}
// Método sugerido
function isHoraValidaRegExp(strHora){
 var re = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/
 return re.test( strHora );
}
function testarSplit(intLoop){
 var arrTestes = new Array(
   "12:34:56", "65:43:21", "02:68:03", "21:43:65", "00:00:00", "23:59:59",
   "24:00:00", "teste123", "11:77:88", "55:66:11", "33:00:99", "99:99:99" )
 for( var n=0; n <= intLoop; n++ )
 {
  var strMensagem = "Método Split:\n--------------------\n"
  for( var i=0; i < arrTestes.length; i++ )
  {
   strMensagem +=
    "\"" + arrTestes[i] + "\" = " +
    isHoraValidaSplit(arrTestes[i]) + "\n";
  }
 }
 return strMensagem
}
function testarRegExp(intLoop){
 // Massa para testes da função isHoraValida()
 var arrTestes = new Array(
   "12:34:56", "65:43:21", "02:68:03", "21:43:65", "00:00:00", "23:59:59",
   "24:00:00", "teste123", "11:77:88", "55:66:11", "33:00:99", "99:99:99" )
 for( var n=0; n <= intLoop; n++ )
 {
  var strMensagem = "Método RegExp:\n--------------------\n"
  for( var i=0; i < arrTestes.length; i++ )
  {
   strMensagem +=
    "\"" + arrTestes[i] + "\" = " +
    isHoraValidaRegExp(arrTestes[i]) + "\n";
  }
 }
 return strMensagem
}
</script>
<script language="VBScript">
 Const intExecucoes = 5 'Tiramos a média de 5 execuções
 Const intLoop = 1000 'Cada execução avalia 12 horas, 1000 vezes.
 intAcumuladoSplit = 0
 strMensagemSplit  = ""
 For i = 1 to intExecucoes
  inicio = Timer
  strMensagemSplit  = testarSplit(intLoop)
  intAcumuladoSplit = intAcumuladoSplit + ( Timer - inicio )
 Next
 intAcumuladoRegExp = 0
 strMensagemRegExp  = ""
 For i = 1 to intExecucoes
  inicio = Timer
  strMensagemRegExp  = testarRegExp(intLoop)
  intAcumuladoRegExp = intAcumuladoRegExp + ( Timer - inicio )
 Next
 ' Computar o tempo médio dispendido na avaliação das 60.000 validações
 intMediaSplit  = intAcumuladoSplit  / intExecucoes
 intMediaRegExp = intAcumuladoRegExp / intExecucoes
 strMensagemSplit  = strMensagemSplit  & vbTab & vbLF & "=" & _
      formatNumber( intMediaSplit, 4 ) & " s"
 strMensagemRegExp = strMensagemRegExp & vbTab & vbLF & "=" & _
      formatNumber( intMediaRegExp,4 ) & " s"
 arrSplit  = split( strMensagemSplit , vbLF )
 arrRegExp = split( strMensagemRegExp, vbLF )
 strMensagem = ""
 For i = 0 To UBound( arrSplit )
  strMensagem = strMensagem & arrSplit(i) & vbTab & "|  " & arrRegExp(i) & vbCRLF
 Next
 MsgBox strMensagem & vbCRLF & vbTab & _
   " Diferença de " & FormatPercent( intMediaSplit / intMediaRegExp, 2 )
</script>

Pronto, pode rodar o teste e surpreenda-se (o teste completo levou quase 4 segundos num P3 650/512M RAM. Você verá que a implementação da validação com expressões regulares possui a mesma eficiência do algoritmo inicial, mas com 90% menos código e cerca de 307,5% mais rápido! Isso mesmo: trezentos e sete e meio por cento, ou um terço do tempo original. Se o salário mínimo aumentasse na mesma proporção, ia ter muita gente feliz, pois o mesmo passaria de R$ 240,00 para R$ 738,00. Um aumento pra lá de bem vindo.


Bom, fica a dica: expressões regulares, que num primeiro instante parecem complexas, podem resolver problemas de identificação de padrão com menos código e mais performance. Mas nunca esqueça de documentar bem suas expressões regulares. A vítima pode ser você.



Até a próxima!



Esta matéria foi postada originalmente no ASP4Developers por Rubens N. Farias (site), que na época era "pós-graduado em análise de sistemas orientados a objetos, MCP, MCSD, MCAD, MCSD.NET e consultor em TI, além de idealizador do projeto ASP4Developers. Desenvolve sistemas sob medida, focados na satisfação do usuário, com qualidade e custo realista.". Hoje, vai saber...

0 comentários: