PHP strtotime函数用法、实现原理和源码分析


Posted in PHP onFebruary 04, 2015

源码位置:\ext\date\php_date.c

/* {{{ proto int strtotime(string time [, int now ])

   Convert string representation of date and time to a timestamp */

PHP_FUNCTION(strtotime)

{

    char *times, *initial_ts;

    int   time_len, error1, error2;

    struct timelib_error_container *error;

    long  preset_ts = 0, ts;
    timelib_time *t, *now;

    timelib_tzinfo *tzi;
    tzi = get_timezone_info(TSRMLS_C);
    if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS() TSRMLS_CC, “sl”, ×, &time_len, &preset_ts) != FAILURE) {

        /* We have an initial timestamp */

        now = timelib_time_ctor();
        initial_ts = emalloc(25);

        snprintf(initial_ts, 24, “@%ld UTC”, preset_ts);

        t = timelib_strtotime(initial_ts, strlen(initial_ts), NULL, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper); /* we ignore the error here, as this should never fail */

        timelib_update_ts(t, tzi);

        now->tz_info = tzi;

        now->zone_type = TIMELIB_ZONETYPE_ID;

        timelib_unixtime2local(now, t->sse);

        timelib_time_dtor(t);

        efree(initial_ts);

    } else if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, “s|l”, ×, &time_len, &preset_ts) != FAILURE) {

        /* We have no initial timestamp */

        now = timelib_time_ctor();

        now->tz_info = tzi;

        now->zone_type = TIMELIB_ZONETYPE_ID;

        timelib_unixtime2local(now, (timelib_sll) time(NULL));

    } else {

        RETURN_FALSE;

    }
    if (!time_len) {

        timelib_time_dtor(now);    

        RETURN_FALSE;

    }
    t = timelib_strtotime(times, time_len, &error, DATE_TIMEZONEDB, php_date_parse_tzfile_wrapper);

    error1 = error->error_count;

    timelib_error_container_dtor(error);

    timelib_fill_holes(t, now, TIMELIB_NO_CLONE);

    timelib_update_ts(t, tzi);

    ts = timelib_date_to_int(t, &error2);
    timelib_time_dtor(now);

    timelib_time_dtor(t);
    if (error1 || error2) {

        RETURN_FALSE;

    } else {

        RETURN_LONG(ts);

    }

}

/* }}} */

strtotime函数在使用strtotime(“-1 month”)求上一个月的今天时会出一些状况,

因此也引出写这篇文章,本文包括如下内容:

1).strtotime函数的一些用法
2).strtotime函数的实现基本原理
3).strtotime(“-1 month”)求值失败的原因

strtotime函数的一些用法

1、 strtotime(“JAN”)和strtotime(“January”)

这两个用法的效果是一样的,都是返回指定月份的今天,如果指定月份没有今天,则顺延到下一个月。 如在2011-03-31计算二月,代码:

echo date("Y-m-d H:i:s", strtotime("feb", strtotime("2011-03-31")));

程序会输出: 2011-03-03 00:00:00。 从表象来看,这个结果也许不一定是我们想要的,但是这也算是一种解决方案,这种方案是由什么决定的呢? strtotime函数在执行月份的计算时只计算了月份,相当于直接将月份设置为指定的月份的值,而如jan,january都会有一个对应内部数值。

2、 first关键字

first是一个辅助型的关键字,它可以与星期,天等可以指定确认值的关键字组合使用,如求2011年的第一个星期天:

echo date("Y-m-d H:i:s", strtotime("second sunday", strtotime("2011-01-01"))), "<br />";

在PHP的源码中,对于first与星期和天的组合使用是分开的,即first day对应一个处理操作, 在最终的C实现中,天的值指定为1,即time结构中的d字段指定为1,如下代码:
switch (time->relative.first_last_day_of) {

    case 1: /* first */

        time->d = 1;

        break;

    case 2: /* last */

        time->d = 0;

        time->m++;

        break;

}

3、previous和next关键字

与first类似,previous关键字可以与星期,天组合使用,表示指定时间的前一个星期几或前一天。如下所示代码:

echo date("Y-m-d H:i:s", strtotime("previous sunday", strtotime("2011-02-01"))), "<br />";

程序会输出:2011-01-30 00:00:00
程序求2011-02-01的前一个星期天。

next关键字与previous相反,它表示下一个星期几或后一天。

4、 last关键字

last关键字既可以作为上一个,也可以作为最后一个。如求上一个星期天的日期:

echo date("Y-m-d H:i:s", strtotime("last sunday", strtotime("2011-02-05"))), "<br />";

程序会输出: 2011-01-30 00:00:00

当程序作为最后时,其应用场景是指定日期所在月的最后一天,相当于date(“t”)的结果。如求2000年2月的最后一天:

echo date("Y-m-d H:i:s", strtotime("last day", strtotime("2000-02-01"))), "<br />";

first、previous、last和this关键字在re文件中属于同一组。

5、 back和front关键字

这两个关键字是对一天中的小时的向前和向后操作,其调用格式如下:

