如何在Java中进行日期合法性检查

93

我觉得很奇怪,在Java中创建Date对象最明显的方式已经被弃用,并似乎被一个不太容易使用的宽容日历所替代。

如何检查作为日、月和年组合给出的日期是否是有效日期?

例如,2008-02-31(即yyyy-mm-dd)将是无效日期。


对于任何有类似问题的人,请考虑您是否需要非公历日历支持。 - MSalters
25个回答

91

关键在于df.setLenient(false);。这对于简单情况已经足够了。如果您正在寻找更健壮的(我怀疑)和/或类似joda-time的替代库,那么请查看用户“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;
        }
}

3
请尝试使用“09-04-201a”。这将创建一个不合法的日期。 - ceklock
@ceklock 这就是它的工作方式,无论您是否使用 setLenientSimpleDateFormat 将始终解析直到匹配模式并忽略字符串的其余部分,因此您会得到 201 作为年份。 - Daniel Naber
@ceklock 我在我的解决方案中刚刚提到了它。 这可能会为某人节省一两分钟的时间。 - Sufian
1
引入异常处理会导致性能大幅下降,因此如果您预计在正常操作中出现格式错误的输入(例如验证用户输入),那么这可能是一个糟糕的设计。但是,如果该方法用作对始终应该有效的输入进行双重检查(除了错误),那么就可以接受。 - RavenMan
2
请注意,像 java.util.Datejava.util.Calendarjava.text.SimpleDateFormat 等旧的日期时间类现已成为遗留系统,并已被 Java 8 及以上版本内置的 java.time 类所取代。请参阅 Oracle 的 教程 - Basil Bourque

49

正如@Maglob所示,基本方法是使用SimpleDateFormat.parse测试从字符串到日期的转换。这将捕获无效的日/月组合,如2008-02-31。

然而,在实践中,这通常是不够的,因为SimpleDateFormat.parse非常宽容。有两种行为可能会让您担心:

日期字符串中存在无效字符 令人惊讶的是,例如使用locale格式 =“yyyy-MM-dd”,甚至当isLenient == false时,2008-02-2x也将“通过”作为有效日期。

年份:2、3或4位数字? 您可能还想强制执行4位数字的年份,而不是允许默认的SimpleDateFormat行为(这将根据您的格式是“yyyy-MM-dd”还是“yy-MM-dd”而以不同方式解释“12-02-31”)。

使用标准库的严格解决方案

因此,完整的字符串转日期测试可能看起来像这样:正则表达式匹配和强制日期转换的组合。正则表达式的技巧是使其符合语言环境。

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

请注意该正则表达式假定格式字符串仅包含日、月、年和分隔符字符。除此之外,格式可以是任何区域设置格式,例如:"d/MM/yy","yyyy-MM-dd"等等。可以像这样获取当前区域设置的格式字符串:

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

Joda Time - 更好的替代品?

最近我听说了 joda time ,并且想进行比较。以下是两个要点:

  1. joda time 在日期字符串中对无效字符的限制方面似乎比 SimpleDateFormat 更好。
  2. 目前看不到强制执行四位数年份的方法(但我猜你可以为此创建自己的DateTimeFormatter)。

它非常容易使用:

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

最终我选择了joda替代方案,并自己检查值是否与模式长度匹配... - Aritz
更新:可怕的旧遗留类(DateSimpleDateFormat等)现已被现代java.time类所取代。同样,Joda-Time项目处于维护模式,并建议迁移到java.time类。 - Basil Bourque

42

简短概述

使用严格模式java.time.DateTimeFormatter来解析LocalDate。捕获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.
)

在解析后,您可能需要检查合理的值。例如,一个出生日期在过去一百年内。

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

避免使用旧的日期时间类

避免使用最早版本的Java中附带的麻烦的旧日期时间类。现在已经被java.time类所取代。

LocalDate & DateTimeFormatter & ResolverStyle

LocalDate类表示仅包含日期值而不包含时间和时区的值。

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

java.time.DateTimeFormatter类可以设置为使用ResolverStyle枚举中定义的任意三种宽容模式解析字符串。我们在上述代码中插入一行以尝试每种模式。

f = f.withResolverStyle ( ResolverStyle.LENIENT );

结果:

  • ResolverStyle.LENIENT
    日期:2000年3月2日
  • ResolverStyle.SMART
    日期:2000年2月29日
  • ResolverStyle.STRICT
    错误:java.time.format.DateTimeParseException: 无法解析文本“31/02/2000”:无效日期“FEBRUARY 31”
我们可以看到,在ResolverStyle.LENIENT模式下,无效日期向前移动相同数量的天数。在ResolverStyle.SMART模式(默认模式)下,会做出一个逻辑决策,将日期保持在该月份内,并选择该月份最后一天,即闰年的2月29日,因为该月没有31日。ResolverStyle.STRICT模式会抛出异常,指出没有这样的日期。
这三种模式都是合理的,具体取决于您的业务问题和政策。听起来在您的情况下,您想要使用严格模式拒绝无效日期而不是调整它。

Table of all date-time types in Java, both modern and legacy.


