Tony Marston's Blog About software development, PHP and OOP

A class for validating and formatting dates

Posted on 9th June 2003 by Tony Marston

Amended on 21st November 2011

Intended Audience
Prerequisites
Defining the class to handle dates
- Class Variables
- Class Constructor
- Accept date from the User
- Display date to the User
- Incrementing or Decrementing a date
Using this class in your code
Summary
Amendment History
Comments

Intended Audience

This tutorial is intended for developers who want to take a user-supplied date and format it so that it can be added to the database, and then to take a date from the database and format it for display to the user.

Prerequisites

It is assumed you have basic knowledge of PHP. This tutorial will cover some of the following areas in PHP:

Defining the class to handle dates

Class Variables

We must start by defining our date class and the class variables which we would like to persist between one function call and the next:

class date_class
{
   var $monthalpha;            // array of 3-character month names
   var $internaldate;          // date as held in the database (yyyymmdd)
   var $externaldate;          // date as shown to the user (dd Mmm yyyy)
   var $errors;                // error messages
   var $date_format = 'dmy';   // date format - 'dmy', 'mdy' or 'ymd'

Class Constructor

This is followed by what is known as the 'class constructor', a function which is called automatically when an instance of this class is created. In PHP4 the name of the constructor is the same as the class name. It is used here to construct an array of month names. Note that we are forcing the index number to start at 1 instead of the default of 0.

   function date_class ()
   {
      $this->monthalpha = array(1 => 'Jan','Feb','Mar','Apr','May','Jun',
                                     'Jul','Aug','Sep','Oct','Nov','Dec');

      if (isset($GLOBALS['date_format'])) {
          $this->date_format = $GLOBALS['date_format'];
      } // if
	   
   } // date_class

Accept date from the User

The first method (function) in this class will accept input from the user, validate it, and format it ready for writing to the database. The user may input a date in a number of different ways, so we will use regular expressions to help us decipher the user's input.

With regular expressions we can supply a pattern with any number of component parts, and if the input string matches the pattern each part of the input which matched a part of the pattern is placed in an output array.

The first regular expression will look for input in the format d(d)?m(m)?y(yyy) (1 or 2 digits, a separator, 1 or 2 digits, a separator, then 1-4 digits). For a separator I am allowing any character which is not a number or a letter.

   function getInternalDate ($input)
   {
		
      // look for d(d)?m(m)?(yyyy) format (may also be m(m)?d(d)?y(yyy) format)
      $pattern = '/'
               . '(^[0-9]{1,2})'      // 1 or 2 digits
               . '([^0-9a-zA-Z])'     // not alpha or numeric
               . '([0-9]{1,2})'       // 1 or 2 digits
               . '([^0-9a-zA-Z])'     // not alpha or numeric
               . '([0-9]{0,4}$)'      // 0 to 4 digits
               . '/';
      if (preg_match($pattern, $input, $regs)) {
          if (preg_match('#^(dmy|dd/mm/yyyy|dd\.mm\.yyyy|dd/mm/yy)$#i', $this->date_format)) {
              $result = $this->verifyDate($regs[1], $regs[3], $regs[5]);
          } else { // assume 'mdy'
              $result = $this->verifyDate($regs[3], $regs[1], $regs[5]);
          } // if
          return $result;
      } // if

If this fails the next regular expression will look for input in the format d(d)?MMM?y(yyy).

      // look for d(d)?MMM?(yyyy) format
      $pattern = '/'
                .'(^[0-9]{1,2})'    // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([a-zA-Z]{1,})'   // 1 or more alpha
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,4}$)'    // 1 to 4 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[3],$regs[5]);
         return $result;
      } // if

The next regular expression will look for input in the format d(d)MMMy(yyy).

      // look for d(d)MMM(yyyy) format
      $pattern = '/'
                .'(^[0-9]{1,2})'    // 1 or 2 digits
                .'([a-zA-Z]{1,})'   // 1 or more alpha
                .'([0-9]{1,4}$)'    // 1 to 4 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[2],$regs[3]);
         return $result;
      } // if

The next regular expression will look for input in the format MMM?d(d)?y(yyy).

      // look for MMM?d(d)?(yyyy) format
      $pattern = '/'
                .'(^[a-zA-Z]{1,})'  // 1 or more alpha
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2})'     // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,4}$)'    // 1 to 4 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[3],$regs[1],$regs[5]);
         return $result;
      } // if

