项目中有个功能是比较会员是否过期,review同事的代码,发现其写法比较奇葩,但线上竟也未出现bug。

实现大致如下:

$expireTime = "2014-05-01 00:00:00";
$currentTime = date('Y-m-d H:i:s', time()); if($currentTime < $expireTime) {
return false;
} else {
return true;
}

如果两个时间需要进行比较,通常是转换成unix时间戳,用两个int型的数字进行比较。该实现却特意将时间表示成string,然后对两个string进行比较运算。

撇开写法不谈,我很好奇的是php内部是如何进行比较的。

闲话少说,还是从源码开始跟踪。

编译期

在zend_language_parse.y中可以发现类似下述语法:

expr === expr    { zend_do_binary_op(ZEND_IS_IDENTICAL, &$$, &$, &$ TSRMLS_CC); }
expr !== expr { zend_do_binary_op(ZEND_IS_NOT_IDENTICAL, &$$, &$, &$ TSRMLS_CC); }
expr == expr { zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$, &$ TSRMLS_CC); }
expr != expr { zend_do_binary_op(ZEND_IS_NOT_EQUAL, &$$, &$, &$ TSRMLS_CC); }
expr < expr { zend_do_binary_op(ZEND_IS_SMALLER, &$$, &$, &$ TSRMLS_CC); }
expr <= expr { zend_do_binary_op(ZEND_IS_SMALLER_OR_EQUAL, &$$, &$, &$ TSRMLS_CC); }
expr > expr { zend_do_binary_op(ZEND_IS_SMALLER, &$$, &$, &$ TSRMLS_CC); }
expr >= expr { zend_do_binary_op(ZEND_IS_SMALLER_OR_EQUAL, &$$, &$, &$ TSRMLS_CC); }

很明显,此处编译成opcode用的便是zend_do_binary_op。

void zend_do_binary_op(zend_uchar op, znode *result, const znode *op1, const znode *op2 TSRMLS_DC) /* {{{ */
{
zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC); opline->opcode = op;
opline->result.op_type = IS_TMP_VAR;
opline->result.u.var = get_temporary_variable(CG(active_op_array));
opline->op1 = *op1;
opline->op2 = *op2;
*result = opline->result;
}

该函数并没有做什么特别的处理,仅仅是简单保存了opcode、操作数1和操作数2。

执行期

根据opcode,跳转到相应的处理函数:ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER。

static int ZEND_FASTCALL  ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline); zval *result = &EX_T(opline->result.u.var).tmp_var; compare_function(result,
&opline->op1.u.constant,
&opline->op2.u.constant TSRMLS_CC);
ZVAL_BOOL(result, (Z_LVAL_P(result) < 0)); ZEND_VM_NEXT_OPCODE();
}

注意到,两个zval的比较是利用compare_function来处理。

ZEND_API int compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */
{
int ret;
int converted = 0;
zval op1_copy, op2_copy;
zval *op_free; while (1) {
switch (TYPE_PAIR(Z_TYPE_P(op1), Z_TYPE_P(op2))) {
case TYPE_PAIR(IS_LONG, IS_LONG):
...
case TYPE_PAIR(IS_DOUBLE, IS_LONG):
...
case TYPE_PAIR(IS_DOUBLE, IS_DOUBLE):
...
...
// 两个字符串进行比较
case TYPE_PAIR(IS_STRING, IS_STRING):
zendi_smart_strcmp(result, op1, op2);
return SUCCESS;
...
}
}
}

该函数例举了若干种情况,根据本文case,进入zendi_smart_strcmp一窥究竟:

ZEND_API void zendi_smart_strcmp(zval *result, zval *s1, zval *s2) /* {{{ */
{
int ret1, ret2;
long lval1, lval2;
double dval1, dval2; // 尝试将字符串转成数字类型
if ((ret1=is_numeric_string(Z_STRVAL_P(s1), Z_STRLEN_P(s1), &lval1, &dval1, 0)) &&
(ret2=is_numeric_string(Z_STRVAL_P(s2), Z_STRLEN_P(s2), &lval2, &dval2, 0))) {
// 进行数字之间的比较
...
} else {
// 无法全部转成数字
// 则调用zend_binary_zval_strcmp
// 本质为memcmp的一层封装
Z_LVAL_P(result) = zend_binary_zval_strcmp(s1, s2);
ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_LVAL_P(result)));
}
}

那么“2014-05-01 00:00:00”能否转化成数字么?

还是得看下is_numeric_string的实现规则。

