`
saybody
  • 浏览: 869340 次
  • 性别: Icon_minigender_2
  • 来自: 西安
文章分类
社区版块
存档分类
最新评论

正则应用之——日期正则表达式

阅读更多

1 概述

首先需要说明的一点,无论是Winform,还是Webform,都有很成熟的日历控件,无论从易用性还是可扩展性上看,日期的选择和校验还是用日历控件来实现比较好。

前几天在CSDN多个版块看到需要日期正则的帖子,所以整理了这篇文章,和大家一起讨论交流,如有遗漏或错误的地方,还请大家指正。

日期正则一般是对格式有要求,且数据不是直接由用户输入时使用。因应用场景的不同,写出的正则也不同,复杂程度也自然不同。正则的书写需要根据具体情况具体分析,一个基本原则就是:只写合适的,不写复杂的

对于日期提取,只要能与非日期区分开,写最简单的正则即可,如

\d{4}-\d{2}-\d{2}

如果可以在源字符串中唯一定位yyyy-MM-dd格式的日期,则可用做提取。

对于验证,如果仅仅是验证字符组成及格式是没有多大意义的,还要加入对规则的校验。由于闰年的存在,使得日期的校验正则变得比较复杂。

先来考察一下日期的有效范围以及什么是闰年。

2 日期的规则

2.1 日期的有效范围

对于日期的有效范围,不同的应用场景会有所不同。

MSDN中定义的DateTime对象的有效范围是:0001-01-01 00:00:009999-12-31 23:59:59

UNIX时间戳的0按照ISO 8601规范为 1970-01-01T00:00:00Z

而实际应用中,日期的范围基本上不会超出DateTime所规定的范围,所以正则验证取其中常用的日期范围即可。

2.2 什么是闰年

(以下摘自百度百科)

闰年(leap year)是为了弥补因人为历法规定造成的年度天数与地球实际公转周期的时间差而设立的。补上时间差的年份为闰年。

地球绕日运行周期为3655小时4846秒(合365.24219天),即一回归年tropical year)。公历的平年只有365日,比回归年短约0.2422 日,每四年累积约一天,把这一天加于2月末(即229日),使当年时间长度变为366日,这一年就为闰年。

需要注意的是,现在的公历是根据罗马人的“儒略历”改编而得。由于当时没有了解到每年要多算出0.0078天的问题,从公元前46年,到16世纪,一共累计多出了10天。为此,当时的教皇格雷果里十三世,将1582105日人为规定为1015日。并开始了新闰年规定。即规定公历年份是整百数的,必须是400的倍数才是闰年,不是400的倍数的就是平年。比如,1700年、1800年和1900年为平年,2000年为闰年。此后,平均每年长度为365.2425天,约4年出现1天的偏差。按照每四年一个闰年计算,平均每年就要多算出0.0078天,经过四百年就会多出大约3天来,因此,每四百年中要减少三个闰年。闰年的计算,归结起来就是通常说的:四年一闰;百年不闰,四百年再闰

2.3 日期的格式

根据不同的语言文化,日期的连字符会有所不同,通常有以下几种格式:

yyyyMMdd

yyyy-MM-dd

yyyy/MM/dd

yyyy.MM.dd

3 日期正则表达式构建

3.1 规则分析

写复杂正则的一个常用方法,就是先把不相关的需求拆分开,分别写出对应的正则,然后组合,检查一下相互的关联关系以及影响,基本上就可以得出对应的正则。

按闰年的定义可知,日期可以有几种分类方法。

3.1.1 根据天数是否与年份有关划分为两类

与年份无关的一类中,根据每月天数的不同,又可细分为两类

Ø 135781012月为1-31

Ø 46911月为1-30

与年份有关的一类中

Ø 平年2月为1-28

Ø 闰年2月为1-29

3.1.2 根据包含日期不同可划分为四类

Ø 所有年份的所有月份都包含1-28

Ø 所有年份除2月外都包含2930

Ø 所有年份135781012月都包含31

Ø 闰年2月包含29

3.1.3 分类方法选择

因为日期分类之后的实现,是要通过(exp1|exp2|exp3)这种分支结构来实现的,而分支结构是从左侧分支依次向右开始尝试匹配,当有一个分支匹配成功时,就不再向右尝试,否则尝试所有分支后并报告失败。

分支的多少,每个分支的复杂程度都会影响匹配效率,考虑到被验证日期概率分布,绝大多数都是落到1-28日内,所以采用第二种分类方法,会有效提高匹配效率。

3.2 正则实现

采用3.1.2节的分类方法,就可以针对每一个规则写出对应的正则,以下暂按MM-dd格式进行实现。

先考虑与年份无关的前三条规则,年份可统一写作

(?!0000)[0-9]{4}

下面仅考虑月和日的正则

Ø 包括平年在内的所有年份的月份都包含1-28

(0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-8])

Ø 包括平年在内的所有年份除2月外都包含2930

(0[13-9]|1[0-2])-(29|30)

Ø 包括平年在内的所有年份135781012月都包含31

(0[13578]|1[02])-31)

合起来就是除闰年的229日外的其它所有日期

(?!0000)[0-9]{4}-((0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-8])|(0[13-9]|1[0-2])-(29|30)|(0[13578]|1[02])-31)

接下来考虑闰年的实现

Ø 闰年2月包含29

这里的月和日是固定的,就是02-29,只有年是变化的。

可通过以下代码输出所有的闰年年份,考察规则

for (int i = 1; i < 10000; i++)

{

if ((i % 4 == 0 && i % 100 != 0) || i % 400 == 0)

{

richTextBox2.Text += string.Format("{0:0000}", i) + "\n";

}

}

根据闰年的规则,很容易整理出规则,四年一闰;

([0-9]{2}(0[48]|[2468][048]|[13579][26])

百年不闰,四百年再闰

(0[48]|[2468][048]|[13579][26])00

合起来就是所有闰年的229

([0-9]{2}(0[48]|[2468][048]|[13579][26])|(0[48]|[2468][048]|[13579][26])00)-02-29)

四条规则都已实现,且互相间没有影响,合起来就是所有符合DateTime范围的日期的正则

^((?!0000)[0-9]{4}-((0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-8])|(0[13-9]|1[0-2])-(29|30)|(0[13578]|1[02])-31)|([0-9]{2}(0[48]|[2468][048]|[13579][26])|(0[48]|[2468][048]|[13579][26])00)-02-29)$

考虑到这个正则表达式仅仅是用作验证,所以捕获组没有意义,只会占用资源,影响匹配效率,所以可以使用非捕获组来进行优化。

^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$

以上正则年份0001-9999,格式yyyy-MM-dd。可以通过以下代码验证正则的有效性和性能

DateTime dt = new DateTime(1, 1, 1);

DateTime endDay = new DateTime(9999, 12, 31);

Stopwatch sw = new Stopwatch();

sw.Start();

Regex dateRegex = new Regex(@"^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$");

//Regex dateRegex = new Regex(@"^((?!0000)[0-9]{4}-((0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-8])|(0[13-9]|1[0-2])-(29|30)|(0[13578]|1[02])-31)|([0-9]{2}(0[48]|[2468][048]|[13579][26])|(0[48]|[2468][048]|[13579][26])00)-02-29)$");

Console.WriteLine("开始日期: " + dt.ToString("yyyy-MM-dd"));

while (dt < endDay)

{

if (!dateRegex.IsMatch(dt.ToString("yyyy-MM-dd")))

{

Console.WriteLine(dt.ToString("yyyy-MM-dd") + " false");

}

dt = dt.AddDays(1);

}

if (!dateRegex.IsMatch(dt.ToString("yyyy-MM-dd")))

{

Console.WriteLine(dt.ToString("yyyy-MM-dd") + " false");

}

Console.WriteLine("结束日期: " + dt.ToString("yyyy-MM-dd"));

sw.Stop();

Console.WriteLine("测试用时: " + sw.ElapsedMilliseconds + "ms");

Console.WriteLine("测试完成!");

Console.ReadLine();

4 日期正则表达式扩展

4.1 “年月日”形式扩展

以上实现的是yyyy-MM-dd格式的日期验证,考虑到连字符的不同,以及月和日可能为Md,即yyyy-M-d的格式,可以对以上正则进行扩展

^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])([-/.]?)(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])([-/.]?)(?:29|30)|(?:0?[13578]|1[02])([-/.]?)31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2([-/.]?)29)$

使用反向引用进行简化,年份0001-9999,格式yyyy-MM-ddyyyy-M-d,连字符可以没有或是“-”、“/”、“.”之一。

^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))$

这就是“年月日”这种形式最全的一个正则了,不同含义部分以不同颜色标识,可以根据自己的需要进行栽剪。

4.2 其它形式扩展

了解了以上正则各部分代表的含义,互相间的关系后,就很容易扩展成其它格式的日期正则,如dd/MM/yyyy这种“日月年”格式的日期。

^(?:(?:(?:0?[1-9]|1[0-9]|2[0-8])([-/.]?)(?:0?[1-9]|1[0-2])|(?:29|30)([-/.]?)(?:0?[13-9]|1[0-2])|31([-/.]?)(?:0?[13578]|1[02]))([-/.]?)(?!0000)[0-9]{4}|29([-/.]?)0?2([-/.]?)(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00))$

这种格式需要注意的就是不能用反向引用来进行优了。连字符等可根据自己的需求栽剪。

4.3 添加时间的扩展

时间的规格很明确,也很简单,基本上就HH:mm:ssH:m:s两种形式。

([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]

合入到日期的正则中,yyyy-MM-dd HH:mm:ss

^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)\s+([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$

4.4 年份定制

以上所有涉及到平年的年份里,使用的是0001-9999。当然,年份也可以根据闰年规则定制。

如年份1600-9999,格式yyyy-MM-ddyyyy-M-d,连字符可以没有或是“-”、“/”、“.”之一。

^(?:(?:1[6-9]|[2-9][0-9])[0-9]{2}([-/.]?)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:(?:1[6-9]|[2-9][0-9])(?:0[48]|[2468][048]|[13579][26])|(?:16|[2468][048]|[3579][26])00)([-/.]?)0?2\2(?:29))$

5 特别说明

以上正则采用的是最基本的正则语法规则,绝大多数采用传统NFA引擎的语言都可以支持,包括JavaScriptJava.NET等。

另外需求说明的是,虽然日期的规则相对明确,可以采用这种方式裁剪来得到符合要求的日期正则,但是并不推荐这样使用正则,正则的强大在于它的灵活性,可以根据需求,量身打造最合适的正则,如果只是用来套用模板,那正则也就不称其为正则了。

正则的语法规则并不多,而且很容易入门,掌握语法规则,量体裁衣,才是正则之“道”。

分享到:
评论

相关推荐

    C# 正则应用之——最全的日期正则表达式 没有之一

    前几天在CSDN多个版块看到需要日期正则的帖子,所以整理了这篇文章,和大家一起讨论交流,如有遗漏或错误的地方,还请大家指正。 日期正则一般是对格式有要求,且数据不是直接由用户输入时使用。因应用场景的不同,...

    [正则教程] 正则应用之——日期正则表达式.pdf

    ...

    [正则教程] 正则应用之——日期正则表达式.docx

    ...

    多种net下日期选择方法,另附日期验证的正则表达式

    如果您想验证格式为2010/04/24,2010.04.24的日期,请参考压缩包里的“正则应用之——日期正则表达式.doc”文件。您可以根据自己的需要修改。 这个实例是在vs2008下进行的(net 3.5),如果您使用的是net 2.0,请替换...

    C#全能速查宝典

    1.4.26 IsMatch方法——搜索正则表达式匹配项 67 1.4.27 IsUpper方法——判断是否大写 68 1.4.28 Join方法——串联字符串 69 1.4.29 LastIndexOf方法——确定字符在字符串中最后索引 70 1.4.30 Matches方法——检查...

    Google Android SDK开发范例大全(PDF高清完整版3)(4-3)

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的使用 ...

    Google Android SDK开发范例大全(PDF完整版4)(4-4)

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的使用 ...

    Google Android SDK开发范例大全(PDF高清完整版1)(4-1)

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的使用 ...

    Google Android SDK开发范例大全的目录

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 -p160 5.4 自制发送Email程序——Intent在Email上...

    Google+Android+SDK开发范例大全

    第5章 交互式通信服务与手机控制 5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送...

    Google Android SDK开发范例大全(完整版附部分源码).pdf

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的...

    Google Android sdk 开发范例大全 部分章节代码

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 -p160 5.4 自制发送Email程序——Intent在Email上...

    Google Android SDK 开发范例大全01

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 -p160 5.4 自制发送Email程序——Intent在Email上...

    Google Android SDK 开发范例大全02

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 -p160 5.4 自制发送Email程序——Intent在Email上...

    Google Android SDK开发范例大全(完整版)

    5.1 具有正则表达式的TextView——Linkify规则 5.2 ACTION!CALL!拨打电话——Intent.ACTION.CALL的使用 5.3 自制发送短信程序——SmsManager与PendingIntent对象 5.4 自制发送Email程序——Intent在Email上的使用 ...

    php网络开发完全手册

    7.5 正则表达式与字符操作的综合应用 110 7.5.1 获得与模式匹配的数组单元—— 7.5.1 preg_grep 110 7.5.2 进行全局正则表达式的匹配—— 7.5.2 preg_match_all 111 7.5.3 进行正则表达式的匹配——preg_ 7.5.3 ...

    C#入门——面向对象等,代码+注释

    第19天 01进程 02进程 03FileStream文件流 03复制文件 03正则表达式 04正则表达式下载页面图片(未实现) 第20天 01创建XML文档 02对xml文档增删改查 03.2判断是否登录成功原版 03判断是否登录成功 04Xml增删改查 ...

Global site tag (gtag.js) - Google Analytics