程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> 一個“日期”字符串進行比較的case

一個“日期”字符串進行比較的case

編輯:關於PHP編程

項目中有個功能是比較會員是否過期,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    { zend_do_binary_op(ZEND_IS_IDENTICAL, &$$, &$, &$ !==     { zend_do_binary_op(ZEND_IS_NOT_IDENTICAL, &$$, &$, &$ ==      { zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$, &$ !=      { zend_do_binary_op(ZEND_IS_NOT_EQUAL, &$$, &$, &$ <       { zend_do_binary_op(ZEND_IS_SMALLER, &$$, &$, &$ <=      { zend_do_binary_op(ZEND_IS_SMALLER_OR_EQUAL, &$$, &$, &$ >       { zend_do_binary_op(ZEND_IS_SMALLER, &$$, &$, &$ >=      { 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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved