1. 简介
如果使用得当,正则表达式是匹配各种模式的强大工具。
在本文中,我们将使用java.util.regex包来确定给定的字符串是否包含有效日期。
有关正则表达式的介绍,请参阅Java正则表达式API指南。
2. 日期格式概述
我们将根据国际公历定义一个有效日期,我们的格式将遵循以下通用模式:YYYY-MM-DD。
我们再引入闰年的概念,即年份中包含2月29日。根据公历,如果年份数字能被4整除(除了能被100整除的年份,包括能被400整除的年份),我们就称该年份为闰年。
在所有其他情况下,我们将某一年称为常规年。
有效日期示例:
- 2017-12-31
- 2020-02-29
- 2400-02-29
无效日期的示例:
- 2017/12/31:错误的分隔符
- 2018-1-1:缺少前导0
- 2018-04-31:四月份天数计算错误
- 2100-02-29:由于值除以100,因此今年不是闰年,因此二月限制为28天
3. 实现解决方案
因为我们要使用正则表达式来匹配日期,所以我们首先勾勒出一个接口DateMatcher,它提供一个matches方法:
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();
}
}
这里我们指定有效日期必须由3组整数组成,每组之间用短划线分隔。第一组由4个整数组成,其余两组各由2个整数组成。
匹配日期:2017-12-31、2018-01-31、0000-00-00、1029-99-72
不匹配日期:2018-01、2018-01-XX、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])$
这里我们引入了三组需要匹配的整数范围:
(19|2[0-9])[0-9]{2}
通过匹配以19或2X开头后跟几个任意数字的数字来覆盖有限的年份范围0[1-9]|1[012]
匹配01-12范围内的月份数字0[1-9]|[12][0-9]|3[01]
匹配01-31范围内的日期数字
匹配日期:1900-01-01、2205-02-31、2999-12-31
不匹配日期:1899-12-31、2018-05-35、2018-13-05、3000-01-01、2018-01-XX
3.3 匹配2月29日
为了正确匹配闰年,我们必须首先确定何时遇到闰年,然后确保我们接收2月29日作为这些年份的有效日期。
由于我们限制范围内的闰年数量足够大,我们应该使用适当的可除性规则来过滤它们:
- 如果一个数的最后两位数字能被4整除,那么这个数也能被4整除
- 如果数字的最后两位是00,则该数字可以被100整除
这是一个解决方案:
^((2000|2400|2800|(19|2[0-9])(0[48]|[2468][048]|[13579][26]))-02-29)$
该模式由以下部分组成:
2000|2400|2800
在1900-2999的限制范围内匹配一组闰年,分隔符为40019|2[0-9](0[48]|[2468][048]|[13579][26]))
匹配所有白名单中的年份组合,这些组合的分隔符为4,但不包含100-02-29
2月2日比赛
匹配日期:2020-02-29、2024-02-29、2400-02-29
不匹配日期:2019-02-29、2100-02-29、3200-02-29、2020/02/29
3.4 匹配二月份的一般日期
除了匹配闰年的2月29日之外,我们还需要匹配所有年份的2月的所有其他日子(1日至28日):
^(((19|2[0-9])[0-9]{2})-02-(0[1-9]|1[0-9]|2[0-8]))$
匹配日期:2018-02-01、2019-02-13、2020-02-25
不匹配日期:2000-02-30、2400-02-62、2018/02/28
3.5 匹配31天月份
一月、三月、五月、七月、八月、十月和十二月的月份应匹配1至31天:
^(((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、2022-08-31
不匹配日期:2018-01-32、2019-03-64、2018/01/31
3.6 匹配30天月份
四月、六月、九月和十一月的月份应匹配1至30天:
^(((19|2[0-9])[0-9]{2})-(0[469]|11)-(0[1-9]|[12][0-9]|30))$
匹配日期:2018-04-30、2019-06-30、2020-09-30
不匹配日期:2018-04-31、2019-06-31、2018/04/30
3.7 公历日期匹配器
现在我们可以将上述所有模式组合成一个匹配器,以获得满足所有约束的完整的GregorianDateMatcher:
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到28的任意一天),其余月份的日期匹配第三和第四个分支。 |
由于我们尚未优化此模式以提高可读性,因此请随意尝试其长度。
此时我们已经满足了一开始引入的所有约束。
3.8 性能说明
解析复杂的正则表达式可能会显著影响执行流程的性能,本文的主要目的并非学习一种有效的方法来测试一个字符串是否包含在所有可能日期的集合中。
如果需要一种可靠且快速的方法来验证日期,请考虑使用Java 8提供的LocalDate.parse()。
4. 总结
在本文中,我们学习了如何使用正则表达式来匹配公历的严格格式的日期,并提供格式、范围和月份长度的规则。
Post Directory
