Các hàm PHP dùng để kiểm tra dữ liệu ngày tháng năm sinh người (v1.2)

Trong bài viết này tôi sẽ trình bày với các bạn cách kiểm tra, chuẩn hóa dữ liệu ngày tháng năm sinh ở người trong một tệp dữ liệu lớn.

Một số lưu ý:

  • Định dạng chuẩn mà chúng ta hướng đến là cấu trúc dd/mm/yyyy, ví dụ như 05/10/2008.
  • Cấu trúc này yêu cầu ngày là dạng 2 số, nếu dưới 10 thì số 0 phải được bổ sung; tháng cũng là dạng 2 số, nếu dưới 10 thì số 0 cũng phải được bổ sung vào. Năm là dạng 4 số, ví dụ 1998, 2003. Nếu năm là dạng 2 số nó cần chuyển về dạng 4 số;
  • Thứ tự dữ liệu là ngày rồi đến tháng và cuối cùng là năm;
  • Dấu phân cách giữa ngày, tháng, năm là dấu /, các dạng phân cách hợp lệ phổ biến khác là . và – sẽ đều được chuyển về dạng tiêu chuẩn là dấu /
  • Cuối cùng là xác thực dữ liệu ngày tháng có phải là tồn tại thực hay không, ví dụ ngày 30/2/2015 không phải là ngày thực vì tháng 2 chỉ có tối đa 29 ngày vào năm nhuận, và 28 ngày vào các năm khác. Ngày 30/2/2015 nhìn sơ thì thấy hợp chuẩn, nhưng nó không tồn tại thực. PHP có sẵn câu lệnh để chúng ta làm điều này, do vậy sẽ không cần viết thêm hàm;

Việc đồng bộ dữ liệu tập trung vào:

  • Hướng đến định dạng chuẩn. Tức là chuyển đổi các dữ liệu ngày tháng năm đúng nhưng chưa chuẩn về dạng chuẩn chung;
  • Các dữ liệu sai thường không sửa (bởi có rất ít căn cứ để sửa chính xác);
  • Nếu việc sửa sai dữ liệu cần làm, ví dụ dạng ngày tháng năm có khoảng trống dư như 15/ 2/ 1987 cần phải đánh giá mức độ phổ biến của kiểu sai này trong tệp dữ liệu. Nếu nó phổ biến thì có khả năng sửa. Nếu nó hy hữu, không nên sửa và tất nhiên không đưa nó vào dữ liệu thống kê, việc sửa dữ liệu ngày tháng năm sai với tần suất rất thấp không có căn cứ đáng tin cậy để đưa ra kết luận là việc sửa có phù hợp hay không. Tốt nhất là không sửa và không dùng;

Hàm PHP quan trọng mà chúng ta sẽ dùng là hàm preg_match để so khớp, kết hợp nó với regex (biểu thức chính quy) giúp chúng ta có kết quả vừa nhanh chóng vừa chính xác.

Mã tổng hợp, sau phần mã tổng hợp sẽ là phần giải thích từng hàm:

<?php

/* 
 * PHP functions to check Vietnamese people's date of birth v1.2
 * MIT License
 * Nguyen Duc Anh - freehost.page
 */

///=============================================================================
// kiểm tra đầu vào có phải ngày tháng năm hay không
// bất kể dạng nào, không riêng gì ngày tháng năm sinh
function vn_is_date_ok($dateip){
    $rs = 0;
    
    $date = trim($dateip, ' '); // loại bỏ khoảng trống 2 bên, dự phòng vì cũng hiếm gặp
    // kiểm tra khớp với kiểu dữ liệu ngày tháng năm, mẫu bên dưới, chấp nhận 3 kiểu phân cách là /, - và .
    $reg_dmy = '/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/'; // kiểu ngày, tháng, năm
    $reg_mdy = '/^(0?[1-9]|1[012])[\/\-\.](0?[1-9]|[12]\d|3[01])[\/\-\.](\d{2}|\d{4})$/'; // kiểu tháng, ngày, năm

    $rs_datex = preg_match($reg_dmy, $date);
    $rs_datey = preg_match($reg_mdy, $date);

    if (($rs_datex == 1) || ($rs_datey == 1)) {$rs = 1;} //bằng 1 nghĩa là phải khớp một trong hai dạng này
                    
return $rs;
}


