Como sanidade verificar uma data em Java

Acho curioso que a maneira mais óbvia de criar objects Date em Java tenha sido preterida e pareça ter sido “substituída” por um não tão óbvio para usar o calendar leniente.

Como você verifica se uma data, dada como uma combinação de dia, mês e ano, é uma data válida?

Por exemplo, 2008-02-31 (como em aaaa-mm-dd) seria uma data inválida.

A maneira atual é usar a class de calendar. Tem o método setLenient que validará a data e lançará e a exceção se estiver fora do intervalo, como no seu exemplo.

Esqueceu de adicionar: Se você obtiver uma instância do calendar e definir a hora usando sua data, é assim que você obtém a validação.

 Calendar cal = Calendar.getInstance(); cal.setLenient(false); cal.setTime(yourDate); try { cal.getTime(); } catch (Exception e) { System.out.println("Invalid date"); } 

A chave é df.setLenient (false); . Isso é mais que suficiente para casos simples. Se você está procurando por uma biblioteca mais robusta (eu duvido) e / ou alternativas como o joda-time então olhe aqui (não a resposta aceita mas a resposta do usuário chamado “tardate”): Como verificar uma data em java

 final static String DATE_FORMAT = "dd-MM-yyyy"; public static boolean isDateValid(String date) { try { DateFormat df = new SimpleDateFormat(DATE_FORMAT); df.setLenient(false); df.parse(date); return true; } catch (ParseException e) { return false; } } 

Como mostrado por @Maglob, a abordagem básica é testar a conversão da cadeia de caracteres até a data usando SimpleDateFormat.parse . Isso vai pegar combinações dia / mês inválidas como 2008-02-31.

No entanto, na prática isso raramente é suficiente, já que o SimpleDateFormat.parse é extremamente liberal. Existem dois comportamentos com os quais você pode se preocupar:

Caracteres inválidos na cadeia de datas Surpreendentemente, 2008-02-2x irá “passar” como uma data válida com formato de localidade = “aaaa-MM-dd” por exemplo. Mesmo quando isLenient == false.

Anos: 2, 3 ou 4 dígitos? Você também pode impor anos de 4 dígitos em vez de permitir o comportamento padrão de SimpleDateFormat (que interpretará “12-02-31” de forma diferente, dependendo de o formato ser “aaaa-MM-dd” ou “aa-MM-dd”). )

Uma solução estrita com a biblioteca padrão

Portanto, um teste completo de string para data poderia ter esta aparência: uma combinação de correspondência de expressão regular e, em seguida, uma conversão de data forçada. O truque com o regex é torná-lo localmente amigável.

  Date parseDate(String maybeDate, String format, boolean lenient) { Date date = null; // test date string matches format structure using regex // - weed out illegal characters and enforce 4-digit year // - create the regex based on the local format string String reFormat = Pattern.compile("d+|M+").matcher(Matcher.quoteReplacement(format)).replaceAll("\\\\d{1,2}"); reFormat = Pattern.compile("y+").matcher(reFormat).replaceAll("\\\\d{4}"); if ( Pattern.compile(reFormat).matcher(maybeDate).matches() ) { // date string matches format structure, // - now test it can be converted to a valid date SimpleDateFormat sdf = (SimpleDateFormat)DateFormat.getDateInstance(); sdf.applyPattern(format); sdf.setLenient(lenient); try { date = sdf.parse(maybeDate); } catch (ParseException e) { } } return date; } // used like this: Date date = parseDate( "21/5/2009", "d/M/yyyy", false); 

Observe que a expressão regex assume que a string de formato contém apenas caracteres de dia, mês, ano e separador. Além disso, o formato pode estar em qualquer formato de local: “d / MM / aa”, “aaaa-MM-dd” e assim por diante. A string de formato para a localidade atual pode ser obtida assim:

 Locale locale = Locale.getDefault(); SimpleDateFormat sdf = (SimpleDateFormat)DateFormat.getDateInstance(DateFormat.SHORT, locale ); String format = sdf.toPattern(); 

Joda Time – Melhor Alternativa?

Eu tenho ouvido sobre o tempo da joda recentemente e pensei em comparar. Dois pontos:

  1. Parece ser melhor em ser estrito sobre caracteres inválidos na cadeia de datas, ao contrário de SimpleDateFormat
  2. Ainda não consigo encontrar uma maneira de aplicar anos com 4 dígitos (mas eu acho que você poderia criar seu próprio DateTimeFormatter para essa finalidade)

É bem simples de usar:

 import org.joda.time.format.*; import org.joda.time.DateTime; org.joda.time.DateTime parseDate(String maybeDate, String format) { org.joda.time.DateTime date = null; try { DateTimeFormatter fmt = DateTimeFormat.forPattern(format); date = fmt.parseDateTime(maybeDate); } catch (Exception e) { } return date; } 

Você pode usar SimpleDateFormat

Por exemplo, algo como:

 boolean isLegalDate(String s) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); sdf.setLenient(false); return sdf.parse(s, new ParsePosition(0)) != null; } 

