php strtotime()方法的坑

先上结果。

strtotime是一个标准PHP内置函数,从PHP4起就已经存在,提供了灰常多简便算法供我们使用,比如-1 month, +1 month, next month。(源码在ext/date目录下,有其全部实现。附上php8源码地址:https://github.com/php/php-src/tree/master/ext/date

但是在实际项目中,差点因为它闹出生产事故,2022年3月31号执行-1 month,竟然是2022年3月3号,strtotime这个方法有bug吗?

NO!

通过分析源码,strtotime这个方法的底层逻辑是这样的:

strtotime函数的第一个参数是一个字符串,由词法解析工具:re2c进行处理(re2c也就是c对php词法的分析器和生成器)。strtotime的解析器在/ext/date/lib目录下,parse_date.re文件。 当用户以参数的形式传入一个字符串,此字符串将交给此程序处理,针对其字符串的不同,匹配不同的处理函数。

当第一个参数传入-1 month时,执行下列正则,第二个参数不传则取当前时间。

reltextunit = 'ms' | 'µs' | (('msec'|'millisecond'|'µsec'|'microsecond'|'usec'|'sec'|'second'|'min'|'minute'|'hour'|'day'|'fortnight'|'forthnight'|'month'|'year') 's'?) | 'weeks' | daytext;

relnumber = ([+-]*[ \t]*[0-9]{1,13});
relative = relnumber space? (reltextunit | 'week' );

最终-1和month被分开识别,month对应操作TIMELIB_MONTH。

/**
 * The time_part parameter is a flag. It can be TIMELIB_TIME_PART_KEEP in case
 * the time portion should not be reset to midnight, or
 * TIMELIB_TIME_PART_DONT_KEEP in case it does need to be reset. This is used
 * for not overwriting the time portion for 'X weekday'.
 */
static void timelib_set_relative(const char **ptr, timelib_sll amount, int behavior, Scanner *s, int time_part)
{
	const timelib_relunit* relunit;

	if (!(relunit = timelib_lookup_relunit(ptr))) {
		return;
	}

	switch (relunit->unit) {
		case TIMELIB_MICROSEC: s->time->relative.us += amount * relunit->multiplier; break;
		case TIMELIB_SECOND:   s->time->relative.s += amount * relunit->multiplier; break;
		case TIMELIB_MINUTE:   s->time->relative.i += amount * relunit->multiplier; break;
		case TIMELIB_HOUR:     s->time->relative.h += amount * relunit->multiplier; break;
		case TIMELIB_DAY:      s->time->relative.d += amount * relunit->multiplier; break;
		case TIMELIB_MONTH:    s->time->relative.m += amount * relunit->multiplier; break;
		case TIMELIB_YEAR:     s->time->relative.y += amount * relunit->multiplier; break;

		case TIMELIB_WEEKDAY:
			TIMELIB_HAVE_WEEKDAY_RELATIVE();
			if (time_part != TIMELIB_TIME_PART_KEEP) {
				TIMELIB_UNHAVE_TIME();
			}
			s->time->relative.d += (amount > 0 ? amount - 1 : amount) * 7;
			s->time->relative.weekday = relunit->multiplier;
			s->time->relative.weekday_behavior = behavior;
			break;

		case TIMELIB_SPECIAL:
			TIMELIB_HAVE_SPECIAL_RELATIVE();
			if (time_part != TIMELIB_TIME_PART_KEEP) {
				TIMELIB_UNHAVE_TIME();
			}
			s->time->relative.special.type = relunit->multiplier;
			s->time->relative.special.amount = amount;
	}
}

如上,代码执行只是对month进行相对值的加减,回到`bug`本身,3月31号执行-1 month,结果为2月31日,2022年2月只有28天,2月31比2月28多3天,即为3月3日。

所以不是php的扩展方法有bug,是我没有搞清楚方法执行的底层逻辑用出了bug。

找到了原因,那是不是说strtotime就不能准确计算上个月下个月了呢? 

NO!

从PHP5.3开始, date新增了一些特殊格式,来解决这个问题:"first day of" 和 "last day of" 

 

firstdayof | lastdayof
	{
		DEBUG_OUTPUT("firstdayof | lastdayof");
		TIMELIB_INIT;
		TIMELIB_HAVE_RELATIVE();

		/* skip "last day of" or "first day of" */
		if (*ptr == 'l' || *ptr == 'L') {
			s->time->relative.first_last_day_of = TIMELIB_SPECIAL_LAST_DAY_OF_MONTH;
		} else {
			s->time->relative.first_last_day_of = TIMELIB_SPECIAL_FIRST_DAY_OF_MONTH;
		}

		TIMELIB_DEINIT;
		return TIMELIB_LF_DAY_OF_MONTH;
	}

可以看到这里优雅地获取了操作月的第一天或最后一天,不会出现超越,然后自动格式化日期的情况。

验证一下:

 结束收工!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值