首頁  >  文章  >  後端開發  >  php日期字串比較實例

php日期字串比較實例

WBOY
WBOY原創
2016-07-25 09:13:13948瀏覽

專案中有個功能是比較會員是否過期,review同事的程式碼,發現其寫法比較奇葩,但線上竟也未出現bug。

實作:

  1. $expireTime = "2014-05-01 00:00:00";
  2. $currentTime = date('Y-m-d H:i:s' , time());
  3. if($currentTime return false;
  4. } else {
  5. return true;
  6. }

  1. 複製程式碼
  2. 如果兩個時間需要比較,通常是轉換成unix時間戳,用兩個int型的數字比較。該實作卻刻意將時間表示成string,然後對兩個string進行比較運算。
撇開寫法不談,我很好奇的是php內部是如何比較的。
閒話少說,還是從原始碼開始追蹤。
編譯期 在zend_language_parse.y中可以發現類似下述語法:

  1. expr === expr { zend_do_binary_op(ZEND_IS_IDENTICAL, &$$, &$1, &$3 TSRMLS_CC); }
  2. zend_do_binary_op(ZEND_IS_NOT_IDENTICAL, &$$, &$1, &$3 TSRMLS_CC); }
  3. expr == expr { zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$1, & zend_do_binary_op(ZEND_IS_EQUAL, &$$, &$1, &$3 TSRM_CC); zend_do_binary_op(ZEND_IS_NOT_EQUAL, &$$, &$1, &$3 TSRMLS_CC); }
  4. expr expr > expr { zend_do_binary_op(ZEND_IS_SMALLER, & >, &$3, &$1 TSRMLS_CC); ZEND_IS_SMALLER_OR_EQUAL, &$$, &$3, &$1 TSRMLS_CC); }
  5. 複製程式碼
  6. 很明顯,這裡編譯成opcode用的便是zopcode用的便是zop。
void zend_do_binary_op(zend_uchar op, znode *result, const znode *op1, const znode *op2 TSRMLS_DC) /RM>
zend_op *opline = get_next_op(CG(active_op_array) TSRMLS_CC);

opline->opcode = op;

opline->result.op_type = IS_TMP_Rult_TMP_Rult. .var = get_temporary_variable(CG(active_op_array));

opline->op1 = *op1;
opline->op2 = *op2;
*result = opline->result
    ;
  1. }
  2. >
  3. 複製程式碼
  4. 函數並沒有做什麼特別的處理,只是簡單保存了opcode、運算元1和操作數2。
  5. 執行期 根據opcode,跳到對應的處理函數:ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER。
  6. static int ZEND_FASTCALL ZEND_IS_SMALLER_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANivER_ARGS) 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)
ZEND_VM_NEXT_OPCODE();
}
  1. 注意到,兩個zval的比較是利用compare_function來處理。
  2. ZEND_API int compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC) /* {{{ */
  3. /
  4. int converted = 0;
  5. zval op1_copy, op2_copy;
  6. zval *op_free;
  7. while (1) {
  8. switch (TYPE_PAIRffidf ( op2))) {
  9. case TYPE_PAIR(IS_LONG, IS_LONG):
  10. ...
  11. case TYPE_PAIR(IS_DOUBLE, IS_LONG):
  12. ...
  13. case TYPEDOUB_IS_y_
  14. ...
  15. ...
  16. // 兩個字串進行比較
case TYPE_PAIR(IS_STRING, IS_STRING):
zendi_smart_strcmp(result, op1, op2); return SUCCESS; ...
} }} 複製程式碼

此函數例舉了若干種情況,根據本文case,進入zendi_smart_strcmp一窺究竟:

  1. ZEND_API void zendi_smart_strcmp(zval *result, zval *s1, zval *s2) /* {{{ */
  2. { 🎜> , ret2;
  3. 長 lval1, lval2;
  4. double dval1, dval2;
  5. // 嘗試將字串轉成數字型別
  6. if ((ret1=is_numeric_string(Z_STRVAL, Z_STRLEN_P(s1), &lval1, &dval1, 0)) &&
  7. (ret2=is_numeric_string(Z_STRVAL_P(s2), Z_STRLEN_P(s2), &lval2, &dval2, 0))) {比較
  8. ...
  9. } else {
  10. // 無法全部轉成數字
  11. // 則呼叫zend_binary_zval_strcmp
  12. // 本質為memcmp的一層封裝
  13. Z_LVAL_🎜> // 本質為memcmp的一層封裝
  14. Z_LVAL_P(result)_P(result) = zend_binary_zval_strcmp(s1, s2);
  15. ZVAL_LONG(result, ZEND_NORMALIZE_BOOL(Z_LVAL_P(result)));
  16. }
  17. }