java.time

Com a API Date and Time (classs java.time ) incorporada ao Java 8 e posterior, você pode usar a class LocalDate .

 public static boolean isDateValid(int year, int month, int day) { boolean dateIsValid = true; try { LocalDate.of(year, month, day); } catch (DateTimeException e) { dateIsValid = false; } return dateIsValid; } 

tl; dr

Use o modo estrito em java.time.DateTimeFormatter para analisar um LocalDate . Armadilha para o DateTimeParseException .

 LocalDate.parse( // Represent a date-only value, without time-of-day and without time zone. "31/02/2000" , // Input string. DateTimeFormatter // Define a formatting pattern to match your input string. .ofPattern ( "dd/MM/uuuu" ) .withResolverStyle ( ResolverStyle.STRICT ) // Specify leniency in tolerating questionable inputs. ) 

Após a análise, você pode verificar o valor razoável. Por exemplo, uma data de nascimento nos últimos cem anos.

 birthDate.isAfter( LocalDate.now().minusYears( 100 ) ) 

Evite classs legadas de data e hora

Evite usar as antigas classs de data / hora problemáticas fornecidas com as primeiras versões do Java. Agora suplantado pelas classs java.time .

LocalDate & DateTimeFormatter & ResolverStyle

A class LocalDate representa um valor somente de datas sem hora do dia e sem fuso horário.

 String input = "31/02/2000"; DateTimeFormatter f = DateTimeFormatter.ofPattern ( "dd/MM/uuuu" ); try { LocalDate ld = LocalDate.parse ( input , f ); System.out.println ( "ld: " + ld ); } catch ( DateTimeParseException e ) { System.out.println ( "ERROR: " + e ); } 

A class java.time.DateTimeFormatter pode ser configurada para analisar cadeias com qualquer um dos três modos leniency definidos no enum ResolverStyle . Nós inserimos uma linha no código acima para testar cada um dos modos.

 f = f.withResolverStyle ( ResolverStyle.LENIENT ); 

Os resultados:

  • ResolverStyle.LENIENT
    ld: 2000-03-02
  • ResolverStyle.SMART
    ld: 2000-02-29
  • ResolverStyle.STRICT
    ERRO: java.time.format.DateTimeParseException: o texto ’31 / 02/2000 ‘não pôde ser analisado: data inválida’ FEVEREIRO 31 ‘

Podemos ver que no modo ResolverStyle.LENIENT , a data inválida é movida para frente um número equivalente de dias. No modo ResolverStyle.SMART (o padrão), uma decisão lógica é feita para manter a data no mês e ir com o último dia possível do mês, 29 de fevereiro em um ano bissexto, pois não há 31 dias naquele mês. O modo ResolverStyle.STRICT lança uma exceção reclamando que não existe tal data.

Todos os três são razoáveis, dependendo do problema e das políticas do seu negócio. Parece que no seu caso você quer que o modo estrito rejeite a data inválida em vez de ajustá-la.


Sobre o java.time

O framework java.time está embutido no Java 8 e posterior. Essas classs substituem as antigas classs herdadas de data e hora legadas , como java.util.Date , Calendar e SimpleDateFormat .

O projeto Joda-Time , agora em modo de manutenção , aconselha a migration para as classs java.time .

Para saber mais, veja o Oracle Tutorial . E pesquise o Stack Overflow para muitos exemplos e explicações. A especificação é JSR 310 .

Você pode trocar objects java.time diretamente com seu database. Use um driver JDBC compatível com o JDBC 4.2 ou posterior. Não há necessidade de strings, não há necessidade de classs java.sql.* .

Onde obter as classs java.time?

  • Java SE 8 , Java SE 9 e posterior
    • Construídas em.
    • Parte da API Java padrão com uma implementação integrada.
    • O Java 9 adiciona alguns resources e correções menores.
  • Java SE 6 e Java SE 7
    • Grande parte da funcionalidade java.time é transferida para o Java 6 e 7 no ThreeTen-Backport .
  • Android
    • Versões posteriores das implementações do pacote Android das classs java.time.
    • Para Android anterior (<26), o projeto ThreeTenABP adapta o ThreeTen-Backport (mencionado acima). Veja Como usar o ThreeTenABP… .

O projeto ThreeTen-Extra estende java.time com classs adicionais. Este projeto é um campo de testes para possíveis futuras adições ao java.time. Você pode encontrar algumas classs úteis aqui como Interval , YearWeek , YearQuarter e muito mais .

Uma solução estrita alternativa usando a biblioteca padrão é executar o seguinte:

1) Crie um SimpleDateFormat estrito usando seu padrão

