Cómo comprobar la cordura de una fecha en Java

Me parece curioso que la forma más obvia de crear objetos Date en Java haya quedado obsoleta y parece haber sido “sustituida” con un calendario no tan obvio como el uso indulgente.

¿Cómo verifica que una fecha, dada como una combinación de día, mes y año, sea una fecha válida?

Por ejemplo, 2008-02-31 (como en aaaa-mm-dd) sería una fecha no válida.

La forma actual es usar la clase de calendario. Tiene el método setLenient que validará la fecha y throw y excepción si está fuera de rango como en su ejemplo.

Olvidó agregar: si obtiene una instancia de calendario y establece la hora con su fecha, así es como obtiene la validación.

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

La clave es df.setLenient (falso); . Esto es más que suficiente para casos simples. Si está buscando una biblioteca más robusta (lo dudo) y / o alternativa como joda-time, mire la respuesta del usuario “tardate”

 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 se muestra en @Maglob, el enfoque básico es probar la conversión de cadena a fecha utilizando SimpleDateFormat.parse . Eso atrapará combinaciones de día / mes inválidas como 2008-02-31.

Sin embargo, en la práctica eso es raramente suficiente dado que SimpleDateFormat.parse es excesivamente liberal. Hay dos comportamientos que podrían preocuparle:

Caracteres no válidos en la cadena de fecha Sorprendentemente, 2008-02-2x “pasará” como una fecha válida con el formato de configuración regional = “aaaa-MM-dd”, por ejemplo. Incluso cuando isLenient == es falso.

Años: 2, 3 o 4 dígitos? Es posible que desee aplicar años de 4 dígitos en lugar de permitir el comportamiento predeterminado de SimpleDateFormat (que interpretará “12-02-31” de forma diferente según si su formato fue “aaaa-MM-dd” o “aa-MM-dd”). )

Una solución estricta con la biblioteca estándar

Por lo tanto, una prueba completa de cadena a fecha podría verse así: una combinación de concordancia de expresiones regulares, y luego una conversión de fecha forzada. El truco con la expresión regular es hacer que sea amigable con la configuración regional.

  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); 

Tenga en cuenta que la expresión regular asume que la cadena de formato contiene solo día, mes, año y caracteres separadores. Aparte de eso, el formato puede estar en cualquier formato de configuración regional: “d / MM / aa”, “aaaa-MM-dd”, y así sucesivamente. La cadena de formato para la configuración regional actual se puede obtener de esta manera:

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

Joda Time – ¿Mejor alternativa?

He estado escuchando sobre el tiempo de joda recientemente y pensé que lo compararía. Dos puntos:

  1. Parece mejor ser estricto sobre los caracteres no válidos en la cadena de fecha, a diferencia de SimpleDateFormat
  2. Todavía no veo una manera de aplicar años de 4 dígitos (pero supongo que podría crear su propio DateTimeFormatter para este propósito)

Es bastante simple 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; } 

Puedes usar SimpleDateFormat

Por ejemplo, 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

Con Date y Time API (clases java.time ) integradas en Java 8 y posterior, puede usar la clase 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 el modo estricto en java.time.DateTimeFormatter para analizar un LocalDate . Trampa para 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. ) 

Después del análisis, puede verificar el valor razonable. Por ejemplo, una fecha de nacimiento dentro de los últimos cien años.

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

Evite las clases heredadas de fecha y hora

Evite utilizar las molestas clases antiguas de fecha y hora incluidas con las primeras versiones de Java. Ahora suplantado por las clases java.time .

LocalDate & DateTimeFormatter & ResolverStyle

La clase LocalDate representa un valor de solo fecha sin hora del día y sin zona horaria.

 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 ); } 

La clase java.time.DateTimeFormatter se puede configurar para analizar cadenas con cualquiera de los tres modos de indulgencia definidos en la enumeración ResolverStyle . Insertamos una línea en el código de arriba para probar cada uno de los modos.

 f = f.withResolverStyle ( ResolverStyle.LENIENT ); 

Los resultados:

  • ResolverStyle.LENIENT
    ld: 2000-03-02
  • ResolverStyle.SMART
    ld: 2000-02-29
  • ResolverStyle.STRICT
    ERROR: java.time.format.DateTimeParseException: No se pudo analizar el texto ’31 / 02/2000 ‘: fecha inválida’ FEBRERO 31 ‘

