关于java:判断字符串是否为有效日期的最快方法

Fastest way to tell if a string is a valid date

我在工作中支持一个公共库,该库对给定的字符串执行许多检查以查看其是否为有效日期。 Java API,commons-lang库和JodaTime都具有可以解析字符串并将其转换为日期的方法,以便让您知道它是否实际上是一个有效的日期,但是我希望有一种方法无需实际创建日期对象(或使用JodaTime库的DateTime)而进行验证的方法。例如,这是一段简单的示例代码:

1
2
3
4
5
6
7
8
9
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)。我确实也考虑过使用正则表达式,但随后需要知道每个月的天数,leap年等等。

结果

解析日期一千万次

1
2
3
4
5
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

有一些非常有创意的答案,我很感激!我想现在我只需要确定我需要代码看起来像什么的灵活性即可。我要说的是dfb的答案是正确的,因为它纯粹是最快的,这是我最初提出的问题。谢谢!


如果您真的很在意性能,而您的日期格式真的那么简单,那么只需预先计算所有有效字符串并将它们散列到内存中即可。您上面的格式在2050年之前只有约800万个有效组合

Slanec编辑-参考实现

此实现取决于您的特定日期格式。它可以适应那里的任何特定日期格式(就像我的第一个答案一样,但要好一些)。

它对1900至2050年的所有dates进行了设置(存储为字符串-其中有54787个),然后将给定的日期与存储的日期进行比较。

一旦创建了dates集,它的速度就快到了地狱。快速的微基准测试显示比我的第一个解决方案提高了10倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}

附:可以对其进行修改以使用Set或什至Trove的TIntHashSet,这可以大大减少内存使用(因此允许使用更长的时间跨度),然后性能下降到仅比我的原始解决方案低的水平。


您可以重新思考-当String绝对不是日期时,尝试尽快失败:

  • 它是null
  • length不是8(根据您的示例日期格式!)
  • 它包含数字以外的任何内容(如果您的日期格式仅用于数字日期)

如果这些都不适用,则尝试解析它-最好使用预制的静态Format对象,不要在每次运行的方法上都创建一个对象。

评论后编辑

基于这个巧妙的技巧,我编写了一种快速验证方法。它看起来很丑陋,但比通常的库方法(在任何标准情况下都应使用!)要快得多,因为它依赖于您特定的日期格式,并且不会创建Date对象。它以int的形式处理日期,并从那开始。

我只测试了daysInMonth()方法(Peter Lawrey提出的leap年条件),所以我希望没有明显的错误。

快速(估计!)微基准测试表明速度提高了30倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;
}

附:上面的示例方法将为" 99999999"返回true。我的只会对存在的日期返回true :)。


我认为,了解某个日期是否有效的更好方法是定义一个方法,例如:

1
2
3
4
5
6
7
8
9
10
11
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。


这是我检查日期是否采用正确格式并且实际上是有效日期的方法。假定我们不需要SimpleDateFormat将错误的日期转换为正确的日期,而是一种方法仅返回false。
输出到控制台仅用于检查该方法在每个步骤中的工作方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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));
    }

}

If following line throws exception then it is invalid date else this will return valid date. Please make sure you use appropriate DateTimeFormatter in the following statement.

LocalDate.parse(uncheckedStringDate,DateTimeFormatter.BASIC_ISO_DATE)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 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;
    }

如果返回i = 2,则表示日期无效;如果日期有效,则返回1


可以结合使用正则表达式和手动leap年检查。从而:

1
2
3
4
5
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

根据dfb的答案,您可以执行两步哈希。

  • 创建一个表示日期的简单对象(日,月,年)。计算下一个50年的每个日历日,应该少于2万个不同的日期。
  • 进行正则表达式以确认您的输入字符串是否与yyyyMMdd相匹配,但不检查该值是否为有效日期(例如,将通过99999999)
  • 检查函数将首先执行正则表达式,如果成功,则将其传递给哈希函数检查。假设您的日期对象是8位+ 8位+ 8位(对于1900年之后的年份),则为24位* 20k,则整个哈希表应该非常小...肯定在500Kb以下,并且如果序列化并从磁盘非常快速地加载,压缩。