那麼「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 char *ptr;
int base = 10, digits = 0, dp_or_e = 0;
    double local_dval;
  1. zend_uchar type;
  2. if (!length) { 0🎜;
  3. }
  4. /* trim掉字串開頭的空白部分*/
  5. while (*str == ' ' || *str == 't' || *str == 'n ' || *str == 'r' || *str == 'v' || *str == 'f') {
  6. str++;
  7. length--;
  8. }
  9. ptr = str;
  10. if (*ptr == '-' || *ptr == '+') {
  11. ptr++;
  12. }
  13. if (ZEND_IS_DIGIT(*ptr )) {
  14. /* 判斷是否為16進位*/
  15. if (length > 2 && *str == '0' && (str[1] == 'x' || str[1] = = 'X')) {
  16. base = 16;
  17. ptr += 2;
  18. }
  19. /* 忽略後續的若干0 */
  20. while (*ptr == '0') {
  21. ptr++;
  22. }
  23. /* 計算數字的位數,決定是整型還是浮點*/
  24. for (type = IS_LONG; !(digits >= MAX_LENGTH_OF_LONG && (dval || allow_errors == 1)); digits++, ptr++) {
  25. check_digits:
  26. if (ZEND_IS_DIGIT(*ptr) || (base == 16 & ZEND_IS_v) {
  27. continue;
  28. } else if (base == 10) {
  29. if (*ptr == '.' && dp_or_e goto process_double;
  30. } elseif (> goto if ( (*ptr == 'e' || *ptr == 'E') && dp_or_e const char *e = ptr + 1;
  31. if (*e == '- ' || *e == '+') {
  32. ptr = e++;
  33. }
  34. if (ZEND_IS_DIGIT(*e)) {
  35. goto process_double;
  36. }
  37. }
  38. }
  39. break;
  40. }
  41. if (base == 10) {
  42. if (digits >= MAX_LENGTH_OF_LONG) {
  43. dp_or_g > goto process_double;
  44. }
  45. } else if (!(digits if& ptr[-digits] if& ptr[-digits] ifif (dval) {
  46. local_dval = zend_hex_strtod(str, (char **)&ptr);
  47. }
  48. type = IS_DOUBLE;
  49. }
  50. } else if (*ptr == '.' && ZEND_IS_DIGIT(ptr[1])) {
  51. // 處理浮點數
  52. } else {
  53. return 0;
  54. }
  55. // 如果不允許容錯,則報錯退出
  56. if (ptr != str + length) {
  57. if (!allow_errors) {
  58. return 0;
  59. }
  60. if (allow_errors == -1) {
  61. zend_error(E_NOTICE , "A non well formed numeric value encountered");
  62. }
  63. }
  64. // 允許容錯,則嘗試將str轉成數字
  65. if (type == IS_LONG) {
  66. if (digits == MAX_LENGTH_OF_LONG - 1) {
  67. int cmp = strcmp(&ptr[-digits], long_min_digits);
  68. if (!(cmp. *str == '-'))) {
  69. if (dval) {
  70. *dval = zend_strtod(str, NULL);
  71. }
  72. return IS_DOUBLE;
  73. }
  74. }
  75. if (lval) {
  76. *lval = strtol(str, NULL, base);
  77. }
  78. return IS_LONG;
  79. } else {
  80. if (dval) {
  81. *dval = local_dval;
  82. }
  83. return IS_DOUBLE;
  84. }
  85. }
  86. }
  87. }
  88. }
  89. }
複製程式碼🎜>

程式碼比較長,不過仔細閱讀,str轉num的規則還是很清楚的。

尤其註意的是allow_errors這個參數,它直接決定了本例中無法將「2014-05-01 00:00:00」轉換成數字。

因而最後其實「2014-04-17 00:00:00」

既然是memcmp,便不難理解為何文章開始提到的寫法也能正確運作。

容錯轉換 何時allow_errors為true呢?一個極好的例子就是zend_parse_parameters,zend_parse_parameters的實作不再細述,有興趣的讀者可以自行研究。其中呼叫is_numeric_string時將allow_errors置為了-1。

舉個例子:

  1. static void php_date(INTERNAL_FUNCTION_PARAMETERS, int localtime)
  2. {
  3. charformatts;
  4. char *string;
  5. // 期望的第二個參數為timestamp,為long
  6. // 假設上層呼叫時,誤傳入了string,那麼zend_parse_parameters依然會盡可能的嘗試將string解析為long
  7. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l", &format, &format_len, &ts) == FAILURE) {
  8. RETURN_FALSEd; ) == 1) {
  9. ts = time(NULL);
  10. }
  11. string = php_format_date(format, format_len, ts, localtime TSVALLS_CC); }
  12. 複製程式碼
  13. 這是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
  1. 複製代碼
複製代碼
雖然報出🎜>等級的錯誤,但仍成功將'0-1-2'轉成了0

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn