4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Intl\DateFormatter\DateFormat
;
14 use Symfony\Component\Intl\Exception\NotImplementedException
;
15 use Symfony\Component\Intl\Globals\IntlGlobals
;
16 use Symfony\Component\Intl\DateFormatter\DateFormat\MonthTransformer
;
19 * Parser and formatter for date formats
21 * @author Igor Wiedler <igor@wiedler.ch>
25 private $quoteMatch = "'(?:[^']+|'')*'";
26 private $implementedChars = 'MLydQqhDEaHkKmsz';
27 private $notImplementedChars = 'GYuwWFgecSAZvVW';
33 private $transformers;
41 * @param string $pattern The pattern to be used to format and/or parse values
42 * @param string $timezone The timezone to perform the date/time calculations
44 public function __construct($pattern, $timezone)
46 $this->pattern
= $pattern;
47 $this->timezone
= $timezone;
49 $implementedCharsMatch = $this->buildCharsMatch($this->implementedChars
);
50 $notImplementedCharsMatch = $this->buildCharsMatch($this->notImplementedChars
);
51 $this->regExp
= "/($this->quoteMatch|$implementedCharsMatch|$notImplementedCharsMatch)/";
53 $this->transformers
= array(
54 'M' => new MonthTransformer(),
55 'L' => new MonthTransformer(),
56 'y' => new YearTransformer(),
57 'd' => new DayTransformer(),
58 'q' => new QuarterTransformer(),
59 'Q' => new QuarterTransformer(),
60 'h' => new Hour1201Transformer(),
61 'D' => new DayOfYearTransformer(),
62 'E' => new DayOfWeekTransformer(),
63 'a' => new AmPmTransformer(),
64 'H' => new Hour2400Transformer(),
65 'K' => new Hour1200Transformer(),
66 'k' => new Hour2401Transformer(),
67 'm' => new MinuteTransformer(),
68 's' => new SecondTransformer(),
69 'z' => new TimeZoneTransformer(),
74 * Return the array of Transformer objects
76 * @return Transformer[] Associative array of Transformer objects (format char => Transformer)
78 public function getTransformers()
80 return $this->transformers
;
84 * Format a DateTime using ICU dateformat pattern
86 * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value
88 * @return string The formatted value
90 public function format(\DateTime
$dateTime)
94 $formatted = preg_replace_callback($this->regExp
, function($matches) use ($that, $dateTime) {
95 return $that->formatReplace($matches[0], $dateTime);
102 * Return the formatted ICU value for the matched date characters
104 * @param string $dateChars The date characters to be replaced with a formatted ICU value
105 * @param DateTime $dateTime A DateTime object to be used to generate the formatted value
107 * @return string The formatted value
109 * @throws NotImplementedException When it encounters a not implemented date character
111 public function formatReplace($dateChars, $dateTime)
113 $length = strlen($dateChars);
115 if ($this->isQuoteMatch($dateChars)) {
116 return $this->replaceQuoteMatch($dateChars);
119 if (isset($this->transformers
[$dateChars[0]])) {
120 $transformer = $this->transformers
[$dateChars[0]];
122 return $transformer->format($dateTime, $length);
125 // handle unimplemented characters
126 if (false !== strpos($this->notImplementedChars
, $dateChars[0])) {
127 throw new NotImplementedException(sprintf("Unimplemented date character '%s' in format '%s'", $dateChars[0], $this->pattern
));
132 * Parse a pattern based string to a timestamp value
134 * @param \DateTime $dateTime A configured DateTime object to use to perform the date calculation
135 * @param string $value String to convert to a time value
137 * @return int The corresponding Unix timestamp
139 * @throws \InvalidArgumentException When the value can not be matched with pattern
141 public function parse(\DateTime
$dateTime, $value)
143 $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern
);
144 $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/';
148 if (preg_match($reverseMatchingRegExp, $value, $matches)) {
149 $matches = $this->normalizeArray($matches);
151 foreach ($this->transformers
as $char => $transformer) {
152 if (isset($matches[$char])) {
153 $length = strlen($matches[$char]['pattern']);
154 $options = array_merge($options, $transformer->extractDateOptions($matches[$char]['value'], $length));
158 // reset error code and message
159 IntlGlobals
::setError(IntlGlobals
::U_ZERO_ERROR
);
161 return $this->calculateUnixTimestamp($dateTime, $options);
164 // behave like the intl extension
165 IntlGlobals
::setError(IntlGlobals
::U_PARSE_ERROR
, 'Date parsing failed');
171 * Retrieve a regular expression to match with a formatted value.
173 * @param string $pattern The pattern to create the reverse matching regular expression
175 * @return string The reverse matching regular expression with named captures being formed by the
176 * transformer index in the $transformer array
178 public function getReverseMatchingRegExp($pattern)
182 $escapedPattern = preg_quote($pattern, '/');
184 // ICU 4.8 recognizes slash ("/") in a value to be parsed as a dash ("-") and vice-versa
185 // when parsing a date/time value
186 $escapedPattern = preg_replace('/\\\[\-|\/]/', '[\/\-]', $escapedPattern);
188 $reverseMatchingRegExp = preg_replace_callback($this->regExp
, function($matches) use ($that) {
189 $length = strlen($matches[0]);
190 $transformerIndex = $matches[0][0];
192 $dateChars = $matches[0];
193 if ($that->isQuoteMatch($dateChars)) {
194 return $that->replaceQuoteMatch($dateChars);
197 $transformers = $that->getTransformers();
198 if (isset($transformers[$transformerIndex])) {
199 $transformer = $transformers[$transformerIndex];
200 $captureName = str_repeat($transformerIndex, $length);
202 return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')';
206 return $reverseMatchingRegExp;
210 * Check if the first char of a string is a single quote
212 * @param string $quoteMatch The string to check
214 * @return Boolean true if matches, false otherwise
216 public function isQuoteMatch($quoteMatch)
218 return ("'" === $quoteMatch[0]);
222 * Replaces single quotes at the start or end of a string with two single quotes
224 * @param string $quoteMatch The string to replace the quotes
226 * @return string A string with the single quotes replaced
228 public function replaceQuoteMatch($quoteMatch)
230 if (preg_match("/^'+$/", $quoteMatch)) {
231 return str_replace("''", "'", $quoteMatch);
234 return str_replace("''", "'", substr($quoteMatch, 1, -1));
238 * Builds a chars match regular expression
240 * @param string $specialChars A string of chars to build the regular expression
242 * @return string The chars match regular expression
244 protected function buildCharsMatch($specialChars)
246 $specialCharsArray = str_split($specialChars);
248 $specialCharsMatch = implode('|', array_map(function($char) {
250 }, $specialCharsArray));
252 return $specialCharsMatch;
256 * Normalize a preg_replace match array, removing the numeric keys and returning an associative array
257 * with the value and pattern values for the matched Transformer
263 protected function normalizeArray(array $data)
267 foreach ($data as $key => $value) {
268 if (!is_string($key)) {
272 $ret[$key[0]] = array(
282 * Calculates the Unix timestamp based on the matched values by the reverse matching regular
283 * expression of parse()
285 * @param \DateTime $dateTime The DateTime object to be used to calculate the timestamp
286 * @param array $options An array with the matched values to be used to calculate the timestamp
288 * @return Boolean|int The calculated timestamp or false if matched date is invalid
290 protected function calculateUnixTimestamp(\DateTime
$dateTime, array $options)
292 $options = $this->getDefaultValueForOptions($options);
294 $year = $options['year'];
295 $month = $options['month'];
296 $day = $options['day'];
297 $hour = $options['hour'];
298 $hourInstance = $options['hourInstance'];
299 $minute = $options['minute'];
300 $second = $options['second'];
301 $marker = $options['marker'];
302 $timezone = $options['timezone'];
304 // If month is false, return immediately (intl behavior)
305 if (false === $month) {
306 IntlGlobals
::setError(IntlGlobals
::U_PARSE_ERROR
, 'Date parsing failed');
312 if ($hourInstance instanceof HourTransformer
) {
313 $hour = $hourInstance->normalizeHour($hour, $marker);
316 // Set the timezone if different from the default one
317 if (null !== $timezone && $timezone !== $this->timezone
) {
318 $dateTime->setTimezone(new \
DateTimeZone($timezone));
322 preg_match_all($this->regExp
, $this->pattern
, $matches);
323 if (in_array('yy', $matches[0])) {
324 $dateTime->setTimestamp(time());
325 $year = $year > $dateTime->format('y') +
20 ? 1900 +
$year : 2000 +
$year;
328 $dateTime->setDate($year, $month, $day);
329 $dateTime->setTime($hour, $minute, $second);
331 return $dateTime->getTimestamp();
335 * Add sensible default values for missing items in the extracted date/time options array. The values
336 * are base in the beginning of the Unix era
338 * @param array $options
342 private function getDefaultValueForOptions(array $options)
345 'year' => isset($options['year']) ? $options['year'] : 1970,
346 'month' => isset($options['month']) ? $options['month'] : 1,
347 'day' => isset($options['day']) ? $options['day'] : 1,
348 'hour' => isset($options['hour']) ? $options['hour'] : 0,
349 'hourInstance' => isset($options['hourInstance']) ? $options['hourInstance'] : null,
350 'minute' => isset($options['minute']) ? $options['minute'] : 0,
351 'second' => isset($options['second']) ? $options['second'] : 0,
352 'marker' => isset($options['marker']) ? $options['marker'] : null,
353 'timezone' => isset($options['timezone']) ? $options['timezone'] : null,