关于 java.time

java.time 框架内置于 Java 8 及以后版本。这些类替代了老旧的日期时间类,如 遗留java.util.DateCalendarSimpleDateFormat

要了解更多,请参见Oracle教程。并在Stack Overflow上搜索许多示例和说明。规范是JSR 310Joda-Time项目现在处于维护模式,建议迁移到java.time类。
您可以直接与数据库交换java.time对象。使用符合JDBC 4.2或更高版本的JDBC驱动程序。无需字符串,也无需使用java.sql.*类。
如何获取java.time类?

Table of which java.time library to use with which version of Java or Android

ThreeTen-Extra项目通过增加类扩展了java.time。该项目是java.time可能未来添加的内容的试验场。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuarter更多


假设该项目正在使用Java 8+进行编译,那么你的答案是正确的。当项目在Java 8中时,我也使用这些类。但有时我不得不接触那些丑陋的古老JSP脚本(甚至不支持Java 7)。在那里验证日期是一种痛苦。因此我来到这里。尽管你的答案是最正确的方法...结论:我完了。 - KarelG
@KarelG 请重新阅读关于回溯到Java 6和Java 7的倒数第二段。我没有在回溯版本中验证这种行为,但建议您尝试一下。 - Basil Bourque

40

您可以使用SimpleDateFormat

例如:

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

2
这种方法的一个问题是它会接受0003-0002-001。 - despot
1
另一个问题是将月份设置为13会返回一个日期,其中月份为次年的01月。 - 8bitjunkie
请注意,像java.util.Datejava.util.Calendarjava.text.SimpleDateFormat这样的旧日期时间类现在已经成为遗留系统,被内置于Java 8及更高版本中的java.time类所取代。请参阅Oracle的教程 - Basil Bourque

39

目前的方法是使用Calendar类。它具有setLenient方法,该方法将验证日期并在超出范围时引发异常,就像您的示例一样。

忘记添加: 如果您获取一个日历实例并使用您的日期设置时间,这就是您获得验证的方式。

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

3
不要认为这个代码可以直接使用。Calendar.setTime需要一个java.util.Date类型的参数,所以在你得到"yourDate"对象之前,字符串已经被转换了。 - tardate
35
示例代码存在三个问题:
  1. 在获取日历实例后,必须调用cal.setLenient(false)。否则,例如Feb 31 2007这样的日期会被认为是有效的。
  2. 调用cal.setTime()方法不会抛出异常。您必须在setTime()调用之后调用cal.getTime(),如果日期无效,将抛出异常。
  3. 括号缺失:在catch之前缺少'}'。
- Liron Yahdav
这种方法也比其他方法慢。请参见https://dev59.com/hHI95IYBdhLWcg3wyRM0#18252071 - despot
6
FYI,像 java.util.Datejava.util.Calendarjava.text.SimpleDateFormat 这些麻烦的旧日期时间类现在已经过时,被 Java 8 及更高版本内置的 java.time 类所取代。请参见 Oracle 的 教程 - Basil Bourque
4
不验证像2月31日这样的日期。 - shikha singh

16

java.time

使用内置于Java 8及更高版本的日期和时间APIjava.time类),您可以使用LocalDate类。

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

2
默认情况下,此代码使用ResolverStyle.SMART,该值会将结果调整为有效日期而不是抛出异常。因此,此代码无法实现问题的目标。请参见我的答案,其中包括使用ResolverStyle.STRICT的示例和解决方案。 - Basil Bourque
@BasilBourqueno,不是这样的。我刚测试了一下,LocalDate.of()的工作方式与.withResolverStyle(ResolverStyle.STRICT)DateTimeFormatter非常相似。 - Det

8

Aravind的答案 基础上,修复了 ceklock 在评论中指出的问题。我添加了一个方法来验证 dateString 是否包含任何无效字符。

具体做法如下:

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

7
一种使用标准库的严格解决方法是执行以下步骤:
1) 使用您的模式创建一个严格的SimpleDateFormat;
2) 尝试使用格式化对象解析用户输入的值;
3) 如果成功,使用相同的日期格式(来自(1))重新格式化 (2) 的结果得到的日期;
4) 将重新格式化的日期与原始用户输入的值进行比较。 如果它们相等,则输入的值严格匹配您的模式。
这样,您就不需要创建复杂的正则表达式 - 在我的情况下,我需要支持SimpleDateFormat的所有模式语法,而不仅仅是某些类型,比如只有天、月和年。

这绝对是正确的方法,另请参阅网站http://www.dreamincode.net/forums/topic/14886-date-validation-using-simpledateformat/上的示例代码。 - Victor Ionescu

5

我认为最简单的方法是将字符串转换为日期对象,然后再将其转换回字符串。如果两个字符串仍然匹配,给定的日期字符串就可以了。

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

5

我建议你使用来自apache的org.apache.commons.validator.GenericValidator类。

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

注意: strict - 是否需要严格匹配日期格式。


我已经添加了使用GenericValidator方法的答案: https://dev59.com/RHVC5IYBdhLWcg3wpS3f#68069005 - SANAT

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接