echo date("Y-m-d H:i:s", strtotime("back of 24", strtotime("2011-02-01"))), "<br />";

echo date("Y-m-d H:i:s", strtotime("front of 24", strtotime("2011-02-01"))), "<br />";

back表示将时间设置指定小时值的后一个小时的15分的位置。如果是24点,则算到第二天的0点15分。
front表示将时间设置指定小时值的前一个小时的45分的位置。如果是0点,则算前一天的23点45分。
上面的代码输出:2011-02-02 00:15:00 2011-02-01 23:45:00。 其中back of和front of后接的数组必须大于等于0并且小于等于24。

strtotime函数的实现基本原理
官方文档对于strtotime函数的说明是这样的:本函数预期接受一个包含美国英语日期格式的字符串并尝试将其解析为 Unix 时间戳 (自 January 1 1970 00:00:00 GMT 起的秒数),其值相对于 now 参数给出的时间,如果没有提供此参数则用系统当前时间。

这是一个标准PHP内置函数,从PHP4起就已经存在。strtotime函数是以一个扩展的方式加载进来的,在ext/date目录下有其全部实现。 作为一个标准的内置函数,其定义格式也是标准的,如下:

PHP_FUNCTION(strtotime)

    //  处理输入,对于是否有第二个参数有没的处理
    //  调用相关函数,实现字符串的解析和结果计算
    //  返回结果

}

在输入处理中,先识别两个参数都存在的情况并进行处理,如果不是此种状态,则处理第二个参数不存在的情况, 如果都没有,则报错,返回FALSE。

strtotime函数的第一个参数是一个字符串,对于这个字符串,由于其复杂性,PHP使用了其词法解析一样的工具:re2c。在/ext/date/lib目录下,从parse_date.re文件我们可以看到其原始的re文件。 当用户以参数的形式传入一个字符串,此字符串将交给此程序处理,针对其字符串的不同,匹配不同的处理函数。 如strtotime(“yesterday”)调用,分析字符串时,将匹配yesterday字符串,此字符串对应函数如下:

'yesterday'

{

    DEBUG_OUTPUT("yesterday");

    TIMELIB_INIT;

    TIMELIB_HAVE_RELATIVE();

    TIMELIB_UNHAVE_TIME();
    s->time->relative.d = -1;

    TIMELIB_DEINIT;

    return TIMELIB_RELATIVE;

}

这里有几个关键的结构体:
typedef struct Scanner {

    int           fd;

    uchar        *lim, *str, *ptr, *cur, *tok, *pos;

    unsigned int  line, len;

    struct timelib_error_container *errors;
    struct timelib_time *time;

    const timelib_tzdb  *tzdb;

} Scanner;
typedef struct timelib_time {

    timelib_sll      y, m, d;     /* Year, Month, Day */

    timelib_sll      h, i, s;     /* Hour, mInute, Second */

    double           f;           /* Fraction */

    int              z;           /* GMT offset in minutes */

    char            *tz_abbr;     /* Timezone abbreviation (display only) */

    timelib_tzinfo  *tz_info;     /* Timezone structure */

    signed int       dst;         /* Flag if we were parsing a DST zone */

    timelib_rel_time relative;
    timelib_sll      sse;         /* Seconds since epoch */
    unsigned int   have_time, have_date, have_zone, have_relative, have_weeknr_day;
    unsigned int   sse_uptodate; /* !0 if the sse member is up to date with the date/time members */

    unsigned int   tim_uptodate; /* !0 if the date/time members are up to date with the sse member */

    unsigned int   is_localtime; /*  1 if the current struct represents localtime, 0 if it is in GMT */

    unsigned int   zone_type;    /*  1 time offset,

                                  *  3 TimeZone identifier,

                                  *  2 TimeZone abbreviation */

} timelib_time;
typedef struct timelib_rel_time {

    timelib_sll y, m, d; /* Years, Months and Days */

    timelib_sll h, i, s; /* Hours, mInutes and Seconds */
    int weekday; /* Stores the day in 'next monday' */

    int weekday_behavior; /* 0: the current day should *not* be counted when advancing forwards; 1: the current day *should* be counted */
    int first_last_day_of;

    int invert; /* Whether the difference should be inverted */

    timelib_sll days; /* Contains the number of *days*, instead of Y-M-D differences */
    timelib_special  special;

    unsigned int   have_weekday_relative, have_special_relative;

} timelib_rel_time;

s->time->relative.d = -1;所表示的意思是当前时间的相对天数是-1。 这只是中间词法解析的中间结果,但是最后结果是通过这些中间结果计算出来的。

strtotime(“-1 month”)求值失败的原因

虽然strtotime(“-1 month”)这种方法对于后一个月比前一个月的天数的情况会求值失败,但是从其本质上来说,这并没有错。 PHP这样实现也无可厚非。只是我们的需求决定了我们不能使用这种方法,因此我们称其为求值失败。

我们来看它的实现过程,由于没有第二个参数,所以程序使用默认的当前时间。 第一个参数传入的是-1 month字符串,这个字符串所对应的re文件中的正则为:

