From 9fd07125e261d2ffd75c0032fec236431b4c0baf Mon Sep 17 00:00:00 2001 From: Denis Flaven Date: Fri, 29 Apr 2016 07:53:45 +0000 Subject: [PATCH] Helper class for date & time format conversions between the various conventions for expressing date & time formats. SVN:trunk[4017] --- core/datetimeformat.class.inc.php | 305 +++++++++++++++++++++++ dictionaries/dictionary.itop.core.php | 14 ++ dictionaries/fr.dictionary.itop.core.php | 14 ++ test/testlist.inc.php | 15 +- 4 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 core/datetimeformat.class.inc.php diff --git a/core/datetimeformat.class.inc.php b/core/datetimeformat.class.inc.php new file mode 100644 index 000000000..8c398e682 --- /dev/null +++ b/core/datetimeformat.class.inc.php @@ -0,0 +1,305 @@ + + + +/** + * Helper class to generate Date & Time formatting strings in the various conventions + * from the PHP DateTime::createFromFormat convention. + * + * Example: + * + * $oFormat = new DateTimeFormat('m/d/Y H:i'); + * $oFormat->ToExcel(); + * >> 'MM/dd/YYYY HH:mm' + * + * @author Denis Flaven + * + */ +class DateTimeFormat +{ + protected $sPHPFormat; + + /** + * Constructs the DateTimeFormat object + * @param string $sPHPFormat A format string using the PHP 'DateTime::createFromFormat' convention + */ + public function __construct($sPHPFormat) + { + $this->sPHPFormat = $sPHPFormat; + } + + /** + * Return the mapping table for converting between various conventions for date/time formats + */ + protected static function GetFormatMapping() + { + return array( + // Days + 'd' => array('regexpr' => '(0[1-9]|[1-2][0-9]||3[0-1])', 'datepicker' => 'dd', 'excel' => 'dd', 'moment' => 'DD'), // Day of the month: 2 digits (with leading zero) + 'j' => array('regexpr' => '([1-9]|[1-2][0-9]||3[0-1])', 'datepicker' => 'd', 'excel' => '%d', 'moment' => 'D'), // Day of the month: 1 or 2 digits (without leading zero) + // Months + 'm' => array('regexpr' => '(0[1-9]|1[0-2])', 'datepicker' => 'mm', 'excel' => 'MM', 'moment' => 'MM' ), // Month on 2 digits i.e. 01-12 + 'n' => array('regexpr' => '([1-9]|1[0-2])', 'datepicker' => 'm', 'excel' => '%M', 'moment' => 'M'), // Month on 1 or 2 digits 1-12 + // Years + 'Y' => array('regexpr' => '([0-9]{4})', 'datepicker' => 'yy', 'excel' => 'YYYY', 'moment' => 'YYYY'), // Year on 4 digits + 'y' => array('regexpr' => '([0-9]{2})', 'datepicker' => 'y', 'excel' => 'YY', 'moment' => 'YY'), // Year on 2 digits + // Hours + 'H' => array('regexpr' => '([0-1][0-9]|2[0-3])', 'datepicker' => 'HH', 'excel' => 'HH', 'moment' => 'HH'), // Hour 00..23 + 'h' => array('regexpr' => '(0[1-9]|1[0-2])', 'datepicker' => 'hh', 'excel' => 'hh', 'moment' => 'hh'), // Hour 01..12 + 'G' => array('regexpr' => '([1-9]|[1[0-9]|2[0-3])', 'datepicker' => 'H', 'excel' => '%H', 'moment' => 'H'), // Hour 0..23 + 'g' => array('regexpr' => '([1-9]|1[0-2])', 'datepicker' => 'h', 'excel' => '%h', 'moment' => 'h'), // Hour 1..12 + 'a' => array('regexpr' => '(am|pm)', 'datepicker' => 'tt', 'excel' => 'am/pm', 'moment' => 'a'), + 'A' => array('regexpr' => '(AM|PM)', 'datepicker' => 'TT', 'excel' => 'AM/PM', 'moment' => 'A'), + // Minutes + 'i' => array('regexpr' => '([0-5][0-9])', 'datepicker' => 'mm', 'excel' => 'mm', 'moment' => 'mm'), + // Seconds + 's' => array('regexpr' => '([0-5][0-9])', 'datepicker' => 'ss', 'excel' => 'ss', 'moment' => 'ss'), + ); + } + + /** + * Transform the PHP format into the specified format, taking care of escaping the litteral characters + * using the supplied escaping expression + * @param string $sOutputFormatCode THe target format code: regexpr|datepicker|excel|moment + * @param string $sEscapePattern The replacement string for escaping characters in the output string. %s is the source char. + * @param string $bEscapeAll True to systematically escape all litteral characters + * @param array $sSpecialChars A string containing the only characters to escape in the output + * @return string The string in the requested format + */ + protected function Transform($sOutputFormatCode, $sEscapePattern, $bEscapeAll = false, $sSpecialChars = '') + { + $aMappings = static::GetFormatMapping(); + $sResult = ''; + + $bEscaping = false; + for($i=0; $i < strlen($this->sPHPFormat); $i++) + { + if (($this->sPHPFormat[$i] == '\\')) + { + $bEscaping = true; + continue; + } + + if ($bEscaping) + { + if (($sSpecialChars === '') || (strpos($sSpecialChars, $this->sPHPFormat[$i]) !== false)) + { + $sResult .= sprintf($sEscapePattern, $this->sPHPFormat[$i]); + } + else + { + $sResult .= $this->sPHPFormat[$i]; + } + + $bEscaping = false; + } + else if(array_key_exists($this->sPHPFormat[$i], $aMappings)) + { + // Not a litteral value, must be replaced by its regular expression pattern + $sResult .= $aMappings[$this->sPHPFormat[$i]][$sOutputFormatCode]; + } + else + { + if ($bEscapeAll || (strpos($sSpecialChars, $this->sPHPFormat[$i]) !== false)) + { + $sResult .= sprintf($sEscapePattern, $this->sPHPFormat[$i]); + } + else + { + // Normal char with no special meaning, no need to escape it + $sResult .= $this->sPHPFormat[$i]; + } + } + } + + return $sResult; + } + + /** + * Format a date into the supplied format string + * @param mixed $date An int, string, DateTime object or null !! + * @throws Exception + * @return string The formatted date + */ + public function Format($date) + { + if ($date == null) + { + $sDate = ''; + } + else if (($date === '0000-00-00') || ($date === '0000-00-00 00:00:00')) + { + $sDate = ''; + } + else if ($date instanceof DateTime) + { + // Parameter is a DateTime + $sDate = $date->format($this->sPHPFormat); + } + else if (is_int($date)) + { + // Parameter is a Unix timestamp + $oDate = new DateTime(); + $oDate->setTimestamp($date); + $sDate = $oDate->format($this->sPHPFormat); + } + else if (is_string($date)) + { + $oDate = new DateTime($date); + $sDate = $oDate->format($this->sPHPFormat); + } + else + { + throw new Exception(__CLASS__."::Format: Unexpected date value: ".print_r($date, true)); + } + return $sDate; + } + + /** + * Parse a date in the supplied format and return the date as a string in the internal format + * @param string $sDate The string to parse + * @param string $sFormat The format, in PHP createFromFormat convention + * @throws Exception + * @return DateTime|null + */ + public function Parse($sDate) + { + if (($sDate == null) || ($sDate == '0000-00-00 00:00:00') || ($sDate == '0000-00-00')) + { + return null; + } + else + { + $sFormat = preg_replace('/\\?/', '', $this->sPHPFormat); // replace escaped characters by a wildcard for parsing + $oDate = DateTime::createFromFormat($this->sPHPFormat, $sDate); + if ($oDate === false) + { + throw new Exception(__CLASS__."::Parse: Unable to parse the date: '$sDate' using the format: '{$this->sPHPFormat}'"); + } + return $oDate; + } + } + + /** + * Get the date or datetime format string in the jQuery UI date picker format + * @return string The format string using the date picker convention + */ + public function ToDatePicker() + { + return $this->Transform('datepicker', "'%s'"); + } + + /** + * Get a date or datetime format string in the Excel format + * @param string $sFormat + * @return string The format string using the Excel convention + */ + public function ToExcel($sFormat = null) + { + return $this->Transform('datepicker', "%s"); + } + + /** + * Get a date or datetime format string in the moment.js format + * @param string $sFormat + * @return string The format string using the moment.js convention + */ + public function ToMomentJS($sFormat = null) + { + return $this->Transform('moment', "[%s]", true /* escape all */); + } + + /** + * Get a placeholder text for a date or datetime format string + * @param string $sFormat + * @return string The placeholder text (localized) + */ + public function ToPlaceholder($sFormat = null) + { + $sFormat = ($sFormat == null) ? static::GetFormat() : $sFormat; + $aMappings = static::GetFormatMapping(); + $sResult = ''; + + $bEscaping = false; + for($i=0; $i < strlen($sFormat); $i++) + { + if (($sFormat[$i] == '\\')) + { + $bEscaping = true; + continue; + } + + if ($bEscaping) + { + $sResult .= $sFormat[$i]; // No need to escape characters in the placeholder + $bEscaping = false; + } + else if(array_key_exists($sFormat[$i], $aMappings)) + { + // Not a litteral value, must be replaced by Dict equivalent + $sResult .= Dict::S('Core:DateTime:Placeholder_'.$sFormat[$i]); + } + else + { + + // Normal char with no special meaning + $sResult .= $sFormat[$i]; + } + } + + return $sResult; + } + + /** + * Produces the Date format string by extracting only the date part of the date and time format string + * @return string + */ + public function ToDateFormat() + { + $aDatePlaceholders = array('Y', 'y', 'd', 'j', 'm', 'n'); + $iStart = 999; + $iEnd = 0; + + foreach($aDatePlaceholders as $sChar) + { + $iPos = strpos($this->sPHPFormat, $sChar); + if ($iPos !== false) + { + if (($iPos > 0) && ($aDatePlaceholders[$iPos-1] == '\\')) + { + // The placeholder is actually escaped, it's a litteral character, ignore it + continue; + } + $iStart = min($iStart, $iPos); + $iEnd = max($iEnd, $iPos); + } + } + $sFormat = substr($this->sPHPFormat, $iStart, $iEnd - $iStart + 1); + return $sFormat; + } + + /** + * Get the regular expression to (approximately) validate a date/time for the current format + * The validation does not take into account the number of days in a month (i.e. June 31st will pass, as well as Feb 30th!) + * @return string The regular expression in PCRE syntax + */ + public function ToRegExpr() + { + return '^'.$this->Transform('regexpr', "\\%s", false /* escape all */, '.?*$^()[]/:').'$'; + } +} diff --git a/dictionaries/dictionary.itop.core.php b/dictionaries/dictionary.itop.core.php index 1966b8080..d242becce 100644 --- a/dictionaries/dictionary.itop.core.php +++ b/dictionaries/dictionary.itop.core.php @@ -856,4 +856,18 @@ Dict::Add('EN US', 'English', 'English', array( 'Core:BulkExport:DateTimeFormat' => 'Date and Time format', 'Core:BulkExport:DateTimeFormatDefault_Example' => 'Default format (%1$s), e.g. %2$s', 'Core:BulkExport:DateTimeFormatCustom_Format' => 'Custom format: %1$s', + 'Core:DateTime:Placeholder_d' => 'DD', // Day of the month: 2 digits (with leading zero) + 'Core:DateTime:Placeholder_j' => 'D', // Day of the month: 1 or 2 digits (without leading zero) + 'Core:DateTime:Placeholder_m' => 'MM', // Month on 2 digits i.e. 01-12 + 'Core:DateTime:Placeholder_n' => 'M', // Month on 1 or 2 digits 1-12 + 'Core:DateTime:Placeholder_Y' => 'YYYY', // Year on 4 digits + 'Core:DateTime:Placeholder_y' => 'YY', // Year on 2 digits + 'Core:DateTime:Placeholder_H' => 'hh', // Hour 00..23 + 'Core:DateTime:Placeholder_h' => 'h', // Hour 01..12 + 'Core:DateTime:Placeholder_G' => 'hh', // Hour 0..23 + 'Core:DateTime:Placeholder_g' => 'h', // Hour 1..12 + 'Core:DateTime:Placeholder_a' => 'am/pm', // am/pm (lowercase) + 'Core:DateTime:Placeholder_A' => 'AM/PM', // AM/PM (uppercase) + 'Core:DateTime:Placeholder_i' => 'mm', // minutes, 2 digits: 00..59 + 'Core:DateTime:Placeholder_s' => 'ss', // seconds, 2 digits 00..59 )); diff --git a/dictionaries/fr.dictionary.itop.core.php b/dictionaries/fr.dictionary.itop.core.php index 8daefa744..e4ffa60ab 100644 --- a/dictionaries/fr.dictionary.itop.core.php +++ b/dictionaries/fr.dictionary.itop.core.php @@ -714,6 +714,20 @@ Opérateurs :
'Core:BulkExport:DateTimeFormat' => 'Format de date et heure', 'Core:BulkExport:DateTimeFormatDefault_Example' => 'Format par défaut (%1$s), ex. %2$s', 'Core:BulkExport:DateTimeFormatCustom_Format' => 'Format spécial: %1$s', + 'Core:DateTime:Placeholder_d' => 'JJ', // Day of the month: 2 digits (with leading zero) + 'Core:DateTime:Placeholder_j' => 'J', // Day of the month: 1 or 2 digits (without leading zero) + 'Core:DateTime:Placeholder_m' => 'MM', // Month on 2 digits i.e. 01-12 + 'Core:DateTime:Placeholder_n' => 'M', // Month on 1 or 2 digits 1-12 + 'Core:DateTime:Placeholder_Y' => 'AAAA', // Year on 4 digits + 'Core:DateTime:Placeholder_y' => 'AA', // Year on 2 digits + 'Core:DateTime:Placeholder_H' => 'hh', // Hour 00..23 + 'Core:DateTime:Placeholder_h' => 'h', // Hour 01..12 + 'Core:DateTime:Placeholder_G' => 'hh', // Hour 0..23 + 'Core:DateTime:Placeholder_g' => 'h', // Hour 1..12 + 'Core:DateTime:Placeholder_a' => 'am/pm', // am/pm (lowercase) + 'Core:DateTime:Placeholder_A' => 'AM/PM', // AM/PM (uppercase) + 'Core:DateTime:Placeholder_i' => 'mm', // minutes, 2 digits: 00..59 + 'Core:DateTime:Placeholder_s' => 'ss', // seconds, 2 digits 00..59 'Core:DeletedObjectLabel' => '%1s (effacé)', 'Core:SyncSplitModeCLIOnly' => 'The synchronization can be executed in chunks only if run in mode CLI~~', 'Core:ExecProcess:Code1' => 'Wrong command or command finished with errors (e.g. wrong script name)~~', diff --git a/test/testlist.inc.php b/test/testlist.inc.php index 985f1453d..d95774754 100644 --- a/test/testlist.inc.php +++ b/test/testlist.inc.php @@ -4902,6 +4902,7 @@ class TestDateTimeFormats extends TestBizModel static public function GetDescription() {return 'Check the formating and parsing of dates for various formats';} public function DoExecute() { + require_once(APPROOT.'core/datetimeformat.class.inc.php'); $bRet = true; $aTestFormats = array( 'French (short)' => 'd/m/Y H:i:s', @@ -4919,14 +4920,14 @@ class TestDateTimeFormats extends TestBizModel foreach($aTestFormats as $sDesc => $sFormat) { $this->ReportSuccess("Test of the '$sDesc' format: '$sFormat':"); - AttributeDateTime::SetFormat($sFormat); + $oFormat = new DateTimeFormat($sFormat); foreach($aTestDates as $sTestDate) { $oDate = new DateTime($sTestDate); - $sFormattedDate = AttributeDateTime::Format($oDate, AttributeDateTime::GetFormat()); - $sParsedDate = AttributeDateTime::Parse($sFormattedDate, AttributeDateTime::GetFormat()); - $sPattern = AttributeDateTime::GetRegExpr(); - $bParseOk = ($sParsedDate == $sTestDate); + $sFormattedDate = $oFormat->Format($oDate); + $oParsedDate = $oFormat->Parse($sFormattedDate); + $sPattern = $oFormat->ToRegExpr(); + $bParseOk = ($oParsedDate->format('Y-m-d H:i:s') == $sTestDate); if (!$bParseOk) { $this->ReportError('Parsed ('.$sFormattedDate.') date different from initial date (difference of '.((int)$oParsedDate->format('U')- (int)$oDate->format('U')).'s)'); @@ -4954,11 +4955,11 @@ class TestDateTimeFormats extends TestBizModel foreach($aInvalidTestDates as $sFormatName => $aDatesToParse) { $sFormat = $aTestFormats[$sFormatName]; - AttributeDateTime::SetFormat($sFormat); + $oFormat = new DateTimeFormat($sFormat); $this->ReportSuccess("Test of the '$sFormatName' format: '$sFormat':"); foreach($aDatesToParse as $sDate) { - $sPattern = AttributeDateTime::GetRegExpr(); + $sPattern = $oFormat->ToRegExpr(); $bValidateOk = preg_match('/'.$sPattern.'/', $sDate); if ($bValidateOk) {