Podemos ver que en el modo ResolverStyle.LENIENT , la fecha no válida se mueve hacia adelante un número equivalente de días. En el modo ResolverStyle.SMART (el valor predeterminado), se toma una decisión lógica para mantener la fecha dentro del mes y el último día posible del mes, el 29 de febrero en un año bisiesto, ya que no hay un día 31 en ese mes. El modo ResolverStyle.STRICT arroja una excepción quejándose de que no hay tal fecha.

Los tres son razonables según el problema y las políticas de su empresa. Parece que en su caso desea que el modo estricto rechace la fecha no válida en lugar de ajustarla.


Acerca de java.time

El marco java.time está integrado en Java 8 y posterior. Estas clases suplantan a las problemáticas antiguas clases heredadas de fecha y hora como java.util.Date , Calendar y SimpleDateFormat .

El proyecto Joda-Time , ahora en modo de mantenimiento , aconseja la migración a las clases java.time .

Para obtener más información, consulte el Tutorial de Oracle . Y busque Stack Overflow para obtener muchos ejemplos y explicaciones. La especificación es JSR 310 .

Puede intercambiar objetos java.time directamente con su base de datos. Use un controlador JDBC que cumpla con JDBC 4.2 o posterior. Sin necesidad de cadenas, sin necesidad de clases java.sql.* .

¿Dónde obtener las clases de java.time?

  • Java SE 8 , Java SE 9 y posterior
    • Incorporado.
    • Parte de la API Java estándar con una implementación integrada.
    • Java 9 agrega algunas características y correcciones menores.
  • Java SE 6 y Java SE 7
    • Gran parte de la funcionalidad de java.time se transfiere a Java 6 y 7 en ThreeTen-Backport .
  • Androide
    • Versiones posteriores de las implementaciones del paquete de Android de las clases java.time.
    • Para Android anterior (<26), el proyecto ThreeTenABP adapta ThreeTen-Backport (mencionado anteriormente). Consulte Cómo utilizar ThreeTenABP ….

El proyecto ThreeTen-Extra amplía java.time con clases adicionales. Este proyecto es un terreno de prueba para posibles adiciones futuras a java.time. Puede encontrar algunas clases útiles aquí, como Interval , YearWeek , YearQuarter y más .

Una solución estricta alternativa que utiliza la biblioteca estándar es realizar lo siguiente:

1) Cree un SimpleDateFormat estricto usando su patrón

2) Intentar analizar el valor ingresado por el usuario utilizando el objeto de formato

3) Si tiene éxito, vuelva a formatear la Fecha resultante de (2) usando el mismo formato de fecha (de (1))

4) Compare la fecha reformateada con el valor original ingresado por el usuario. Si son iguales, el valor ingresado se ajusta estrictamente a su patrón.

De esta forma, no es necesario crear expresiones regulares complejas; en mi caso, necesitaba admitir toda la syntax de patrones de SimpleDateFormat, en lugar de limitarme a ciertos tipos, como solo días, meses y años.

Basándome en la respuesta de @Pangea para solucionar el problema señalado por @ceklock , agregué un método para verificar que dateString no contenga ningún carácter no válido.

Así es como lo hago:

 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+"); } 

Le sugiero que use la clase org.apache.commons.validator.GenericValidator de apache.

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

Nota: strict – Si tiene o no una coincidencia exacta con datePattern.

Creo que lo más simple es convertir una cadena en un objeto de fecha y convertirla a una cadena. La cadena de fecha dada está bien si ambas cadenas aún coinciden.

 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; } 

Suponiendo que ambos sean cadenas (de lo contrario ya serían fechas válidas), aquí hay una forma:

 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); } } 

Aquí está la salida que obtengo:

 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 puede ver, maneja bien sus dos casos.

Esto está funcionando bien para mí. Enfoque sugerido arriba 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; } } 

Dos comentarios sobre el uso de SimpleDateFormat.

se debe declarar como una instancia estática si se declara como el acceso estático se debe sincronizar, ya que no es seguro para subprocesos

IME que es mejor que instanciar una instancia para cada análisis de una fecha.

Los métodos anteriores para el análisis de fechas son agradables, simplemente agregué un nuevo cheque en los métodos existentes que comprueban la fecha convertida con la fecha original utilizando el formateador, por lo que funciona para casi todos los casos, tal como lo verifiqué. por ejemplo, 29/02/2013 es una fecha no válida. La función dada analiza la fecha de acuerdo con los formatos de fecha aceptables actuales. Devuelve verdadero si la fecha no se analiza correctamente.

  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; } 

Esto es lo que hice para el entorno de Node que no usa 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); } } 

Y aquí está cómo usarlo:

 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; }