ejemplo - text box java
La forma más rápida de saber si una cadena es una fecha válida (8)
Admito una biblioteca común en el trabajo que realiza muchas verificaciones de una cadena dada para ver si es una fecha válida. La API de Java, la biblioteca commons-lang y JodaTime tienen métodos que pueden analizar una cadena y convertirla en una fecha para avisarte si es realmente una fecha válida o no, pero esperaba que hubiera una manera de hacer la validación sin crear realmente un objeto de fecha (o DateTime como es el caso con la biblioteca JodaTime). Por ejemplo, aquí hay una pieza simple de código de ejemplo:
public boolean isValidDate(String dateString) {
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
try {
df.parse(dateString);
return true;
} catch (ParseException e) {
return false;
}
}
Esto me parece un desperdicio, estamos desechando el objeto resultante. Desde mis puntos de referencia, aproximadamente el 5% de nuestro tiempo en esta biblioteca común se dedica a validar fechas. Espero que me esté perdiendo una API obvia. ¡Cualquier sugerencia seria genial!
ACTUALIZAR
Supongamos que siempre podemos usar el mismo formato de fecha en todo momento (probablemente yyyyMMdd). También pensé en usar una expresión regular, pero entonces tendría que estar al tanto de la cantidad de días en cada mes, años bisiestos, etc.
Resultados
Analizó una fecha 10 millones de veces
Using Java''s SimpleDateFormat: ~32 seconds
Using commons-lang DateUtils.parseDate: ~32 seconds
Using JodaTime''s DateTimeFormatter: ~3.5 seconds
Using the pure code/math solution by Slanec: ~0.8 seconds
Using precomputed results by Slanec and dfb (minus filling cache): ~0.2 seconds
Hubo algunas respuestas muy creativas, lo aprecio! Supongo que ahora solo necesito decidir cuánta flexibilidad necesito para que se vea el código. Voy a decir que la respuesta de dfb es correcta porque fue puramente la más rápida que mis preguntas originales. ¡Gracias!
Si la línea siguiente lanza la excepción, es una fecha no válida; de lo contrario, se devolverá una fecha válida. Por favor, asegúrese de usar el DateTimeFormatter apropiado en la siguiente declaración.
LocalDate.parse (uncheckedStringDate, DateTimeFormatter.BASIC_ISO_DATE)
Creo que la mejor manera de saber si una fecha determinada es válida es definir un método como:
public static boolean isValidDate(String input, String format) {
boolean valid = false;
try {
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
String output = dateFormat.parse(input).format(format);
valid = input.equals(output);
} catch (Exception ignore) {}
return valid;
}
Por un lado, el método verifica que la fecha tenga el formato correcto y, por otro lado, que la fecha corresponda a una fecha válida. Por ejemplo, la fecha "2015/02/29" se analizará a "2015/03/01", por lo que la entrada y la salida serán diferentes, y el método devolverá false.
Esta es mi manera de verificar si la fecha está en el formato correcto y en realidad es una fecha válida. Supongamos que no necesitamos que SimpleDateFormat convierta la fecha incorrecta en una correcta, sino que un método simplemente devuelve falso. La salida a la consola se usa solo para comprobar cómo funciona el método en cada paso.
public class DateFormat {
public static boolean validateDateFormat(String stringToValidate){
String sdf = "yyyy-MM-dd HH:mm:ss";
SimpleDateFormat format=new SimpleDateFormat(sdf);
String dateFormat = "[12]{1,1}[0-9]{3,3}-(([0]{0,1}[1-9]{1,1})|([1]{0,1}[0-2]{1,1}))-(([0-2]{0,1}[1-9]{1,1})|([3]{0,1}[01]{1,1}))[ ](([01]{0,1}[0-9]{1,1})|([2]{0,1}[0-3]{1,1}))((([:][0-5]{0,1}[0-9]{0,1})|([:][0-5]{0,1}[0-9]{0,1}))){0,2}";
boolean isPassed = false;
isPassed = (stringToValidate.matches(dateFormat)) ? true : false;
if (isPassed){
// digits are correct. Now, check that the date itself is correct
// correct the date format to the full date format
String correctDate = correctDateFormat(stringToValidate);
try
{
Date d = format.parse(correctDate);
isPassed = (correctDate.equals(new SimpleDateFormat(sdf).format(d))) ? true : false;
System.out.println("In = " + correctDate + "; Out = "
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(d) + " equals = "
+ (correctDate.equals(new SimpleDateFormat(sdf).format(d))));
// check that are date is less than current
if (!isPassed || d.after(new Date())) {
System.out.println(new SimpleDateFormat(sdf).format(d) + " is after current day "
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
isPassed = false;
} else {
isPassed = true;
}
} catch (ParseException e) {
System.out.println(correctDate + " Exception! " + e.getMessage());
isPassed = false;
}
} else {
return false;
}
return isPassed;
}
/**
* method to fill up the values that are not full, like 2 hours -> 02 hours
* to avoid undesirable difference when we will compare original date with parsed date with SimpleDateFormat
*/
private static String correctDateFormat(String stringToValidate) {
String correctDate = "";
StringTokenizer stringTokens = new StringTokenizer(stringToValidate, "-" + " " + ":", false);
List<String> tokens = new ArrayList<>();
System.out.println("Inside of recognizer");
while (stringTokens.hasMoreTokens()) {
String token = stringTokens.nextToken();
tokens.add(token);
// for debug
System.out.print(token + "|");
}
for (int i=0; i<tokens.size(); i++){
if (tokens.get(i).length() % 2 != 0){
String element = tokens.get(i);
element = "0" + element;
tokens.set(i, element);
}
}
// build a correct final string
// 6 elements in the date: yyyy-MM-dd hh:mm:ss
// come through and add mandatory 2 elements
for (int i=0; i<2; i++){
correctDate = correctDate + tokens.get(i) + "-";
}
// add mandatory 3rd (dd) and 4th elements (hh)
correctDate = correctDate + tokens.get(2) + " " + tokens.get(3);
if (tokens.size() == 4){
correctDate = correctDate + ":00:00";
} else if (tokens.size() == 5){
correctDate = correctDate + ":" + tokens.get(4) + ":00";
} else if (tokens.size() == 6){
correctDate = correctDate + ":" + tokens.get(4) + ":" + tokens.get(5);
}
System.out.println("The full correct date format is " + correctDate);
return correctDate;
}
}
Una prueba de JUnit para eso:
import static org.junit.Assert.*;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(JUnitParamsRunner.class)
public class DateFormatTest {
@Parameters
private static final Object[] getCorrectDate() {
return new Object[] {
new Object[]{"2014-12-13 12:12:12"},
new Object[]{"2014-12-13 12:12:1"},
new Object[]{"2014-12-13 12:12:01"},
new Object[]{"2014-12-13 12:1"},
new Object[]{"2014-12-13 12:01"},
new Object[]{"2014-12-13 12"},
new Object[]{"2014-12-13 1"},
new Object[]{"2014-12-31 12:12:01"},
new Object[]{"2014-12-30 23:59:59"},
};
}
@Parameters
private static final Object[] getWrongDate() {
return new Object[] {
new Object[]{"201-12-13 12:12:12"},
new Object[]{"2014-12- 12:12:12"},
new Object[]{"2014- 12:12:12"},
new Object[]{"3014-12-12 12:12:12"},
new Object[]{"2014-22-12 12:12:12"},
new Object[]{"2014-12-42 12:12:12"},
new Object[]{"2014-12-32 12:12:12"},
new Object[]{"2014-13-31 12:12:12"},
new Object[]{"2014-12-31 32:12:12"},
new Object[]{"2014-12-31 24:12:12"},
new Object[]{"2014-12-31 23:60:12"},
new Object[]{"2014-12-31 23:59:60"},
new Object[]{"2014-12-31 23:59:50."},
new Object[]{"2014-12-31 "},
new Object[]{"2014-12 23:59:50"},
new Object[]{"2014 23:59:50"}
};
}
@Test
@Parameters(method="getCorrectDate")
public void testMethodHasReturnTrueForCorrectDate(String dateToValidate) {
assertTrue(DateFormat.validateDateFormatSimple(dateToValidate));
}
@Test
@Parameters(method="getWrongDate")
public void testMethodHasReturnFalseForWrongDate(String dateToValidate) {
assertFalse(DateFormat.validateDateFormat(dateToValidate));
}
}
Puede revertir su pensamiento: intente fallar lo más rápido posible cuando la Cadena definitivamente no tiene fecha:
- es
null
- su
length
no es 8 (según su formato de fecha de ejemplo) - contiene cualquier otra cosa que un número (si su formato de fecha es solo para fechas numéricas)
Si no se aplica ninguno de ellos, intente analizarlo, preferiblemente con un objeto de Format
estático prefabricado, no cree uno en cada ejecución de método.
EDITAR despues de comentarios
Sobre la base de este truco , escribí un método de validación rápida. Se ve feo, pero es significativamente más rápido que los métodos habituales de la biblioteca (¡que se deben usar en cualquier situación estándar!), Ya que se basa en el formato de fecha específico y no crea un objeto de Date
. Maneja la fecha como un int
y continúa a partir de eso.
daysInMonth()
método daysInMonth()
solo un poco ( la condición del año bisiesto tomada de Peter Lawrey ), así que espero que no haya un error aparente.
Una marca de microbado rápida (¡estimado!) Indicó una aceleración en un factor de 30.
public static boolean isValidDate(String dateString) {
if (dateString == null || dateString.length() != "yyyyMMdd".length()) {
return false;
}
int date;
try {
date = Integer.parseInt(dateString);
} catch (NumberFormatException e) {
return false;
}
int year = date / 10000;
int month = (date % 10000) / 100;
int day = date % 100;
// leap years calculation not valid before 1581
boolean yearOk = (year >= 1581) && (year <= 2500);
boolean monthOk = (month >= 1) && (month <= 12);
boolean dayOk = (day >= 1) && (day <= daysInMonth(year, month));
return (yearOk && monthOk && dayOk);
}
private static int daysInMonth(int year, int month) {
int daysInMonth;
switch (month) {
case 1: // fall through
case 3: // fall through
case 5: // fall through
case 7: // fall through
case 8: // fall through
case 10: // fall through
case 12:
daysInMonth = 31;
break;
case 2:
if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) {
daysInMonth = 29;
} else {
daysInMonth = 28;
}
break;
default:
// returns 30 even for nonexistant months
daysInMonth = 30;
}
return daysInMonth;
}
PS Su método de ejemplo anterior devolverá true
para "99999999". El mío solo devolverá verdadero para las fechas existentes :).
Se podría usar una combinación de regex y la verificación manual del año bisiesto. Así:
if (matches ^/d/d/d/d((01|03|05|07|08|10|12)(30|31|[012]/d)|(04|06|09|11)(30|[012]/d)|02[012]/d)$)
if (endsWith "0229")
return true or false depending on the year being a leap year
return true
return false
Si está realmente preocupado por el rendimiento y su formato de fecha es realmente tan simple, simplemente calcule previamente todas las cadenas válidas y córtelas en la memoria. El formato que tiene arriba solo tiene ~ 8 millones de combinaciones válidas hasta 2050
EDIT por Slanec - implementación de referencia
Esta implementación depende de su formato de fecha específico. Podría adaptarse a cualquier formato de fecha específico (como mi primera respuesta, pero un poco mejor).
Hace un conjunto de todas las dates
desde 1900 hasta 2050 (almacenadas como Cadenas - hay 54787 de ellas) y luego compara las fechas dadas con las almacenadas.
Una vez que se crea el conjunto de dates
, es rápido como el infierno. Una marca rápida de microbado mostró una mejora por un factor de 10 sobre mi primera solución.
private static Set<String> dates = new HashSet<String>();
static {
for (int year = 1900; year < 2050; year++) {
for (int month = 1; month <= 12; month++) {
for (int day = 1; day <= daysInMonth(year, month); day++) {
StringBuilder date = new StringBuilder();
date.append(String.format("%04d", year));
date.append(String.format("%02d", month));
date.append(String.format("%02d", day));
dates.add(date.toString());
}
}
}
}
public static boolean isValidDate2(String dateString) {
return dates.contains(dateString);
}
PS Puede modificarse para usar Set<Integer>
o incluso TIntHashSet
Trove , que reduce mucho el uso de la memoria (y por lo tanto permite usar un período de tiempo mucho mayor), el rendimiento luego cae a un nivel justo por debajo de mi solución original .
Sobre la base de la respuesta de dfb, podría hacer un hash de dos pasos.
- Cree un objeto simple (día, mes, año) que represente una fecha. Calcule cada día calendario durante los próximos 50 años, que deberían tener menos de 20 mil fechas diferentes.
- Haga una expresión regular que confirme si su cadena de entrada coincide con yyyyMMdd, pero no comprueba si el valor es un día válido (por ejemplo, se aprobará 99999999)
- La función de verificación primero hará una expresión regular, y si eso tiene éxito, pásela a la verificación de la función hash. Suponiendo que su objeto de fecha es un 8bit + 8bit + 8bit (para el año posterior a 1900), luego 24 bits * 20k, entonces toda la tabla hash debería ser bastante pequeña ... sin duda, menos de 500Kb, y muy rápida de cargar desde el disco si se serializa y comprimido.
public static int checkIfDateIsExists(String d, String m, String y) {
Integer[] array30 = new Integer[]{4, 6, 9, 11};
Integer[] array31 = new Integer[]{1, 3, 5, 7, 8, 10, 12};
int i = 0;
int day = Integer.parseInt(d);
int month = Integer.parseInt(m);
int year = Integer.parseInt(y);
if (month == 2) {
if (isLeapYear(year)) {
if (day > 29) {
i = 2; // false
} else {
i = 1; // true
}
} else {
if (day > 28) {
i = 2;// false
} else {
i = 1;// true
}
}
} else if (month == 4 || month == 6 || month == 9 || month == 11) {
if (day > 30) {
i = 2;// false
} else {
i = 1;// true
}
} else {
i = 1;// true
}
return i;
}
si devuelve i = 2 significa que la fecha no es válida y devuelve 1 si la fecha es válida