////////////////////////////////////////////////////////////////////////////////
// kiểu ngày, tháng năm của VN
function vn_dmy_style($dateip){
    $rs = 0; // giả định ban đầu là không khớp
    
    $date = trim($dateip, ' '); // loại bỏ khoảng trống 2 bên, dự phòng vì cũng hiếm gặp
    // kiểm tra khớp với kiểu dữ liệu ngày tháng năm, mẫu bên dưới, chấp nhận 3 kiểu phân cách là /, - và .
    $reg_dmy = '/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/'; // kiểu ngày, tháng, năm
    $rs_datex = preg_match($reg_dmy, $date);
    
    if ($rs_datex) {$rs = 1;} //bằng 1 nghĩa là khớp
                    
return $rs;
}


////////////////////////////////////////////////////////////////////////////////
// kiểu tháng, ngày, năm của nước ngoài
function vn_mdy_style($dateip){
    $rs = 0; // giả định ban đầu là không khớp
    
    $date = trim($dateip, ' '); // loại bỏ khoảng trống 2 bên, dự phòng vì cũng hiếm gặp
    // kiểm tra khớp với kiểu dữ liệu ngày tháng năm, mẫu bên dưới, chấp nhận 3 kiểu phân cách là /, - và .
    $reg_mdy = '/^(0?[1-9]|1[012])[\/\-\.](0?[1-9]|[12]\d|3[01])[\/\-\.](\d{2}|\d{4})$/'; // kiểu tháng, ngày, năm
    $rs_datey = preg_match($reg_mdy, $date);
    
    if ($rs_datey) {$rs = 1;} //bằng 1 nghĩa là khớp
                    
return $rs;
}


///=============================================================================
// kiểm tra xem dữ liệu ngày tháng có dấu - hoặc . hay không?
function vn_separation_date_check($date){
    $rs = 0; // giả định là không phải làm gì
    $rsx = preg_match('/\./', $date); // tìm dấu chấm
    $rsy = preg_match('/-/', $date); // tìm dấu gạch ngang
    
    if ($rsx || $rsy) {$rs = 1;}
    
return $rs;        
}


////////////////////////////////////////////////////////////////////////////////
// dữ liệu đầu vào đã vượt qua bài test is_date_ok, giờ chỉ đổi dấu phân cách . và - thành /
function vn_separation_date($date){
    $date2 = preg_replace('/\./', '/', $date); //thay dấu chấm
    $rs = preg_replace('/-/', '/', $date2); //thay dấu gạch ngang
        
return $rs;        
}


///=============================================================================
// kiểm tra xem có phải thêm số không đằng trước không
function vn_zero4date_check($date){
    $rs = 0;//giả định ban đầu là không phải chuyển
    $pt = '/^(\d{2})\/(\d{2})\/(\d{2}|\d{4})$/';
    $rev = preg_match($pt, $date); // tìm mẫu
    
    if(!$rev) {$rs = 1;} // tức là phải thêm số 0
    
return $rs;    
}

////////////////////////////////////////////////////////////////////////////////
// thêm số 0 vào đầu ngày và tháng đối với các giá trị từ 1 tới 9. 
// dữ liệu đầu vào đã vượt qua is_date_ok và separation_date
function vn_zero4date($date){
    $sp_date = mb_split('/',$date); // tách
    $day = (int)$sp_date[0]; // chuyển thành dạng số
    $month = (int)$sp_date[1]; // chuyển thành dạng số
    $year = $sp_date[2];
    
    if ($day < 10) {$day='0'.$day;} // gắn thêm 0 vào
    if ($month < 10) {$month='0'.$month;} // gắn thêm 0 vào
    
    $rs = $day.'/'.$month.'/'.$year;
    
return $rs;    
}


///=============================================================================
// kiểm tra năm sinh có phải dạng 2 số hay không
function vn_date_yy_check($date) {
    $rs = 0;
    $sp_date = mb_split('/',$date); // tách
    $year = (int)$sp_date[2]; // chuyển thành dạng số
    if ($year < 100) {$rs = 1;} // nếu là dạng hai số thì xác nhận

return $rs;    
}


////////////////////////////////////////////////////////////////////////////////
// đổi năm từ dạng 2 số thành dạng 4 số, dành cho ngày tháng năm sinh
function vn_date_yyyy($date) {
    $rs = $date;
    $sp_date = mb_split('/',$date); // tách
    $year = (int)$sp_date[2]; // chuyển thành dạng số
    $now = date("y"); // lấy 2 số cuối năm hiện tại
    
    if ($year < 100) {   
        $day = $sp_date[0]; 
        $month = $sp_date[1]; 
        if ($year > $now) {$year = "19".$year;} // nếu 2 số cuối lớn hơn 2 số năm hiện tại thì gắn 19 vào
        else {$year = "20".$year;}
        $rs = $day.'/'.$month.'/'.$year;
    }

return $rs;    
}


