项目中有个功能是比较会员是否过期,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. Linux笔记-Makefile伪指令解析

    本文是我在博客里面找到的,觉得对makefile的伪指令介绍得非常详细了!也提到了伪指令为何要用.PHONY:来声明!希望我的这篇转过来的文章能够帮助大家理解makefile的伪指令! 我的理解: 拿 ...

  2. 一些Android的博客,没事翻翻

    深入剖析Android系统(推荐) 深入浅出Android系统移植与平台开发系列(推荐) Android系统开篇(小米工程师对系统的整体分析,推荐) Android6.0Framework源码解析系列 ...

  3. Html-完整表格

    表头th 特殊的单元格:加粗.居中 它的用户是取代td的位置即可 <table border="2"> <tr> <th>姓名</th&g ...

  4. SpringMVC学习笔记:SpringMVC框架的执行流程

    一.MVC设计模式 二.SpringMVC框架介绍 三.SpringMVC环境搭建 四.SpringMVC框架的请求处理流程及体系结构

  5. Java中InputStream 、 InputStreamReader 、 BufferedReader的区别

    1.InputStream.OutputStream 处理字节流的抽象类 InputStream 是字节输入流的所有类的超类,一般我们使用它的子类,如FileInputStream等. OutputS ...

  6. CNN中tensorboard数据可视化

    1.CNN_my_test.py import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data ...

  7. 关于react-router-dom的一些记录

    最近在做的新项目使用的技术栈是 React+AntDesign,本篇主要记录在实践过程中使用react-router-dom时遇到的一些方法的总结. 1.HashRouter和BrowserRoute ...

  8. iOS友盟分享的使用总结

    现在几乎所有的APP都会集成分享,为了可以更好的推广自己的APP. 目前市面上常用的分享无非就三个: 1.友盟分享; 2.shareSDK(mob); 3.苹果原生. 由于苹果原生的分享使用起来不方便 ...

  9. 懒散的态度就是一剂慢性毒药——《我是一只IT小小鸟》读后感(第四周)

    进度拖延是所有团队项目的噩梦,有效的进度管理也许能够解决问题,但我认为更根本的是整个团队的工作态度.大家都希望能够加入一个人人都认真负责积极完成任务的团队,但比如何找这样一个团队更重要的是如何将自己变 ...

  10. Mac系统查看端口占用和杀死进程

    查看进程占用 lsof -i tcp:8080 该命令会显示占用8080端口的进程,有其 pid ,可以通过pid关掉该进程 杀死进程 kill pid 例如 kill 39394 转自:https: ...