先上结果。
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;
}
可以看到这里优雅地获取了操作月的第一天或最后一天,不会出现超越,然后自动格式化日期的情况。
验证一下:
结束收工!