1. 引言

正则表达式是处理模式匹配的强大工具,但用起来容易踩坑。本文将使用 java.util.regex 包,通过正则表达式判断字符串是否包含有效日期。

⚠️ 如果对正则基础不熟,建议先参考 Java 正则表达式 API 指南

2. 日期格式概述

我们采用国际通用的公历(Gregorian calendar),日期格式严格遵循 YYYY-MM-DD 模式。特别注意闰年规则:

闰年判定规则:年份能被4整除但不能被100整除,或者能被400整除。其他年份均为平年。

✅ 有效日期示例:

  • 2017-12-31
  • 2020-02-29(闰年)
  • 2400-02-29(闰年)

❌ 无效日期示例:

  • 2017/12/31(分隔符错误)
  • 2018-1-1(缺少前导零)
  • 2018-04-31(4月没有31日)
  • 2100-02-29(2100不是闰年)

3. 实现解决方案

先定义一个 DateMatcher 接口作为基础:

public interface DateMatcher {
    boolean matches(String date);
}

下面逐步构建完整实现:

3.1. 匹配基本格式

先实现最简单的格式校验:

class FormattedDateMatcher implements DateMatcher {
    private static Pattern DATE_PATTERN = Pattern.compile(
      "^\\d{4}-\\d{2}-\\d{2}$");

    @Override
    public boolean matches(String date) {
        return DATE_PATTERN.matcher(date).matches();
    }
}

这个正则要求:

  • 四位年份 + 两位月份 + 两位日期
  • 用短横线分隔

✅ 匹配示例:2017-12-31, 0000-00-00
❌ 不匹配:2018-01, 2020/02/29

3.2. 匹配特定日期范围

增加数值范围约束(限定年份1900-2999):

^((19|2[0-9])[0-9]{2})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$

拆解规则:

  • 年份:19xx20xx-29xx
  • 月份:01-12
  • 日期:01-31

✅ 匹配:1900-01-01, 2999-12-31
❌ 不匹配:1899-12-31, 2018-05-35

3.3. 匹配闰年2月29日

专门处理闰年2月29日:

^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$

关键逻辑:

  • 2000|2400|2800:能被400整除的闰年
  • (19|2[0-9])(0[48]|[2468][048]|[13579][26]):能被4整除但不能被100整除的年份

✅ 匹配:2020-02-29, 2400-02-29
❌ 不匹配:2100-02-29(非闰年)

3.4. 匹配2月其他日期

处理2月1-28日(所有年份通用):

^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$

✅ 匹配:2018-02-01, 2020-02-25
❌ 不匹配:2000-02-30(2月没有30日)

3.5. 匹配31天的月份

处理31天的月份(1,3,5,7,8,10,12月):

^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$

✅ 匹配:2018-01-31, 2021-07-31
❌ 不匹配:2018-01-32(日期超限)

3.6. 匹配30天的月份

处理30天的月份(4,6,9,11月):

^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$

✅ 匹配:2018-04-30, 2019-06-30
❌ 不匹配:2018-04-31(4月只有30日)

3.7. 完整公历日期匹配器

合并所有规则构建最终方案:

class GregorianDateMatcher implements DateMatcher {
    private static Pattern DATE_PATTERN = Pattern.compile(
      "^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$" 
      + "|^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$"
      + "|^(((19|2[0-9])[0-9]{2})-(0[13578]|10|12)-(0[1-9]|[12][0-9]|3[01]))$" 
      + "|^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$");

    @Override
    public boolean matches(String date) {
        return DATE_PATTERN.matcher(date).matches();
    }
}

使用 | 分隔四种匹配规则:

  1. 闰年2月29日
  2. 所有年份2月1-28日
  3. 31天的月份
  4. 30天的月份

💡 这个正则没做性能优化,重点在可读性。实际使用时可考虑精简。

3.8. 性能警告

⚠️ 复杂正则可能严重影响性能!本文主要展示正则的灵活性,**生产环境推荐直接用 LocalDate.parse()**:

// 简单粗暴的替代方案
try {
    LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE);
    return true;
} catch (DateTimeParseException e) {
    return false;
}

4. 结论

通过逐步构建,我们实现了:

  • 严格格式校验(YYYY-MM-DD
  • 月份日期范围验证
  • 闰年特殊处理

完整代码见 GitHub 仓库(Maven项目,可直接运行)。

✅ 总结:正则能搞定日期验证,但实际开发中,用 LocalDate.parse() 更省心。


原始标题:| Baeldung