The next regular expression will look for input in the format MMMddyyyy.

      // look for MMMddyyyy format
      $pattern = '/'
                .'(^[a-zA-Z]{1,})'  // 1 or more alpha
                .'([0-9]{2})'       // 2 digits
                .'([0-9]{4}$)'      // 4 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[2],$regs[1],$regs[3]);
         return $result;
      } // if

The next regular expression will look for input in the format yyyy?m(m)?d(d).

      // look for yyyy?m(m)?d(d) format
      $pattern = '/'
                .'(^[0-9]{4})'      // 4 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2})'     // 1 or 2 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2}$)'    // 1 to 2 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[5],$regs[3],$regs[1]);
         return $result;
      } // if

The next regular expression will look for input in the format ddmmyyyy.

      // look for ddmmyyyy format
      $pattern = '/'
                .'(^[0-9]{2})'      // 2 digits
                .'([0-9]{2})'       // 2 digits
                .'([0-9]{4}$)'      // 4 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[1],$regs[2],$regs[3]);
         return $result;
      } // if

The next regular expression will look for input in the format yyyy?MMM?d(d).

      // look for yyyy?MMM?d(d) format
      $pattern = '/'
                .'(^[0-9]{4})'      // 4 digits
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([a-zA-Z]{1,})'   // 1 or more alpha
                .'([^0-9a-zA-Z])'   // not alpha or numeric
                .'([0-9]{1,2}$)'    // 1 to 2 digits
                .'/';
      
      if (preg_match($pattern, $input, $regs)) {
         $result = $this->verifyDate($regs[5],$regs[3],$regs[1]);
         return $result;
      } // if

If we get to this point in the code it means that we have not found a match against any of those patterns, so we generate an error message and return an empty result to the user.

      $this->errors = 'This is not a valid date';
      return FALSE;
  
   } // getInternalDate

You may have noticed that after matching the input to a pattern function called verifyDate was called. Here is the contents of that function, with verifies that the component parts actually constitute a valid date.

   function verifyDate($day, $month, $year)
   {

This first part will check for the month being supplied as characters instead of numbers, and perform a lookup on the array of month names created in the class constructor. Note that it uses strtolower() to convert the user's input to lowercase, then ucfirst() to make the first character uppercase. This is done to match the contents of $this->monthalpha.

      if (preg_match('/([a-z]{3})/i', $month)) {
         $month = ucfirst(strtolower($month));
         if (!$month = array_search($month, $this->monthalpha)) {
            $this->errors = 'Month name is invalid';
            return FALSE;
         } // if
      } // if

This next part will check that the year has 4 digits, filling in anything that is missing:

      if (strlen($year) == 1) {
         $year = '200' .$year;
      } // if
      if (strlen($year) == 2) {
         $year = '20' .$year;
      } // if
      if (strlen($year) == 3) {
         $year = '2' .$year;
      } // if

Now we can use the PHP checkdate() function to check that this is a valid date, and if it is we can change the format to ccyy-mm-dd ready for writing to the database.

      if (!checkdate($month, $day, $year)) {
         $this->errors = 'This is not a valid date';
         return FALSE;
      } else {
         if (strlen($day) < 2) {
            $day = '0' .$day;		// add leading zero
         } // if
         if (strlen($month) < 2) {
            $month = '0' .$month;	// add leading zero
         } // if
         $this->internaldate = $year .'-' .$month .'-' .$day;
         return $this->internaldate;
      } // if
      
      return;
      
   } // verifyDate

Display date to the User

