Lesson 3 - Finishing DateUtils library for PHP
In the previous lesson, DateUtils library for formatting date and time in PHP, we started working on a simple utility class used for working with date and time in PHP. In today's tutorial, we'll finish up with it.
Pretty date and time
I'm not sure if there is any specific name for this date and time format. However, I have implemented it as prettyDateTime() and prettyDate() methods. If the date is today, yesterday or tomorrow, it'll be written out in words, e.g. "Today". If it's sometime this year, the month will be written out in words, e.g. February 15. If the year is different than the current one, the entire date will be written in digits, e.g. 02/15/2013. Eventually, the time will be added to it as well. Surely you agree that this way is more pleasing to users than a bunch of numbers. When we're done, this library will let you use it with great ease.
First of all, let's create a private method which converts a given DateTime instance to a pretty date:
private static function getPrettyDate($dateTime) { $now = new DateTime(); if ($dateTime->format('Y') != $now->format('Y')) return $dateTime->format(self::DATE_FORMAT); $dayMonth = $dateTime->format('d-m'); if ($dayMonth == $now->format('d-m')) return "Today"; $now->modify('-1 DAY'); if ($dayMonth == $now->format('d-m')) return "Yesterday"; $now->modify('+2 DAYS'); if ($dayMonth == $now->format('d-m')) return "Tomorrow"; return self::$months[$dateTime->format('n') - 1] . ' ' . $dateTime->format('d'); }
In the method, we save the current date. If the years of the current and the given dates differ, we'll return the numeric date including the year. If the day and the month are the same, we'll return "Today". Then, we change the current date and check whether it isn't yesterday or tomorrow. If we exhaust all of the possibilities, we return the day and month as words.
Now, let's add two public methods which will format a date to the desired format:
public static function prettyDate($date) { return self::getPrettyDate(self::getDateTime($date)); } public static function prettyDateTime($date) { $dateTime = self::getDateTime($date); return self::getPrettyDate($dateTime) . $dateTime->format(' H:i:s'); }
Let's test it all out by converting several different dates into different formats:
echo(DateUtils::prettyDate(1446906152) . '<br />'); echo(DateUtils::prettyDateTime('2016-09-19 10:50') . '<br />'); echo(DateUtils::prettyDateTime('2016-09-18 10:50') . '<br />'); echo(DateUtils::prettyDate('20.9.2016') . '<br />'); echo(DateUtils::prettyDate('2016-09-01') . '<br />'); echo(DateUtils::prettyDate('2015/09/25') . '<br />');
The result:
As you can see, our method formats absolutely anything to a human-friendly format.
Parsing
Let's move on to the parsing method. We'll give it regional data as the input (e.g. 01/15/2014) as well as the format in which the date is supposed to be. The method will either throw an exception or return the date in the database format.
public static function parseDateTime($date, $format = self::DATETIME_FORMAT) { if (mb_substr_count($date, ':') == 1) $date .= ':00'; // Removes spaces around separators $a = array('/([\.\:\/])\s+/', '/\s+([\.\:\/])/', '/\s{2,}/'); $b = array('\1', '\1', ' '); $date = trim(preg_replace($a, $b, $date)); // Removes zeroes before numbers $a = array('/^0(\d+)/', '/([\.\/])0(\d+)/'); $b = array('\1', '\1\2'); $date = preg_replace($a, $b, $date); // Creates a DateTime instance which determines whether the given date exists $dateTime = DateTime::createFromFormat($format, $date); $errors = DateTime::getLastErrors(); // Throwing validation exceptions if ($errors['warning_count'] + $errors['error_count'] > 0) { if (in_array($format, self::$errorMessages)) throw new InvalidArgumentException(self::$errorMessages[$format]); else throw new InvalidArgumentException('Invalid value'); } // Returning the value in MySQL format return $dateTime->format(self::$formatDictionary[$format]); }
In the method, we concatenate "00" seconds to the date string in case there is exactly one colon. In that case, the user entered a time without specifying seconds. Then, we remove white characters before separators which are ":" and "/". Thanks to that, dates like "01/15/ 2014 12:00" pass and are converted to the following format, "01/15/2015 12:00:00". This means that the resulting date and time format is always left without white spaces and includes seconds.
We can pass this format to the DateTime class which performs the validation for us. You may find other desperate solutions, on the internet, where people call explode() on the date or other similar approaches. These validations are always incomplete, especially, the range control or control of component presence is quite complicated. Never program something which is already a part of the standard library for the given language! You'll never do it as well since PHP is written in the C language. Even if you do so, you could have used all of that time on something more efficient.
Then, we set the format for the DateTime instance, pass our date to it, and it'll try to parse it. Any errors that occur will be returned using the getLastErrors() method. If there are any, we throw an exception according to the format, otherwise, we return the date in the database format. We'll catch the exception later when we process the data and display it's message out to the user. In this case, it'd be ideal to create a custom exception.
Let's try our method out:
$date = DateUtils::parseDateTime('2/14/ 2016', DateUtils::DATE_FORMAT); echo($date . '<br />'); $dateTime = DateUtils::parseDateTime('02/24/ 2016 10:30', DateUtils::DATETIME_FORMAT); echo($dateTime . '<br />'); $time = DateUtils::parseDateTime('10:30', DateUtils::TIME_FORMAT); echo($time . '<br />');
The result:
Date validation
For the sake of completeness let's also add a validDate() method which will determine whether a given date is valid:
public static function validDate($date, $format = self::DATETIME_FORMAT) { try { self::parseDateTime($date, $format); return true; } catch (InvalidArgumentException $e) { } return false; }
Let's test the method out by entering several dates:
var_dump(DateUtils::validDate('2/24/ 2016', DateUtils::DATETIME_FORMAT)); var_dump(DateUtils::validDate('2/24 16', DateUtils::DATE_FORMAT)); var_dump(DateUtils::validDate('2/29/ 2014', DateUtils::DATE_FORMAT)); var_dump(DateUtils::validDate('2/ 29/ 2012', DateUtils::DATE_FORMAT)); var_dump(DateUtils::validDate('14:56', DateUtils::TIME_FORMAT)); var_dump(DateUtils::validDate('14:62', DateUtils::TIME_FORMAT));
The result:
The method works as expected!
Now()
Since MySQL is not able to set the current date and time for the DateTime column when inserting a new row automatically, I often use the following method, which returns the current date and time in the database format:
public static function dbNow() { $dateTime = new DateTime(); return $dateTime->format(self::DB_DATETIME_FORMAT); }
You may think that I'm a barbarian of sorts because I don't use the native NOW() function, however, this minor flaw is balanced out by the advantages of inserting a row straight from an array. By the way, the array will come straight from a form framework. The query will be generated automatically and will spare us a lot of work. If you've ever worked with ICT social's PDO wrapper, you know what I'm talking about. If you haven't, we'll get to it when we get to the form library.
Test this method out as well:
echo(DateUtils::dbNow());
The result:
The documented library is available for download below, I hope you'll put it
to good use I'm looking
forward to seeing you all in the next lesson, StringUtils library for working with texts in PHP, where we'll start writing
a similar utility class for strings.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 24x (6.75 kB)
Application includes source codes in language PHP