FullTransformer.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Locale\Stub\DateFormat;
  11. use Symfony\Component\Locale\Exception\NotImplementedException;
  12. use Symfony\Component\Locale\Stub\StubIntl;
  13. use Symfony\Component\Locale\Stub\DateFormat\MonthTransformer;
  14. /**
  15. * Parser and formatter for date formats
  16. *
  17. * @author Igor Wiedler <igor@wiedler.ch>
  18. */
  19. class FullTransformer
  20. {
  21. private $quoteMatch = "'(?:[^']+|'')*'";
  22. private $implementedChars = 'MLydQqhDEaHkKmsz';
  23. private $notImplementedChars = 'GYuwWFgecSAZvVW';
  24. private $regExp;
  25. private $transformers;
  26. private $pattern;
  27. private $timezone;
  28. /**
  29. * Constructor
  30. *
  31. * @param string $pattern The pattern to be used to format and/or parse values
  32. * @param string $timezone The timezone to perform the date/time calculations
  33. */
  34. public function __construct($pattern, $timezone)
  35. {
  36. $this->pattern = $pattern;
  37. $this->timezone = $timezone;
  38. $implementedCharsMatch = $this->buildCharsMatch($this->implementedChars);
  39. $notImplementedCharsMatch = $this->buildCharsMatch($this->notImplementedChars);
  40. $this->regExp = "/($this->quoteMatch|$implementedCharsMatch|$notImplementedCharsMatch)/";
  41. $this->transformers = array(
  42. 'M' => new MonthTransformer(),
  43. 'L' => new MonthTransformer(),
  44. 'y' => new YearTransformer(),
  45. 'd' => new DayTransformer(),
  46. 'q' => new QuarterTransformer(),
  47. 'Q' => new QuarterTransformer(),
  48. 'h' => new Hour1201Transformer(),
  49. 'D' => new DayOfYearTransformer(),
  50. 'E' => new DayOfWeekTransformer(),
  51. 'a' => new AmPmTransformer(),
  52. 'H' => new Hour2400Transformer(),
  53. 'K' => new Hour1200Transformer(),
  54. 'k' => new Hour2401Transformer(),
  55. 'm' => new MinuteTransformer(),
  56. 's' => new SecondTransformer(),
  57. 'z' => new TimeZoneTransformer(),
  58. );
  59. }
  60. /**
  61. * Return the array of Transformer objects
  62. *
  63. * @return array Associative array of Transformer objects (format char => Transformer)
  64. */
  65. public function getTransformers()
  66. {
  67. return $this->transformers;
  68. }
  69. /**
  70. * Format a DateTime using ICU dateformat pattern
  71. *
  72. * @param DateTime $dateTime A DateTime object to be used to generate the formatted value
  73. *
  74. * @return string The formatted value
  75. */
  76. public function format(\DateTime $dateTime)
  77. {
  78. $that = $this;
  79. $formatted = preg_replace_callback($this->regExp, function($matches) use ($that, $dateTime) {
  80. return $that->formatReplace($matches[0], $dateTime);
  81. }, $this->pattern);
  82. return $formatted;
  83. }
  84. /**
  85. * Return the formatted ICU value for the matched date characters
  86. *
  87. * @param string $dateChars The date characters to be replaced with a formatted ICU value
  88. * @param DateTime $dateTime A DateTime object to be used to generate the formatted value
  89. *
  90. * @return string The formatted value
  91. *
  92. * @throws NotImplementedException When it encounters a not implemented date character
  93. */
  94. public function formatReplace($dateChars, $dateTime)
  95. {
  96. $length = strlen($dateChars);
  97. if ($this->isQuoteMatch($dateChars)) {
  98. return $this->replaceQuoteMatch($dateChars);
  99. }
  100. if (isset($this->transformers[$dateChars[0]])) {
  101. $transformer = $this->transformers[$dateChars[0]];
  102. return $transformer->format($dateTime, $length);
  103. }
  104. // handle unimplemented characters
  105. if (false !== strpos($this->notImplementedChars, $dateChars[0])) {
  106. throw new NotImplementedException(sprintf("Unimplemented date character '%s' in format '%s'", $dateChars[0], $this->pattern));
  107. }
  108. }
  109. /**
  110. * Parse a pattern based string to a timestamp value
  111. *
  112. * @param DateTime $dateTime A configured DateTime object to use to perform the date calculation
  113. * @param string $value String to convert to a time value
  114. *
  115. * @return int The corresponding Unix timestamp
  116. *
  117. * @throws InvalidArgumentException When the value can not be matched with pattern
  118. */
  119. public function parse(\DateTime $dateTime, $value)
  120. {
  121. $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern);
  122. $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/';
  123. $options = array();
  124. if (preg_match($reverseMatchingRegExp, $value, $matches)) {
  125. $matches = $this->normalizeArray($matches);
  126. foreach ($this->transformers as $char => $transformer) {
  127. if (isset($matches[$char])) {
  128. $length = strlen($matches[$char]['pattern']);
  129. $options = array_merge($options, $transformer->extractDateOptions($matches[$char]['value'], $length));
  130. }
  131. }
  132. return $this->calculateUnixTimestamp($dateTime, $options);
  133. }
  134. // behave like the intl extension
  135. StubIntl::setError(StubIntl::U_PARSE_ERROR, 'Date parsing failed');
  136. return false;
  137. }
  138. /**
  139. * Retrieve a regular expression to match with a formatted value.
  140. *
  141. * @param string $pattern The pattern to create the reverse matching regular expression
  142. *
  143. * @return string The reverse matching regular expression with named captures being formed by the
  144. * transformer index in the $transformer array
  145. */
  146. public function getReverseMatchingRegExp($pattern)
  147. {
  148. $that = $this;
  149. $escapedPattern = preg_quote($pattern, '/');
  150. $reverseMatchingRegExp = preg_replace_callback($this->regExp, function($matches) use ($that) {
  151. $length = strlen($matches[0]);
  152. $transformerIndex = $matches[0][0];
  153. $dateChars = $matches[0];
  154. if ($that->isQuoteMatch($dateChars)) {
  155. return $that->replaceQuoteMatch($dateChars);
  156. }
  157. $transformers = $that->getTransformers();
  158. if (isset($transformers[$transformerIndex])) {
  159. $transformer = $transformers[$transformerIndex];
  160. $captureName = str_repeat($transformerIndex, $length);
  161. return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')';
  162. }
  163. }, $escapedPattern);
  164. return $reverseMatchingRegExp;
  165. }
  166. /**
  167. * Check if the first char of a string is a single quote
  168. *
  169. * @param string $quoteMatch The string to check
  170. *
  171. * @return Boolean true if matches, false otherwise
  172. */
  173. public function isQuoteMatch($quoteMatch)
  174. {
  175. return ("'" === $quoteMatch[0]);
  176. }
  177. /**
  178. * Replaces single quotes at the start or end of a string with two single quotes
  179. *
  180. * @param string $quoteMatch The string to replace the quotes
  181. *
  182. * @return string A string with the single quotes replaced
  183. */
  184. public function replaceQuoteMatch($quoteMatch)
  185. {
  186. if (preg_match("/^'+$/", $quoteMatch)) {
  187. return str_replace("''", "'", $quoteMatch);
  188. }
  189. return str_replace("''", "'", substr($quoteMatch, 1, -1));
  190. }
  191. /**
  192. * Builds a chars match regular expression
  193. *
  194. * @param string $specialChars A string of chars to build the regular expression
  195. *
  196. * @return string The chars match regular expression
  197. */
  198. protected function buildCharsMatch($specialChars)
  199. {
  200. $specialCharsArray = str_split($specialChars);
  201. $specialCharsMatch = implode('|', array_map(function($char) {
  202. return $char.'+';
  203. }, $specialCharsArray));
  204. return $specialCharsMatch;
  205. }
  206. /**
  207. * Normalize a preg_replace match array, removing the numeric keys and returning an associative array
  208. * with the value and pattern values for the matched Transformer
  209. *
  210. * @param array $data
  211. *
  212. * @return array
  213. */
  214. protected function normalizeArray(array $data)
  215. {
  216. $ret = array();
  217. foreach ($data as $key => $value) {
  218. if (!is_string($key)) {
  219. continue;
  220. }
  221. $ret[$key[0]] = array(
  222. 'value' => $value,
  223. 'pattern' => $key
  224. );
  225. }
  226. return $ret;
  227. }
  228. /**
  229. * Calculates the Unix timestamp based on the matched values by the reverse matching regular
  230. * expression of parse()
  231. *
  232. * @param DateTime $dateTime The DateTime object to be used to calculate the timestamp
  233. * @param array $options An array with the matched values to be used to calculate the timestamp
  234. *
  235. * @return Boolean|int The calculated timestamp or false if matched date is invalid
  236. */
  237. protected function calculateUnixTimestamp(\DateTime $dateTime, array $options)
  238. {
  239. $options = $this->getDefaultValueForOptions($options);
  240. $year = $options['year'];
  241. $month = $options['month'];
  242. $day = $options['day'];
  243. $hour = $options['hour'];
  244. $hourInstance = $options['hourInstance'];
  245. $minute = $options['minute'];
  246. $second = $options['second'];
  247. $marker = $options['marker'];
  248. $timezone = $options['timezone'];
  249. // If month is false, return immediately (intl behavior)
  250. if (false === $month) {
  251. StubIntl::setError(StubIntl::U_PARSE_ERROR, 'Date parsing failed');
  252. return false;
  253. }
  254. // Normalize hour
  255. if ($hourInstance instanceof HourTransformer) {
  256. $hour = $hourInstance->normalizeHour($hour, $marker);
  257. }
  258. // Set the timezone if different from the default one
  259. if (null !== $timezone && $timezone !== $this->timezone) {
  260. $dateTime->setTimezone(new \DateTimeZone($timezone));
  261. }
  262. // Normalize yy year
  263. preg_match_all($this->regExp, $this->pattern, $matches);
  264. if (in_array('yy', $matches[0])) {
  265. $dateTime->setTimestamp(time());
  266. $year = $year > $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
  267. }
  268. $dateTime->setDate($year, $month, $day);
  269. $dateTime->setTime($hour, $minute, $second);
  270. return $dateTime->getTimestamp();
  271. }
  272. /**
  273. * Add sensible default values for missing items in the extracted date/time options array. The values
  274. * are base in the beginning of the Unix era
  275. *
  276. * @param array $options
  277. *
  278. * @return array
  279. */
  280. private function getDefaultValueForOptions(array $options)
  281. {
  282. return array(
  283. 'year' => isset($options['year']) ? $options['year'] : 1970,
  284. 'month' => isset($options['month']) ? $options['month'] : 1,
  285. 'day' => isset($options['day']) ? $options['day'] : 1,
  286. 'hour' => isset($options['hour']) ? $options['hour'] : 0,
  287. 'hourInstance' => isset($options['hourInstance']) ? $options['hourInstance'] : null,
  288. 'minute' => isset($options['minute']) ? $options['minute'] : 0,
  289. 'second' => isset($options['second']) ? $options['second'] : 0,
  290. 'marker' => isset($options['marker']) ? $options['marker'] : null,
  291. 'timezone' => isset($options['timezone']) ? $options['timezone'] : null,
  292. );
  293. }
  294. }