日期验证正则表达式

日期正则表达式

网摘 2009年07月20日 07:26:00  @过客的博客

概述

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

前几天在CSDN多个版块看到需要日期正则的帖子,所以整理了这篇文章,和大家一起讨论交流,如有遗漏或错误的地方,还请大家指正。
日期正则一般是对格式有要求,且数据不是直接由用户输入时使用。因应用场景的不同,写出的正则也不同,复杂程度也自然不同。正则的书写需要根据具体情况具体分析,一个基本原则就是:只写合适的,不写复杂的。

对于日期提取,只要能与非日期区分开,写最简单的正则即可,如\d{4}-\d{2}-\d{2}
如果可以在源字符串中唯一定位yyyy-MM-dd格式的日期,则可用做提取。

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

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

日期的规则

  1. 日期的有效范围
    对于日期的有效范围,不同的应用场景会有所不同。
    MSDN 中定义的 DateTime 对象的有效范围是:0001-01-01 00:00:00到9999-12-31 23:59:59。
    UNIX 时间戳的 0 按照 ISO 8601 规范为:1970-01-01T00:00:00Z。

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

  2. 什么是闰年(以下摘自百度百科)

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

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

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

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

     yyyyMMdd
     yyyy-MM-dd
     yyyy/MM/dd
     yyyy.MM.dd
    

日期正则表达式构建

规则分析

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

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

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

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

        - 1、3、5、7、8、10、12 月为 1-31 日
        - 4、6、9、11 月为 1-30 日

    2. 与年份有关的一类中

        -  平年2月为1-28日
        -  闰年2月为1-29日

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

    - 所有年份的所有月份都包含1-28日
    - 所有年份除2月外都包含29和30日
    - 所有年份1、3、5、7、8、10、12月都包含31日
    - 闰年2月包含29日

3. 分类方法选择

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

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

正则实现

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

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

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

下面仅考虑月和日的正则

1. 包括平年在内的所有年份的月份都包含1-28日
    
        (0[1-9]|1[0-2])-(0[1-9]|1[0-9]|2[0-8])

2. 包括平年在内的所有年份除2月外都包含29和30日

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

3. 包括平年在内的所有年份 1、3、5、7、8、10、12 月都包含 31 日

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

合起来就是除闰年的2月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)

接下来考虑闰年的实现

1. 闰年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

    合起来就是所有闰年的2月29日
        
        ([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();

日期正则表达式扩展

“年月日”形式扩展

以上实现的是 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))$

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

其它形式扩展

了解了以上正则各部分代表的含义,互相间的关系后,就很容易扩展成其它格式的日期正则,如 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))$

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

添加时间的扩展

时间的规格很明确,也很简单,基本上就 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]$

年份定制

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

如年份1600-9999,格式yyyy-MM-dd或yyyy-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))$

特别说明

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

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

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

Python 正则表达式验证传统日期

网摘 2014年07月16日 13:52:47  @guaguastd的博客

正则表达式:

(?x)
(?:
    (?#dd/mm)(3[0-1]|[12][0-9]|0?[0-9])/(1[0-2]|0?[1-9])|
    (?#mm/dd)(1[0-2]|0?[1-9])/(3[0-1]|[12][0-9]|0?[0-9]))/
(?#yy or yyyy)(?:[0-9]{2})?[0-9]{2}

Python code:

def dateCheck(sDate):  
  
    import re  
  
    daysinmonth = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)  
    validdate = 0
    patt = "^(?P<month>[0-3]?[0-9])/(?P<day>[0-3]?[0-9])/(?P<year>[0-9]{4})$"
    match = re.search(patt, sDate)  
    if match:  
        month = int(match.group("month"))  
        day = int(match.group("day"))  
        year = int(match.group("year"))  
  
        if year < 50:  
            year += 2000  
        if year < 100:  
            year += 1900  
  
        if month == 2 and year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):  
            if day >= 1 and day <= 29:  
                validdate = 1  
        elif month >=1 and month <= 12:  
            if day >=1 and day <= daysinmonth[month-1]:  
                validdate = 1  
  
    if validdate == 0:  
        print 'date <%s> is invalid!' % sDate  
    else:  
        print 'date <%s> is valid!' % sDate  
  
def main():  
    while 1:  
        sDate = raw_input("Please input date (format is mm/dd/yyyy, exit to quit): ")  
        if sDate == 'exit':  
            break  
        else:  
            dateCheck(sDate)  
  
main()  
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,230评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,261评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,089评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,542评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,542评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,544评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,922评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,578评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,816评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,576评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,658评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,359评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,920评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,859评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,381评论 2 342

推荐阅读更多精彩内容

  • 推荐几个正则表达式编辑器 Debuggex :https://www.debuggex.com/ PyRegex:...
    木易林1阅读 11,452评论 9 151
  • 几个正则表达式编辑器 Debuggex :https://www.debuggex.com/ PyRegex:ht...
    没技术的BUG开发攻城狮阅读 4,581评论 0 23
  • 初衷:看了很多视频、文章,最后却通通忘记了,别人的知识依旧是别人的,自己却什么都没获得。此系列文章旨在加深自己的印...
    DCbryant阅读 3,980评论 0 20
  • 一、Java 简介 Java是由Sun Microsystems公司于1995年5月推出的Java面向对象程序设计...
    子非鱼_t_阅读 4,146评论 1 44
  • 好好对自己 在白天做自己的英雄 在晚上变多情的姑娘 你太遥远 但愿能照顾好自己
    吃了胖阅读 86评论 0 0