datetimeformat.class.inc.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <?php
  2. // Copyright (C) 2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Helper class to generate Date & Time formatting strings in the various conventions
  20. * from the PHP DateTime::createFromFormat convention.
  21. *
  22. * Example:
  23. *
  24. * $oFormat = new DateTimeFormat('m/d/Y H:i');
  25. * $oFormat->ToExcel();
  26. * >> 'MM/dd/YYYY HH:mm'
  27. *
  28. * @author Denis Flaven <denis.flaven@combodo.com>
  29. *
  30. */
  31. class DateTimeFormat
  32. {
  33. protected $sPHPFormat;
  34. /**
  35. * Constructs the DateTimeFormat object
  36. * @param string $sPHPFormat A format string using the PHP 'DateTime::createFromFormat' convention
  37. */
  38. public function __construct($sPHPFormat)
  39. {
  40. $this->sPHPFormat = (string)$sPHPFormat;
  41. }
  42. /**
  43. * @return string
  44. */
  45. public function __toString()
  46. {
  47. return $this->sPHPFormat;
  48. }
  49. /**
  50. * Return the mapping table for converting between various conventions for date/time formats
  51. */
  52. protected static function GetFormatMapping()
  53. {
  54. return array(
  55. // Days
  56. '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)
  57. '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)
  58. // Months
  59. 'm' => array('regexpr' => '(0[1-9]|1[0-2])', 'datepicker' => 'mm', 'excel' => 'MM', 'moment' => 'MM' ), // Month on 2 digits i.e. 01-12
  60. 'n' => array('regexpr' => '([1-9]|1[0-2])', 'datepicker' => 'm', 'excel' => 'm', 'moment' => 'M'), // Month on 1 or 2 digits 1-12
  61. // Years
  62. 'Y' => array('regexpr' => '([0-9]{4})', 'datepicker' => 'yy', 'excel' => 'YYYY', 'moment' => 'YYYY'), // Year on 4 digits
  63. 'y' => array('regexpr' => '([0-9]{2})', 'datepicker' => 'y', 'excel' => 'YY', 'moment' => 'YY'), // Year on 2 digits
  64. // Hours
  65. 'H' => array('regexpr' => '([0-1][0-9]|2[0-3])', 'datepicker' => 'HH', 'excel' => 'HH', 'moment' => 'HH'), // Hour 00..23
  66. 'h' => array('regexpr' => '(0[1-9]|1[0-2])', 'datepicker' => 'hh', 'excel' => 'hh', 'moment' => 'hh'), // Hour 01..12
  67. 'G' => array('regexpr' => '([1-9]|[1[0-9]|2[0-3])', 'datepicker' => 'H', 'excel' => 'H', 'moment' => 'H'), // Hour 0..23
  68. 'g' => array('regexpr' => '([1-9]|1[0-2])', 'datepicker' => 'h', 'excel' => 'h', 'moment' => 'h'), // Hour 1..12
  69. 'a' => array('regexpr' => '(am|pm)', 'datepicker' => 'tt', 'excel' => 'am/pm', 'moment' => 'a'),
  70. 'A' => array('regexpr' => '(AM|PM)', 'datepicker' => 'TT', 'excel' => 'AM/PM', 'moment' => 'A'),
  71. // Minutes
  72. 'i' => array('regexpr' => '([0-5][0-9])', 'datepicker' => 'mm', 'excel' => 'mm', 'moment' => 'mm'),
  73. // Seconds
  74. 's' => array('regexpr' => '([0-5][0-9])', 'datepicker' => 'ss', 'excel' => 'ss', 'moment' => 'ss'),
  75. );
  76. }
  77. /**
  78. * Transform the PHP format into the specified format, taking care of escaping the litteral characters
  79. * using the supplied escaping expression
  80. * @param string $sOutputFormatCode THe target format code: regexpr|datepicker|excel|moment
  81. * @param string $sEscapePattern The replacement string for escaping characters in the output string. %s is the source char.
  82. * @param string $bEscapeAll True to systematically escape all litteral characters
  83. * @param array $sSpecialChars A string containing the only characters to escape in the output
  84. * @return string The string in the requested format
  85. */
  86. protected function Transform($sOutputFormatCode, $sEscapePattern, $bEscapeAll = false, $sSpecialChars = '')
  87. {
  88. $aMappings = static::GetFormatMapping();
  89. $sResult = '';
  90. $bEscaping = false;
  91. for($i=0; $i < strlen($this->sPHPFormat); $i++)
  92. {
  93. if (($this->sPHPFormat[$i] == '\\'))
  94. {
  95. $bEscaping = true;
  96. continue;
  97. }
  98. if ($bEscaping)
  99. {
  100. if (($sSpecialChars === '') || (strpos($sSpecialChars, $this->sPHPFormat[$i]) !== false))
  101. {
  102. $sResult .= sprintf($sEscapePattern, $this->sPHPFormat[$i]);
  103. }
  104. else
  105. {
  106. $sResult .= $this->sPHPFormat[$i];
  107. }
  108. $bEscaping = false;
  109. }
  110. else if(array_key_exists($this->sPHPFormat[$i], $aMappings))
  111. {
  112. // Not a litteral value, must be replaced by its regular expression pattern
  113. $sResult .= $aMappings[$this->sPHPFormat[$i]][$sOutputFormatCode];
  114. }
  115. else
  116. {
  117. if ($bEscapeAll || (strpos($sSpecialChars, $this->sPHPFormat[$i]) !== false))
  118. {
  119. $sResult .= sprintf($sEscapePattern, $this->sPHPFormat[$i]);
  120. }
  121. else
  122. {
  123. // Normal char with no special meaning, no need to escape it
  124. $sResult .= $this->sPHPFormat[$i];
  125. }
  126. }
  127. }
  128. return $sResult;
  129. }
  130. /**
  131. * Format a date into the supplied format string
  132. * @param mixed $date An int, string, DateTime object or null !!
  133. * @throws Exception
  134. * @return string The formatted date
  135. */
  136. public function Format($date)
  137. {
  138. if ($date == null)
  139. {
  140. $sDate = '';
  141. }
  142. else if (($date === '0000-00-00') || ($date === '0000-00-00 00:00:00'))
  143. {
  144. $sDate = '';
  145. }
  146. else if ($date instanceof DateTime)
  147. {
  148. // Parameter is a DateTime
  149. $sDate = $date->format($this->sPHPFormat);
  150. }
  151. else if (is_int($date))
  152. {
  153. // Parameter is a Unix timestamp
  154. $oDate = new DateTime();
  155. $oDate->setTimestamp($date);
  156. $sDate = $oDate->format($this->sPHPFormat);
  157. }
  158. else if (is_string($date))
  159. {
  160. $oDate = new DateTime($date);
  161. $sDate = $oDate->format($this->sPHPFormat);
  162. }
  163. else
  164. {
  165. throw new Exception(__CLASS__."::Format: Unexpected date value: ".print_r($date, true));
  166. }
  167. return $sDate;
  168. }
  169. /**
  170. * Parse a date in the supplied format and return the date as a string in the internal format
  171. * @param string $sDate The string to parse
  172. * @param string $sFormat The format, in PHP createFromFormat convention
  173. * @throws Exception
  174. * @return DateTime|null
  175. */
  176. public function Parse($sDate)
  177. {
  178. if (($sDate == null) || ($sDate == '0000-00-00 00:00:00') || ($sDate == '0000-00-00'))
  179. {
  180. return null;
  181. }
  182. else
  183. {
  184. $sFormat = preg_replace('/\\?/', '', $this->sPHPFormat); // replace escaped characters by a wildcard for parsing
  185. $oDate = DateTime::createFromFormat($this->sPHPFormat, $sDate);
  186. if ($oDate === false)
  187. {
  188. throw new Exception(__CLASS__."::Parse: Unable to parse the date: '$sDate' using the format: '{$this->sPHPFormat}'");
  189. }
  190. return $oDate;
  191. }
  192. }
  193. /**
  194. * Get the date or datetime format string in the jQuery UI date picker format
  195. * @return string The format string using the date picker convention
  196. */
  197. public function ToDatePicker()
  198. {
  199. return $this->Transform('datepicker', "'%s'");
  200. }
  201. /**
  202. * Get a date or datetime format string in the Excel format
  203. * @return string The format string using the Excel convention
  204. */
  205. public function ToExcel()
  206. {
  207. return $this->Transform('excel', "%s");
  208. }
  209. /**
  210. * Get a date or datetime format string in the moment.js format
  211. * @return string The format string using the moment.js convention
  212. */
  213. public function ToMomentJS()
  214. {
  215. return $this->Transform('moment', "[%s]", true /* escape all */);
  216. }
  217. public static function GetJSSQLToCustomFormat()
  218. {
  219. $aPHPToMoment = array();
  220. foreach(self::GetFormatMapping() as $sPHPCode => $aMapping)
  221. {
  222. $aPHPToMoment[$sPHPCode] = $aMapping['moment'];
  223. }
  224. $sJSMapping = json_encode($aPHPToMoment);
  225. $sFunction =
  226. <<<EOF
  227. function PHPDateTimeFormatToSubFormat(sPHPFormat, sPlaceholders)
  228. {
  229. var iMax = 0;
  230. var iMin = 999;
  231. var bEscaping = false;
  232. for(var i=0; i<sPHPFormat.length; i++)
  233. {
  234. var c = sPHPFormat[i];
  235. if (c == '\\\\')
  236. {
  237. bEscaping = true;
  238. continue;
  239. }
  240. if (bEscaping)
  241. {
  242. bEscaping = false;
  243. continue;
  244. }
  245. else
  246. {
  247. if (sPlaceholders.search(c) != -1)
  248. {
  249. iMax = Math.max(iMax, i);
  250. iMin = Math.min(iMin, i);
  251. }
  252. }
  253. }
  254. return sPHPFormat.substr(iMin, iMax - iMin + 1);
  255. }
  256. function PHPDateTimeFormatToMomentFormat(sPHPFormat)
  257. {
  258. var aFormatMapping = $sJSMapping;
  259. var sMomentFormat = '';
  260. var bEscaping = false;
  261. for(var i=0; i<sPHPFormat.length; i++)
  262. {
  263. var c = sPHPFormat[i];
  264. if (c == '\\\\')
  265. {
  266. bEscaping = true;
  267. continue;
  268. }
  269. if (bEscaping)
  270. {
  271. sMomentFormat += '['+c+']';
  272. bEscaping = false;
  273. }
  274. else
  275. {
  276. if (aFormatMapping[c] !== undefined)
  277. {
  278. sMomentFormat += aFormatMapping[c];
  279. }
  280. else
  281. {
  282. sMomentFormat += '['+c+']';
  283. }
  284. }
  285. }
  286. return sMomentFormat;
  287. }
  288. function DateFormatFromPHP(sSQLDate, sPHPFormat)
  289. {
  290. if (sSQLDate === '') return '';
  291. var sPHPDateFormat = PHPDateTimeFormatToSubFormat(sPHPFormat, 'Yydjmn');
  292. var sMomentFormat = PHPDateTimeFormatToMomentFormat(sPHPDateFormat);
  293. return moment(sSQLDate).format(sMomentFormat);
  294. }
  295. function DateTimeFormatFromPHP(sSQLDate, sPHPFormat)
  296. {
  297. if (sSQLDate === '') return '';
  298. var sMomentFormat = PHPDateTimeFormatToMomentFormat(sPHPFormat);
  299. return moment(sSQLDate).format(sMomentFormat);
  300. }
  301. EOF
  302. ;
  303. return $sFunction;
  304. }
  305. /**
  306. * Get a placeholder text for a date or datetime format string
  307. * @return string The placeholder text (localized)
  308. */
  309. public function ToPlaceholder()
  310. {
  311. $aMappings = static::GetFormatMapping();
  312. $sResult = '';
  313. $bEscaping = false;
  314. for($i=0; $i < strlen($this->sPHPFormat); $i++)
  315. {
  316. if (($this->sPHPFormat[$i] == '\\'))
  317. {
  318. $bEscaping = true;
  319. continue;
  320. }
  321. if ($bEscaping)
  322. {
  323. $sResult .= $this->sPHPFormat[$i]; // No need to escape characters in the placeholder
  324. $bEscaping = false;
  325. }
  326. else if(array_key_exists($this->sPHPFormat[$i], $aMappings))
  327. {
  328. // Not a litteral value, must be replaced by Dict equivalent
  329. $sResult .= Dict::S('Core:DateTime:Placeholder_'.$this->sPHPFormat[$i]);
  330. }
  331. else
  332. {
  333. // Normal char with no special meaning
  334. $sResult .= $this->sPHPFormat[$i];
  335. }
  336. }
  337. return $sResult;
  338. }
  339. /**
  340. * Produces a subformat (Date or Time) by extracting the part of the whole DateTime format containing only the given placeholders
  341. * @return string
  342. */
  343. protected function ToSubFormat($aPlaceholders)
  344. {
  345. $iStart = 999;
  346. $iEnd = 0;
  347. foreach($aPlaceholders as $sChar)
  348. {
  349. $iPos = strpos($this->sPHPFormat, $sChar);
  350. if ($iPos !== false)
  351. {
  352. if (($iPos > 0) && ($this->sPHPFormat[$iPos-1] == '\\'))
  353. {
  354. // The placeholder is actually escaped, it's a litteral character, ignore it
  355. continue;
  356. }
  357. $iStart = min($iStart, $iPos);
  358. $iEnd = max($iEnd, $iPos);
  359. }
  360. }
  361. $sFormat = substr($this->sPHPFormat, $iStart, $iEnd - $iStart + 1);
  362. return $sFormat;
  363. }
  364. /**
  365. * Produces the Date format string by extracting only the date part of the date and time format string
  366. * @return string
  367. */
  368. public function ToDateFormat()
  369. {
  370. return $this->ToSubFormat(array('Y', 'y', 'd', 'j', 'm', 'n'));
  371. }
  372. /**
  373. * Produces the Time format string by extracting only the time part of the date and time format string
  374. * @return string
  375. */
  376. public function ToTimeFormat()
  377. {
  378. return $this->ToSubFormat(array('H', 'h', 'G', 'g', 'i', 's', 'a', 'A'));
  379. }
  380. /**
  381. * Get the regular expression to (approximately) validate a date/time for the current format
  382. * 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!)
  383. * @return string The regular expression in PCRE syntax
  384. */
  385. public function ToRegExpr()
  386. {
  387. return '^'.$this->Transform('regexpr', "\\%s", false /* escape all */, '.?*$^()[]/:').'$';
  388. }
  389. }