瀏覽代碼

Helper class for date & time format conversions between the various conventions for expressing date & time formats.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4017 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 9 年之前
父節點
當前提交
fe653e891f

+ 305 - 0
core/datetimeformat.class.inc.php

@@ -0,0 +1,305 @@
+<?php
+// Copyright (C) 2016 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * 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 <denis.flaven@combodo.com>
+ *
+ */
+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 */, '.?*$^()[]/:').'$';
+	}
+}

+ 14 - 0
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
 ));

+ 14 - 0
dictionaries/fr.dictionary.itop.core.php

@@ -714,6 +714,20 @@ Opérateurs :<br/>
 	'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)~~',

+ 8 - 7
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)
 				{