2) Tente analisar o valor inserido pelo usuário usando o object de formato

3) Se for bem sucedido, reformate a data resultante de (2) usando o mesmo formato de data (de (1))

4) Compare a data reformatada com o valor original inserido pelo usuário. Se eles forem iguais, o valor inserido corresponderá estritamente ao seu padrão.

Dessa forma, você não precisa criar expressões regulares complexas – no meu caso, eu precisava suportar toda a syntax de padrões do SimpleDateFormat, em vez de me limitar a certos tipos, como dias, meses e anos.

Com base na resposta do @Pangea para corrigir o problema apontado pelo @ceklock , adicionei um método para verificar se o dateString não contém nenhum caractere inválido.

Aqui está como eu faço:

 private boolean isDateCorrect(String dateString) { try { Date date = mDateFormatter.parse(dateString); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); return matchesOurDatePattern(dateString); //added my method } catch (ParseException e) { return false; } } /** * This will check if the provided string matches our date format * @param dateString * @return true if the passed string matches format 2014-1-15 (YYYY-MM-dd) */ private boolean matchesDatePattern(String dateString) { return dateString.matches("^\\d+\\-\\d+\\-\\d+"); } 

Eu sugiro que você use a class org.apache.commons.validator.GenericValidator do apache.

GenericValidator.isDate(String value, String datePattern, boolean strict);

Nota: estrito – se deve ou não ter uma correspondência exata do datePattern.

Eu acho que o mais simples é apenas converter uma string em um object de data e convertê-lo de volta para uma string. A string de data fornecida é boa se ambas as strings ainda corresponderem.

 public boolean isDateValid(String dateString, String pattern) { try { SimpleDateFormat sdf = new SimpleDateFormat(pattern); if (sdf.format(sdf.parse(dateString)).equals(dateString)) return true; } catch (ParseException pe) {} return false; } 

Supondo que ambos sejam Strings (caso contrário, eles já seriam datas válidas), aqui está uma maneira:

 package cruft; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateValidator { private static final DateFormat DEFAULT_FORMATTER; static { DEFAULT_FORMATTER = new SimpleDateFormat("dd-MM-yyyy"); DEFAULT_FORMATTER.setLenient(false); } public static void main(String[] args) { for (String dateString : args) { try { System.out.println("arg: " + dateString + " date: " + convertDateString(dateString)); } catch (ParseException e) { System.out.println("could not parse " + dateString); } } } public static Date convertDateString(String dateString) throws ParseException { return DEFAULT_FORMATTER.parse(dateString); } } 

Aqui está a saída que recebo:

 java cruft.DateValidator 32-11-2010 31-02-2010 04-01-2011 could not parse 32-11-2010 could not parse 31-02-2010 arg: 04-01-2011 date: Tue Jan 04 00:00:00 EST 2011 Process finished with exit code 0 

Como você pode ver, ele lida bem com os dois casos.

Isso está funcionando muito bem para mim. Abordagem sugerida acima por Ben.

 private static boolean isDateValid(String s) { SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); try { Date d = asDate(s); if (sdf.format(d).equals(s)) { return true; } else { return false; } } catch (ParseException e) { return false; } } 