///=============================================================================
function vn_true_date($date) { // kiểm tra một ngày có phải là thực hay không dữ liệu đầu vào là dd/mm/yy hoặc dd/mm/yyyy
    $rs = 0;
    
    if (isset($date)) {    
    $arr_date = mb_split('/', $date);} // cắt ngày tháng năm thành các chuỗi chuỗi số dựa trên ký tự phân cách

    if (isset($arr_date) && count($arr_date)===3) {   
        $day = (int)$arr_date[0]; // lấy ngày và chuyển sang dạng số
        $month = (int)$arr_date[1]; // lấy tháng và chuyển sang dạng số
        $year = (int)$arr_date[2]; // lấy năm và chuyển sang dạng số

        if (checkdate($month, $day, $year)) {$rs = 1;} // checkdate là hàm có sẵn của PHP
    } 
    
return $rs;
}


///=============================================================================
// giới hạn năm sinh, đã chuẩn hóa về kiểu ngày, tháng , năm 4 số, với việc phân cách bắng dấu /, ví dụ 15/05/2015
// ví dụ giới hạn từ năm 2005 đến năm 2015
// $low là năm thấp nhất chấp nhận được
// $up là năm cao nhất chấp nhận được

function vn_limit_year($date, $low, $up) {
    $rs = 0;
    
    if (isset($date)) {$sp_date = mb_split('/',$date);} // tách
    
    if (isset($sp_date) && count($sp_date)===3) {
        $year = (int)$sp_date[2]; // chuyển thành dạng số
        if ($year >= $low && $year <= $up) {$rs = 1;}
    }
    
return $rs;    
}


///=============================================================================
// lấy tháng trong giả định dữ liệu là tháng/ngày/năm
function vn_month_first($date) {
    $rs = -1;
    if (isset($date)) {$sp_date = mb_split('/',$date);} // tách
    
    if (isset($sp_date) && count($sp_date)===3) {
        $rs = $sp_date[0]; // lấy phần tử đầu tiên trong mảng
        // trong giả định này sẽ là tháng
    }
    
return $rs;    
}


///=============================================================================
// lấy tháng trong giả định dữ liệu là ngày/tháng/năm
function vn_month_second($date) {
    $rs = -1;
    if (isset($date)) {$sp_date = mb_split('/',$date);} // tách
    
    if (isset($sp_date) && count($sp_date)===3) {
        $rs = $sp_date[1]; // lấy phần tử đầu tiên trong mảng
        // trong giả định này sẽ là tháng
    }
    
return $rs;    
}


///=============================================================================
// chuẩn hóa từ A tới Z dữ liệu ngày tháng năm sinh
// chuyển năm về dạng 4 số
// thêm 0 vào ngày tháng dưới 10
// chuyển các ký tự phân cách ngày tháng là - và . về /

function vn_stand_date($date) {
    $date2 = vn_separation_date($date); // chuyển các ký tự phân cách ngày tháng là - và . về /
    $date3 = vn_zero4date($date2); // thêm 0 vào ngày tháng dưới 10
    $rs = vn_date_yyyy($date3); // chuyển năm về dạng 4 số
        
return $rs;        
}

///==================================================================== End code

Đầu tiên là hàm PHP kiểm tra một dữ liệu có phải ngày tháng năm không, nó kiểm tra tương đối chặt, nhưng chưa ép để buộc phải là ngày tháng năm sinh. Cái này có ích trong việc kiểm tra dữ liệu đầu vào:

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// kiểm tra đầu vào có phải ngày tháng năm hay không, bất kể dạng nào, không riêng gì ngày tháng năm sinh
function vn_is_date_ok($dateip){
    $rs = 0;
        $date = trim($dateip, ' '); // loại bỏ khoảng trống 2 bên, dự phòng vì cũng hiếm gặp
        // kiểm tra khớp với kiểu dữ liệu ngày tháng năm, mẫu bên dưới, chấp nhận 3 kiểu phân cách là /, - và .
        $reg_dmy = '/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/'; // kiểu ngày, tháng, năm
        $reg_mdy = '/^(0?[1-9]|1[012])[\/\-\.](0?[1-9]|[12]\d|3[01])[\/\-\.](\d{2}|\d{4})$/'; // kiểu tháng, ngày, năm

        $rs_datex = preg_match($reg_dmy, $date);
        $rs_datey = preg_match($reg_mdy, $date);

        if (($rs_datex == 1) || ($rs_datey == 1)) {$rs = 1;} //bằng 1 nghĩa là phải khớp một trong hai dạng này
                    
return $rs;
}
  • Kết quả của hàm được gắn vào biến $rs, mặc định gán bằng 0, tương đương với FALSE, tức không phải, nếu kiểm tra đúng, giá trị này sẽ chuyển thành 1, tương đương với TRUE. Việc để giá trị như vậy rất tiện cho chúng ta sau này khi kết hợp với hàm kiểu như if;
  • trim() dùng để loại bỏ khoảng trắng trước và sau, cái này để dự phòng, nó không loại bỏ khoảng trắng giữa, khoảng trắng giữa dữ liệu ngày tháng năm có khả năng cao là dữ liệu sai nên chúng ta sẽ không can thiệp điều đó;