static inline zend_uchar is_numeric_string(const char *str, int length, long *lval, double *dval, int allow_errors)
{
const char *ptr;
int base = 10, digits = 0, dp_or_e = 0;
double local_dval;
zend_uchar type; if (!length) {
return 0;
} /* trim掉字符串开头的空白部分 */
while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r' || *str == '\v' || *str == '\f') {
str++;
length--;
}
ptr = str; if (*ptr == '-' || *ptr == '+') {
ptr++;
} if (ZEND_IS_DIGIT(*ptr)) {
/* 判断是否为16进制 */
if (length > 2 && *str == '0' && (str[1] == 'x' || str[1] == 'X')) {
base = 16;
ptr += 2;
} /* 忽略后续的若干0 */
while (*ptr == '0') {
ptr++;
} /* 计算数字的位数,并决定是整型还是浮点 */
for (type = IS_LONG; !(digits >= MAX_LENGTH_OF_LONG && (dval || allow_errors == 1)); digits++, ptr++) {
check_digits:
if (ZEND_IS_DIGIT(*ptr) || (base == 16 && ZEND_IS_XDIGIT(*ptr))) {
continue;
} else if (base == 10) {
if (*ptr == '.' && dp_or_e < 1) {
goto process_double;
} else if ((*ptr == 'e' || *ptr == 'E') && dp_or_e < 2) {
const char *e = ptr + 1; if (*e == '-' || *e == '+') {
ptr = e++;
}
if (ZEND_IS_DIGIT(*e)) {
goto process_double;
}
}
} break;
} if (base == 10) {
if (digits >= MAX_LENGTH_OF_LONG) {
dp_or_e = -1;
goto process_double;
}
} else if (!(digits < SIZEOF_LONG * 2 || (digits == SIZEOF_LONG * 2 && ptr[-digits] <= '7'))) {
if (dval) {
local_dval = zend_hex_strtod(str, (char **)&ptr);
}
type = IS_DOUBLE;
}
} else if (*ptr == '.' && ZEND_IS_DIGIT(ptr[1])) {
// 处理浮点数
} else {
return 0;
} // 如果不允许容错,则报错退出
if (ptr != str + length) {
if (!allow_errors) {
return 0;
}
if (allow_errors == -1) {
zend_error(E_NOTICE, "A non well formed numeric value encountered");
}
} // 允许容错,则尝试将str转成数字
if (type == IS_LONG) {
if (digits == MAX_LENGTH_OF_LONG - 1) {
int cmp = strcmp(&ptr[-digits], long_min_digits); if (!(cmp < 0 || (cmp == 0 && *str == '-'))) {
if (dval) {
*dval = zend_strtod(str, NULL);
} return IS_DOUBLE;
}
} if (lval) {
*lval = strtol(str, NULL, base);
} return IS_LONG;
} else {
if (dval) {
*dval = local_dval;
} return IS_DOUBLE;
}
}

代码比较长,不过仔细阅读,str转num的规则还是很清晰的。

尤其注意的是allow_errors这个参数,它直接决定了本例中无法将“2014-05-01 00:00:00”转化成数字。

因而最后其实“2014-04-17 00:00:00” < “2014-05-01 00:00:00” 的运行是走的memcmp分支。

既然是memcmp,便不难理解为何文章开始提到的写法也能正确运行。

容错转换

何时allow_errors为true呢?一个极好的例子便是zend_parse_parameters,zend_parse_parameters的实现不再细述,有兴趣的读者可以自行研究。其中调用is_numeric_string时将allow_errors置为了-1。

举个例子:

static void php_date(INTERNAL_FUNCTION_PARAMETERS, int localtime)
{
char *format;
int format_len;
long ts;
char *string; // 期望的第二个参数为timestamp,为long
// 假设上层调用时,误传入了string,那么zend_parse_parameters依然会尽可能的尝试将string解析为long
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &format, &format_len, &ts) == FAILURE) {
RETURN_FALSE;
}
if (ZEND_NUM_ARGS() == 1) {
ts = time(NULL);
} string = php_format_date(format, format_len, ts, localtime TSRMLS_CC); RETVAL_STRING(string, 0);
}

这是php的date函数内部实现。

在我们调用date时,如果将第二个参数传入string,效果如下:

echo date('Y-m-d', '0-1-2');

// 输出
PHP Notice: A non well formed numeric value encountered in Command line code on line 1
1970-01-01

