RegEx para correspondência de padrão de data em Java
1. Introdução
Expressões regulares são uma ferramenta poderosa para combinar vários tipos de padrões quando usadas adequadamente.
Neste artigo, usaremos o pacotejava.util.regex para determinar se um determinadoString contém uma data válida ou não.
Para uma introdução às expressões regulares, consulteour Guide To Java Regular Expressions API.
2. Visão geral do formato de data
Vamos definir uma data válida em relação ao calendário gregoriano internacional. Nosso formato seguirá o padrão geral:YYYY-MM-DD.
Vamos incluir também o conceito de um anoleap que é um ano contendo um dia 29 de fevereiro. According to the Gregorian calendar, we’ll call a year leap if the year number can be divided evenly by 4 except for those which are divisible by 100 but including those which are divisible by 400.
Em todos os outros casos, chamaremos um ano deregular.
Exemplos de datas válidas:
-
31-12-2017
-
29/02/2020
-
2400-02-29
Exemplos de datas inválidas:
-
2017/12/31: delimitador de token incorreto
-
2018-1-1: faltando zeros à esquerda
-
2018-04-31: contagem de dias errada para abril
-
2100-02-29: este ano não é um salto, pois o valor se divide por100, então fevereiro é limitado a 28 dias
3. Implementando uma Solução
Como vamos combinar uma data usando expressões regulares, vamos primeiro esboçar uma interfaceDateMatcher, que fornece um único métodomatches:
public interface DateMatcher {
boolean matches(String date);
}
Apresentaremos a implementação passo a passo abaixo, visando uma solução completa no final.
3.1. Combinando o formato amplo
Começaremos criando um protótipo muito simples que lida com as restrições de formato de nosso matcher:
class FormattedDateMatcher implements DateMatcher {
private static Pattern DATE_PATTERN = Pattern.compile(
"^\\d{4}-\\d{2}-\\d{2}$");
@Override
public boolean matches(String date) {
return DATE_PATTERN.matcher(date).matches();
}
}
Aqui, estamos especificando quea valid date must consist of three groups of integers separated by a dash. O primeiro grupo é composto de quatro inteiros, com os dois grupos restantes tendo dois inteiros cada.
Datas de correspondência:2017-12-31,2018-01-31,0000-00-00,1029-99-72
Datas não correspondentes:2018-01,2018-01-XX,2020/02/29
3.2. Correspondência com o formato de data específico
Nosso segundo exemplo aceita intervalos de tokens de data, bem como nossa restrição de formatação. Por simplicidade, restringimos nosso interesse aos anos 1900 - 2999.
Agora que correspondemos com sucesso ao nosso formato geral de data, precisamos restringir ainda mais isso - para garantir que as datas estejam realmente corretas:
^((19|2[0-9])[0-9]{2})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$
Aqui, introduzimos trêsgroups de intervalos inteiros que precisam corresponder:
-
(19|2[0-9])[0-9]{2}
cobre um intervalo restrito de anos, combinando um número que começa com19 ou2X seguido por alguns dígitos. -
0[1-9]|1[012]
corresponde a um número de mês em um intervalo de01-12 -
0[1-9]|[12][0-9]|3[01]
corresponde a um número de dia em um intervalo de01-31
Datas de correspondência:1900-01-01,2205-02-31,2999-12-31
Datas não correspondentes:1899-12-31,2018-05-35,2018-13-05,3000-01-01,2018-01-XX
3.3. Correspondendo a 29 de fevereiro
Para combinar os anos bissextos corretamente, devemos primeiroidentify when we have encountered a leap year e, em seguida, certificar-se de que aceitamos 29 de fevereiro como uma data válida para esses anos.
Como o número de anos bissextos em nosso intervalo restrito é grande o suficiente, devemos usar as regras de divisibilidade apropriadas para filtrá-las:
-
Se o número formado pelos dois últimos dígitos em um número for divisível por 4, o número original será divisível por 4
-
Se os dois últimos dígitos do número forem 00, o número será divisível por 100
Aqui está uma solução:
^((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)$
O padrão consiste nas seguintes partes:
-
2000|2400|2800
corresponde a um conjunto de anos bissextos com um divisor de400 em um intervalo restrito de1900-2999 -
19|2[0-9](0[48]|[2468][048]|[13579][26]))
corresponde a todas as combinações dewhite-list de anos que têm um divisor de4 e não têm um divisor de100 -
-02-29
corresponde aFebruary 2nd
Datas de correspondência:2020-02-29,2024-02-29,2400-02-29
Datas não correspondentes:2019-02-29,2100-02-29,3200-02-29,2020/02/29
3.4. Combinando dias gerais de fevereiro
Além de corresponder a 29 de fevereiro em anos bissextos,we also need to match all other days of February (1 – 28) in all years:
^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$
Datas de correspondência:2018-02-01,2019-02-13,2020-02-25
Datas não correspondentes:2000-02-30,2400-02-62,2018/02/28
3.5. Meses de 31 dias correspondentes
Os meses de janeiro, março, maio, julho, agosto, outubro e dezembro devem corresponder entre 1 e 31 dias:
^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$
Datas de correspondência:2018-01-31,2021-07-31,2022-08-31
Datas não correspondentes:2018-01-32,2019-03-64,2018/01/31
3.6. Meses de 30 dias correspondentes
Os meses de abril, junho, setembro e novembro devem corresponder entre 1 e 30 dias:
^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$
Datas de correspondência:2018-04-30,2019-06-30,2020-09-30
Datas não correspondentes:2018-04-31,2019-06-31,2018/04/30
3.7. Gregorian Date Matcher
Agora podemoscombine all of the patterns above into a single matcher to have a complete GregorianDateMatcher satisfazendo todas as restrições:
class GregorianDateMatcher implements DateMatcher {
private static Pattern DATE_PATTERN = Pattern.compile(
"^((2000|2400|2800|(19|2[0-9](0[48]|[2468][048]|[13579][26])))-02-29)$"
+ "|^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$"
+ "|^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$"
+ "|^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$");
@Override
public boolean matches(String date) {
return DATE_PATTERN.matcher(date).matches();
}
}
We’ve used an alternation character “|” to match at least one dos quatro ramos. Portanto, a data válida de fevereiro corresponde à primeira ramificação de 29 de fevereiro de um ano bissexto ou à segunda ramificação de qualquer dia de1 a28. As datas dos meses restantes correspondem à terceira e à quarta ramificações.
Como não otimizamos esse padrão em favor de uma melhor legibilidade, sinta-se à vontade para experimentar um comprimento dele.
Neste momento, satisfazemos todas as restrições que introduzimos no início.
3.8. Nota sobre o desempenho
Parsing complex regular expressions may significantly affect the performance of the execution flow. O objetivo principal deste artigo não era aprender uma maneira eficiente de testar uma string quanto à sua participação em um conjunto de todas as datas possíveis.
Considere o uso deLocalDate.parse() fornecido pelo Java8 se uma abordagem confiável e rápida para validar uma data for necessária.
4. Conclusão
Neste artigo, aprendemos como usar expressões regulares para corresponder à data estritamente formatada do calendário gregoriano, fornecendo regras de formato, intervalo e também a duração dos meses.
Todo o código apresentado neste artigo está disponívelover on Github. Este é um projeto baseado em Maven, portanto, deve ser fácil importar e executar como está.