$reg_dmy = '/^(0?[1-9]|[12]\d|3[01])[\/\-\.](0?[1-9]|1[012])[\/\-\.](\d{2}|\d{4})$/';

Đây là biểu thức chính quy bắt khá chặt dữ liệu đầu vào ngày tháng năm, nó yêu cầu:

  • Ngày chỉ bắt đầu từ 1 đến 31, có thể có hoặc không có số 0 đằng trước ngày dưới 10;
  • Tháng chỉ bắt đầu từ 1 đến 12, có thể có hoặc không có số 0 đằng trước tháng dưới 10;
  • Năm chỉ có thể là số ở dạng hai số hoặc bốn số;
  • Thứ tự yêu cầu là ngày rồi đến tháng, rồi mới đến năm;
  • Ký tự ngăn cách chỉ có thể là dấu / hoặc – hoặc .
  • Không chấp nhận ký tự ngăn cách khác;
  • Dấu ^ và $ trong biểu thức chính quy ở trên yêu cầu toàn bộ chuỗi phải khớp với biểu thức chính quy này chứ không phải là một phần của chuỗi là đủ. Nghĩa là khi dùng nó chúng ta cũng loại luôn ngay các dữ liệu có các ký tự lạ không hợp chuẩn, ví dụ như ?, ‘ hay khoảng trắng,.., bất kỳ ký tự nào không phải số, không phải là một trong 3 ký tự phân cách được chấp nhận;

$reg_mdy = '/^(0?[1-9]|1[012])[\/\-\.](0?[1-9]|[12]\d|3[01])[\/\-\.](\d{2}|\d{4})$/';

Có ý nghĩa tương tự, chỉ khác là nó theo định dạng tháng, ngày rồi năm.

$rs_datex = preg_match($reg_dmy, $date);
$rs_datey = preg_match($reg_mdy, $date);

Hai hàm để so sánh kết quả, nó trả về 1 nếu khớp, trả về 0 nếu không khớp.

if (($rs_datex == 1) || ($rs_datey == 1)) {$rs = 1;} //bằng 1 nghĩa là phải khớp một trong hai dạng này

Cái này dùng để gán giá trị cho biến $rs. Nếu khớp một trong 2 dạng thì xác thự đó là dữ liệu ngày tháng năm theo yêu cầu của chúng ta.


Tiếp theo là hàm chuyển đổi dấu phân cách:

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// dữ liệu đầu vào đã là ngày tháng năm tiêu chuẩn (is_date_ok), giờ chỉ đổi dấu phân cách . và - thành /
function vn_separation_date($date){
    $date2 = preg_replace('/\./', '/', $date); //thay dấu chấm
    $date3 = preg_replace('/-/', '/', $date2); //thay dấu gạch ngang
        
return $date3;        
}

Dữ liệu đầu vào đạt yêu cầu khi kiểm tra với hàm is_date_ok ở trên sẽ được đưa vào đây.

Chúng ta sẽ thay thế dấu phân cách dạng . và dạng - thành dạng / cho đồng bộ.


Ở đây chúng ta sẽ thêm số 0 vào ngày dưới 10, và cả vào tháng dưới 10 nữa, cho phù hợp với yêu cầu:

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// thêm số 0 vào đầu ngày và tháng đối với các giá trị từ 1 tới 9. Dữ liệu đầu vào đã đạt is_date_ok() và separation_date()

function vn_zero4date($date){
    $sp_date = mb_split('/',$date); // tách
    $day = (int)$sp_date[0]; // chuyển thành dạng số
    $month = (int)$sp_date[1]; // chuyển thành dạng số
    $year = $sp_date[2];
    
    if ($day < 10) {$day='0'.$day;} // gắn thêm 0 vào
    if ($month < 10) {$month='0'.$month;} // gắn thêm 0 vào
    
    $date2 = $day.'/'.$month.'/'.$year;
    
return $date2;    
}