虽然报出notice级别的错误,但依然成功将'0-1-2'转成了0

一个“日期”字符串进行比较的case的更多相关文章

  1. 让用户输入一个日期字符串,将其转换成日期格式, 格式是(yyyy/MM/dd,yyyyMMdd,yyyy-MM-dd)中的一种, 任何一种转换成功都可以; 如果所有的都无法转换,输出日期格式非法。

    第三种方法 while(true) {             Date d;        System.out.println("正在进行第一次匹配,请稍后~—~");     ...

  2. Java中如何判断一个日期字符串是否是指定的格式

    判断日期格式是否满足要求 import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date ...

  3. javascript转换日期字符串为Date对象

    把一个日期字符串如“2007-2-28 10:18:30”转换为Date对象: 1: var strArray=str.split(" "); var strDate=strArr ...

  4. SQL to_char,to_date日期字符串转换问题

    1.转换函数 与date操作关系最大的就是两个转换函数:to_date(),to_char() to_date() 作用将字符类型按一定格式转化为日期类型: 具体用法:to_date('2004-11 ...

  5. SQL 生成一个日期范围

    有时想按日或月生成一个序列,就像2014-1-1.2014-1-2.2014-1-3... 在sql server中可以写个函数来实现. /* 生成一个日期范围,如2014.01.2014.02... ...

  6. 日期字符串转换为NSDate

    // 纯数字日期 NSString *str1 = "; // 日期字符串 NSString *str2 = @"2015/05/12 10:22:01"; // 带时区 ...

  7. easyui datebox定位到某一个日期, easyui datebox直接定位到具体的日期, easyui datebox MoveTo方法使用

    easyui datebox定位到某一个日期, easyui datebox直接定位到具体的日期, easyui datebox MoveTo方法使用 >>>>>> ...

  8. python生成随机日期字符串

    python生成随机日期字符串 生成随机的日期字符串,用于插入数据库. 通过时间元组设定一个时间段,开始和结尾时间转换成时间戳. 时间戳中随机取一个,再生成时间元组,再把时间元组格式化输出为字符串 # ...

  9. Java时间日期字符串格式转换大全

    import java.text.*; import java.util.Calendar; public class VeDate { /** * 获取现在时间 * * @return 返回时间类型 ...

随机推荐

  1. ORACLE: private ,dao中util执行规范,nextval计数把通过nextval插入但已删除的列也统计在内向后计数

    private DAO中的util.rs.sql都应该为private. 其中每个具体方法执行增删改查操作前打开数据库连接,操作完成后关闭数据库连接.操作要规范,不然易出错. nextval seq_ ...

  2. 【angular5 项目积累总结】项目公共样式

    main.css @font-face { font-family: 'wf_segoe-ui_normal'; src: local('Segoe UI'),url('../fonts/segoe- ...

  3. LINQ-Group子句、Into子句及orderby子句

    1. Group子句 LINQ表达式必须以from子句开头,以select或Group子句结束,所以除了使用select子句也可以使用Group子句来返回元素分组后的结果.Group子句用来查询结果分 ...

  4. spring mongodb查询

    MongoRepository 查询条件 Keyword Sample Logical result After findByBirthdateAfter(Date date) {"birt ...

  5. sql sever 执行较大的文件脚本

    1.用管理员身份打开cmd工具 2.执行命令 osql -S  localhost -U sa -P 123456 -i D:/test.sql -S 服务器地址  本地可简写 . -U 用户名 -P ...

  6. MyBatis别名

    Spring的别名管理比较规范,有严格的接口规范,SimpleAliasRegistry实现 -> AliasRegistry接口,而且是线程安全的,Map也用的是ConcurrentHashM ...

  7. 【SSH网上商城项目实战18】过滤器实现购物登录功能的判断

    转自:https://blog.csdn.net/eson_15/article/details/51425010 上一节我们做完了购物车的基本操作,但是有个问题是:当用户点击结算时,我们应该做一个登 ...

  8. pollard_rho 算法进行质因数分解

    //************************************************ //pollard_rho 算法进行质因数分解 //*********************** ...

  9. java 自定义泛型

    package com.direct.demo; import java.util.ArrayList; import java.util.HashMap; import java.util.Link ...

  10. BZOJ1093 [SCOI2003]字符串折叠

    Description 折叠的定义如下: 1. 一个字符串可以看成它自身的折叠.记作S  S 2. X(S)是X(X>1)个S连接在一起的串的折叠.记作X(S)  SSSS…S(X个S). ...