Dois comentários sobre o uso de SimpleDateFormat.

ele deve ser declarado como uma instância estática se declarado como access estático deve ser sincronizado, pois não é thread-safe

IME que é melhor que instanciar uma instância para cada análise de uma data.

Acima dos methods de análise de data são bons, eu acabei de adicionar novos methods de verificação existentes que checam a data convertida com a data original usando o formatador, então funciona para quase todos os casos conforme eu verifiquei. Por exemplo, 02/29/2013 é uma data inválida. Dada a function, analise a data de acordo com os formatos de data aceitos atuais. Retorna true se a data não for analisada com sucesso.

  public final boolean validateDateFormat(final String date) { String[] formatStrings = {"MM/dd/yyyy"}; boolean isInvalidFormat = false; Date dateObj; for (String formatString : formatStrings) { try { SimpleDateFormat sdf = (SimpleDateFormat) DateFormat.getDateInstance(); sdf.applyPattern(formatString); sdf.setLenient(false); dateObj = sdf.parse(date); System.out.println(dateObj); if (date.equals(sdf.format(dateObj))) { isInvalidFormat = false; break; } } catch (ParseException e) { isInvalidFormat = true; } } return isInvalidFormat; } 

Aqui está o que eu fiz para o ambiente Node usando sem bibliotecas externas:

 Date.prototype.yyyymmdd = function() { var yyyy = this.getFullYear().toString(); var mm = (this.getMonth()+1).toString(); // getMonth() is zero-based var dd = this.getDate().toString(); return zeroPad([yyyy, mm, dd].join('-')); }; function zeroPad(date_string) { var dt = date_string.split('-'); return dt[0] + '-' + (dt[1][1]?dt[1]:"0"+dt[1][0]) + '-' + (dt[2][1]?dt[2]:"0"+dt[2][0]); } function isDateCorrect(in_string) { if (!matchesDatePattern) return false; in_string = zeroPad(in_string); try { var idate = new Date(in_string); var out_string = idate.yyyymmdd(); return in_string == out_string; } catch(err) { return false; } function matchesDatePattern(date_string) { var dateFormat = /[0-9]+-[0-9]+-[0-9]+/; return dateFormat.test(date_string); } } 

E aqui está como usá-lo:

 isDateCorrect('2014-02-23') true 
 // to return valid days of month, according to month and year int returnDaysofMonth(int month, int year) { int daysInMonth; boolean leapYear; leapYear = checkLeap(year); if (month == 4 || month == 6 || month == 9 || month == 11) daysInMonth = 30; else if (month == 2) daysInMonth = (leapYear) ? 29 : 28; else daysInMonth = 31; return daysInMonth; } // to check a year is leap or not private boolean checkLeap(int year) { Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, year); return cal.getActualMaximum(Calendar.DAY_OF_YEAR) > 365; }