Tiếp đến là phần chuyển năm từ dạng 2 số về dạng 4 số, chỉ dành cho ngày tháng năm sinh.

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// đổi năm từ dạng 2 số thành dạng 4 số, dành cho ngày tháng năm sinh
function vn_date_yyyy($date) {
    $sp_date = mb_split('/',$date); // tách
    $day = $sp_date[0]; 
    $month = $sp_date[1]; 
    $year = (int)$sp_date[2]; // chuyển thành dạng số
    $now = date("y"); // lấy 2 số cuối năm hiện tại
    
    if ($year < 100) {
        if ($year > $now) {$year = "19".$year;} // nếu 2 số cuối lớn hơn 2 số năm hiện tại thì gắn 19 vào
        else {$year = "20".$year;}
    }
    $date2 = $day.'/'.$month.'/'.$year;

return $date2;    
}

Ở đây tôi chỉ thiết kế nó cho ngày tháng năm sinh của những người trong khoảng thời gian 100 năm tính từ hiện tại, rơi vào khoảng 1922 cho đến 2021 (năm viết bài là năm 2021). Các năm tiếp theo nó sẽ tự điều chỉnh tăng dần. Ví dụ vào năm 2024 thì sẽ là 1925 cho đến 2024.

Thuật toán đơn giản theo quy tắc ngón tay cái, đúng trong phần lớn trường hợp. Ở đây nếu 2 số cuối của năm lớn hơn 2 số cuối của năm hiện tại thì gắn 19 vào, nếu không thì gắn 20 vào. Bạn nào cần điều chỉnh riêng sẽ cần bổ sung thêm mã.


Tiếp đến là hàm chuẩn hóa tổng hợp từ các hàm trên:

//// LƯU Ý 
///=============================================================================
// chuẩn hóa từ A tới Z dữ liệu ngày tháng năm sinh
// chuyển năm về dạng 4 số
// thêm 0 vào ngày tháng dưới 10
// chuyển các ký tự phân cách ngày tháng là - và . về /

function vn_stand_date($date) {
    $date2 = vn_separation_date($date); // chuyển các ký tự phân cách ngày tháng là - và . về /
    $date3 = vn_zero4date($date2); // thêm 0 vào ngày tháng dưới 10
    $rs = vn_date_yyyy($date3); // chuyển năm về dạng 4 số
        
return $rs;        
}

Khi cần xác thực một ngày nào đó có phải là ngày tháng năm thực hay không bạn có thể dùng hàm này:

function vn_true_date($date) { // kiểm tra một ngày có phải là thực hay không dữ liệu đầu vào là dd/mm/yy hoặc dd/mm/yyyy
    $rs = 0;
    
    if (isset($date)) {    
    $arr_date = mb_split('/', $date);} // cắt ngày tháng năm thành các chuỗi chuỗi số dựa trên ký tự phân cách

    if (isset($arr_date) && count($arr_date)===3) {   
        $day = (int)$arr_date[0]; // lấy ngày và chuyển sang dạng số
        $month = (int)$arr_date[1]; // lấy tháng và chuyển sang dạng số
        $year = (int)$arr_date[2]; // lấy năm và chuyển sang dạng số

        if (checkdate($month, $day, $year)) {$rs = 1;} // checkdate là hàm có sẵn của PHP
    } 
    
return $rs;
}

Dự phòng bạn nào kiểm tra thẳng từ dữ liệu đầu vào, mà không qua các bước đầu tôi bổ sung thêm isset($date) để tránh trường hợp đầu vào là NULL hoặc rỗng dẫn đến xử lý có vấn đề.

Nếu ngày tháng năm là đúng nó sẽ trả về kết quả TRUE.


Hàm giới hạn năm sinh. Ví dụ bạn muốn lấy dữ liệu chỉ từ những ai sinh trong khoảng thời gian nào đó. Cái này cũng thường dùng trong phân tích, khi dữ liệu năm sinh phân tán không đều, những năm tập trung nhiều đối tượng để phân tích nên tạo thành nhóm riêng, vì dữ liệu có tính tin cậy cao hơn khi rút trích thống kê về thời gian.

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// giới hạn năm sinh, đã chuẩn hóa về kiểu ngày, tháng , năm 4 số, với việc phân cách bắng dấu /, ví dụ 15/05/2015
// ví dụ giới hạn từ năm 2005 đến năm 2015
// $low là năm thấp nhất chấp nhận được
// $up là năm cao nhất chấp nhận được

function vn_limit_year($date,$low,$up) {
    $rs = 0;
    $sp_date = mb_split('/',$date); // tách
    $year = (int)$sp_date[2]; // chuyển thành dạng số
    if ($year >= $low && $year <= $up) {$rs=1;}
    
return $rs;    
}