reltextunit = (('sec'|'second'|'min'|'minute'|'hour'|'day'|'fortnight'|'forthnight'|'month'|'year') 's'?) | 'weeks' | daytext;
relnumber = ([+-]*[ \t]*[0-9]+);

relative = relnumber space? (reltextunit | 'week' );

最终relative会对应一系列操作,程序会识别出前面的-1 和后面的month字符串,month对应一种操作类型:TIMELIB_MONTH。 在此之后,根据识别出来的数字和操作类型执行操作,如下代码:
case TIMELIB_MONTH:  s->time->relative.m += amount * relunit->multiplier; break;

如上代码,则是直接记录月份的相对值减一。 但是对于类似于3月31号这样的情况,2月没有31号,程序会自动将日期计算到下一个月。
PHP 相关文章推荐
PHP中防止SQL注入攻击和XSS攻击的两个简单方法
Apr 15 PHP
使用cookie实现统计访问者登陆次数
Jun 08 PHP
php文件上传的例子及参数详解
Dec 12 PHP
PHP将HTML转换成文本的实现代码
Jan 21 PHP
php实现Mongodb自定义方式生成自增ID的方法
Mar 23 PHP
深入理解PHP变量的值类型和引用类型
Oct 21 PHP
CI框架AR操作(数组形式)实现插入多条sql数据的方法
May 18 PHP
php投票系统之增加与删除投票(管理员篇)
Jul 01 PHP
linux平台编译安装PHP7并安装Redis扩展与Swoole扩展实例教程
Sep 30 PHP
彻底搞懂PHP 变量结构体
Oct 11 PHP
thinkPHP5.1框架使用SemanticUI实现分页功能示例
Aug 03 PHP
PHP RabbitMQ消息列队
May 11 PHP
php curl登陆qq后获取用户信息时证书错误
Feb 03 #PHP
ecshop实现smtp发送邮件
Feb 03 #PHP
PHP中使用php://input处理相同name值的表单数据
Feb 03 #PHP
PHP中实现接收多个name相同但Value不相同表单数据实例
Feb 03 #PHP
四种php中webservice实现的简单架构方法及实例
Feb 03 #PHP
php上传中文文件名乱码问题处理方案
Feb 03 #PHP
php上传文件常见问题总结
Feb 03 #PHP
You might like
php 文件上传代码(限制jpg文件)
2010/01/05 PHP
CodeIgniter采用config控制的多语言实现根据浏览器语言自动转换功能
2014/07/18 PHP
Android AsyncTack 异步任务实例详解
2016/11/02 PHP
PHP递归实现文件夹的复制、删除、查看大小操作示例
2017/08/11 PHP
PHP判断json格式是否正确的实现代码
2017/09/20 PHP
PHP PDO数据库操作预处理与注意事项
2019/03/16 PHP
解决Laravel 使用insert插入数据,字段created_at为0000的问题
2019/10/11 PHP
js查错流程归纳
2012/05/04 Javascript
JS+flash实现chrome和ie浏览器下同时可以复制粘贴
2013/09/22 Javascript
基于jQuery实现Div窗口震动特效代码-代码简单
2015/08/28 Javascript
借助FileReader实现将文件编码为Base64后通过AJAX上传
2015/12/24 Javascript
解析JavaScript面向对象概念中的Object类型与作用域
2016/05/10 Javascript
JS学习之表格的排序简单实例
2016/05/16 Javascript
深入剖析JavaScript面向对象编程
2016/07/12 Javascript
jQuery实现导航高亮的方法【附demo源码下载】
2016/11/09 Javascript
搭建Bootstrap离线文档的方法
2016/12/02 Javascript
AngularJS过滤器filter用法分析
2016/12/11 Javascript
使用selenium抓取淘宝的商品信息实例
2018/02/06 Javascript
使用express搭建一个简单的查询服务器的方法
2018/02/09 Javascript
jQuery length 和 size()区别总结
2018/04/26 jQuery
Python通过select实现异步IO的方法
2015/06/04 Python
用Python实现命令行闹钟脚本实例
2016/09/05 Python
Python实现读取字符串按列分配后按行输出示例
2018/04/17 Python
使用python验证代理ip是否可用的实现方法
2018/07/25 Python
使用pandas读取文件的实现
2019/07/31 Python
Django自定义全局403、404、500错误页面的示例代码
2020/03/08 Python
在pycharm中使用matplotlib.pyplot 绘图时报错的解决
2020/06/01 Python
python爬虫调度器用法及实例代码
2020/11/30 Python
Bose英国官方网站:美国知名音响品牌
2020/01/26 全球购物
ajax是什么及其工作原理
2012/02/08 面试题
《金色的脚印》教后反思
2014/04/23 职场文书
公司节能减排倡议书
2014/05/14 职场文书
学生意外伤害赔偿协议书
2014/09/17 职场文书
退伍军人感言
2015/08/01 职场文书
Java8 CompletableFuture 异步回调
2022/04/28 Java/Android
vue3不同环境下实现配置代理
2022/05/25 Vue.js