This function will take a date from the database (although it could come from other sources) and format it ready for display to the user.

   function getExternalDate ($input)
   {

This first part checks for input in the format yyyymmdd.

      if (strlen($input) == 8) {
         $pattern = '/'
                   .'(^[0-9]{4})'    // 4 digits (yyyy)
                   .'([0-9]{2})'     // 2 digits (mm)
                   .'([0-9]{2}$)'    // 2 digits (dd)
                   .'/';
         if (preg_match($pattern, $input, $regs)) {
            if (!checkdate($regs[2], $regs[3], $regs[1])) {
               $this->errors = 'This is not a valid date';
               return FALSE;
            } else {
               $monthnum = (int)$regs[2];
               $this->externaldate = "$regs[3] " .$this->monthalpha[$monthnum] ." $regs[1]";
               return $this->externaldate;
            } // if
         } // if
         $this->errors = "Invalid date format: expected 'yyyymmdd'";
         return FALSE;
      } // if

If this does not find a match this next part checks for input in the format yyyy-mm-dd.

      if (strlen($input) == 10) {
         $pattern = '/'
                   .'(^[0-9]{4})'   // 4 digits (yyyy)
                   .'([^0-9])'      // not a digit
                   .'([0-9]{2})'    // 2 digits (mm)
                   .'([^0-9])'      // not a digit
                   .'([0-9]{2}$)'   // 2 digits (dd)
                   .'/';
         if (preg_match($pattern, $input, $regs)) {
            if (!checkdate($regs[3], $regs[5], $regs[1])) {
               $this->errors = 'This is not a valid date';
               return FALSE;
            } else {
               $monthnum = (int)$regs[3];
               $this->externaldate = "$regs[5] "  .$this->monthalpha[$monthnum] ." $regs[1]";
               return $this->externaldate;
            } // if
         } // if
         $this->errors = "Invalid date format: expected 'dd-mm-yyyy'";
         return FALSE;
      } // if

If we have not found a match against any of these patterns then we generate an error message before returning an empty result to the user.

      $this->errors = 'This is not a valid date';
      return $input;
      
   } // getExternalDate

Incrementing or Decrementing a date

There may come a time when you need to find a date which is several days before or after a given date. This can be done by using the GregoriantoJD() function to convert a Gregorian date to a Julian date (the number of days since a base date, which is 4714 BC in this case), add or subtract from the Julian day count, then use the JDtoGregorian() function to convert from Julian back into Gregorian. This can be done by adding another function to our date class. In the following code you will notice that the new addDays() function makes use of the getInternalDate() function to process the input and the getExternalDate() function to format the output.

   function addDays ($internaldate, $days) 
   // add a number of days (may be negative) to $internaldate (YYYY-MM-DD) 
   // and return the result in the same format 
   { 
    
      // ensure date is in internal format 
      $internaldate = $this->getInternalDate($internaldate); 
       
      // convert to the number of days since basedate (4714 BC) 
      $julian = GregoriantoJD(substr($internaldate,5,2) 
                             ,substr($internaldate,8,2) 
                             ,substr($internaldate,0,4)); 
         
      $days = (int)$days; 
      $julian = $julian + $days; 
         
      // convert from Julian to Gregorian (format m/d/y) 
      $gregorian = JDtoGregorian($julian); 
         
      // split date into its component parts 
      list ($month, $day, $year) = split ('[/]', $gregorian); 
         
      // convert back into standard format 
      $result = $this->getInternaldate("$day/$month/$year"); 
         
      return $result; 
     
   } // addDays

Using this class in your code

In order to use any of the functions in this class you will need code similar to the following:

First you must create an object from the class (or instantiate a class instance in OOP speak):

$dateobj = RDCsingleton::getInstance('date_class');

This code will take a date in external (user) format and convert it to internal (database) format, with any error messages being inserted into the $errors array.

if (!$internaldate = $dateobj->getInternalDate($_POST['date']) {
   $errors = $dateobj->errors;
} // if

This code will take a date in internal (database) format and convert it to external (user) format, with any error messages being inserted into the $errors array.

if (!$externaldate = $dateobj->getExternalDate($internaldate]) {
   $errors = $dateobj->errors;
} // if

This code will take a date and obtain the dates for the previous and next days.

$today     = date('Y-m-d');
$tomorrow  = $dateobj->addDays($today, +1);
$yesterday = $dateobj->addDays($today, -1);

Summary

It is always useful to have a standard way of validating and formatting dates. Hopefully this tutorial has demonstrated how easily this can be done by using functions defined within a date class. It should now be a straightforward process for you to take this source code and customise it for your own purposes.

The full source code for this class can be viewed here.


Amendment history:

21 Nov 2011 Amended code to replace the deprecated POSIX ereg* functions with the equivalent PCRE preg* functions.

counter