Самый быстрый способ узнать, является ли строка допустимой датой
Я поддерживаю общую библиотеку на работе, которая выполняет много проверок заданной строки, чтобы узнать, является ли она действительной датой. Java API, библиотека commons-lang и JodaTime имеют методы, которые могут анализировать строку и включать ее в дату, чтобы сообщить вам, действительно ли она действительная дата или нет, но я надеялся, что будет такой способ делать валидацию без фактического создания объекта даты (или DateTime, как в случае с библиотекой JodaTime). Например, вот простой пример кода примера:
public boolean isValidDate(String dateString) {
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
try {
df.parse(dateString);
return true;
} catch (ParseException e) {
return false;
}
}
Это кажется мне расточительным, мы отбрасываем полученный объект. Из моих тестов около 5% нашего времени в этой общей библиотеке проводится проверка дат. Я надеюсь, что у меня просто отсутствует очевидный API. Любые предложения были бы замечательными!
UPDATE
Предположим, что мы всегда можем использовать один и тот же формат даты (вероятно, yyyyMMdd). Я тоже думал об использовании регулярного выражения, но тогда ему нужно было бы знать количество дней в каждом месяце, високосные годы и т.д.
Результаты
Разработал дату 10 миллионов раз
Using Java SimpleDateFormat: ~32 seconds
Using commons-lang DateUtils.parseDate: ~32 seconds
Using JodaTime 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
Были очень творческие ответы, я ценю это! Думаю, теперь мне просто нужно решить, насколько мне нужна гибкость, чтобы я хотел, чтобы код выглядел. Я скажу, что ответ dfb правильный, потому что это был самый быстрый, который был моим оригинальным вопросом. Спасибо!
Ответы
Ответ 1
Если вы действительно обеспокоены производительностью, а формат даты - это просто, просто предварительно вычислите все допустимые строки и хэш их в памяти. Формат, который у вас выше, имеет только ~ 8 миллионов действительных комбинаций до 2050
EDIT от Slanec - эталонная реализация
Эта реализация зависит от вашего конкретного формата даты. Он может быть адаптирован к любой конкретной форме даты (как и мой первый ответ, но немного лучше).
Он создает набор из всех dates
с 1900 по 2050 год (хранится как строки - из них 54787), а затем сравнивает указанные даты с сохраненными.
Как только набор dates
создается, он быстро, как черт. Быстрый микробенчмарк показал улучшение в 10 раз по сравнению с моим первым решением.
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);
}
P.S. Его можно изменить, чтобы использовать Set<Integer>
или даже Trove TIntHashSet
, что значительно сокращает использование памяти (и, следовательно, позволяет использовать гораздо больший промежуток времени), производительность затем снижается до уровня чуть ниже моего первоначального решения.
Ответ 2
Вы можете вернуть свое мышление - попытайтесь сбой как можно быстрее, когда строка не имеет даты:
Если ни одно из них не применяется, попробуйте проанализировать его - предпочтительно с предварительно созданным статическим объектом Format
, не создавайте его при каждом запуске метода.
ИЗМЕНИТЬ после комментариев
Основываясь на этом аккуратном трюке, я написал быстрый метод проверки. Он выглядит уродливым, но значительно быстрее, чем обычные методы библиотеки (которые должны использоваться в любой стандартной ситуации!), Поскольку он зависит от вашего конкретного формата даты и не создает объект Date
. Он обрабатывает дату как int
и продолжается от этого.
Я немного протестировал метод daysInMonth()
(условие високосного года, взятое из Питера Лоури), поэтому я надеюсь, что там нет явной ошибки.
Быстрое (оцененное!) микропредметное обозначение показало ускорение в 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;
}
P.S. Ваш примерный метод выше вернет true
для "99999999". Mine вернет true только для существующих дат:).
Ответ 3
Я думаю, что лучший способ узнать, действительна ли определенная дата, - это определить такой метод, как:
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;
}
С одной стороны, метод проверяет, что дата имеет правильный формат, а с другой стороны проверяет, соответствует ли дата действительной дате. Например, дата "2015/02/29" будет проанализирована до "2015/03/01", поэтому вход и выход будут разными, и метод вернет false.
Ответ 4
Это мой способ проверить, соответствует ли дата правильному формату и фактически является допустимой датой. Предположим, нам не нужен SimpleDateFormat, чтобы преобразовать неверную дату в правильную, но вместо этого метод просто возвращает false.
Вывод на консоль используется только для проверки того, как метод работает на каждом шаге.
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;
}
}
Тест JUnit для этого:
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));
}
}
Ответ 5
Основываясь на ответе dfb, вы можете сделать двухэтапный хеш.
- Создайте простой объект (день, месяц, год), представляющий дату. Вычислить каждый календарный день в течение следующих 50 лет, который должен быть менее 20 тыс. Разных дат.
- Сделайте регулярное выражение, которое подтверждает, соответствует ли ваша строка ввода yyyyMMdd, но не проверяет, действительно ли значение является действительным днем (например, 99999999)
- Функция проверки сначала сделает регулярное выражение, и если это удастся - передайте его на проверку хэш-функции. Предполагая, что ваш объект даты имеет 8 бит + 8 бит + 8 бит (за год после 1900 года), тогда 24 бит * 20 тыс., Тогда вся хэш-таблица должна быть довольно маленькой... конечно, до 500 Кб, и очень быстрая загрузка с диска, если она сериализована и сжатый.
Ответ 6
Можно использовать комбинацию регулярного и ручного проверки високосного года. Таким образом:
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
Ответ 7
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;
}
если он возвращает я = 2, означает, что дата недействительна и возвращает 1, если дата действительна.
Ответ 8
Если следующая строка выдает исключение, то это неверно date else, это вернет действительную дату. Убедитесь, что вы используете соответствующий DateTimeFormatter в следующем выражении.
LocalDate.parse(uncheckedStringDate, DateTimeFormatter.BASIC_ISO_DATE)