123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Polyfill\Intl\Icu\DateFormat;
- use Symfony\Polyfill\Intl\Icu\Exception\NotImplementedException;
- use Symfony\Polyfill\Intl\Icu\Icu;
- /**
- * Parser and formatter for date formats.
- *
- * @author Igor Wiedler <igor@wiedler.ch>
- *
- * @internal
- */
- class FullTransformer
- {
- private $quoteMatch = "'(?:[^']+|'')*'";
- private $implementedChars = 'MLydQqhDEaHkKmsz';
- private $notImplementedChars = 'GYuwWFgecSAZvVW';
- private $regExp;
- /**
- * @var Transformer[]
- */
- private $transformers;
- private $pattern;
- private $timezone;
- /**
- * @param string $pattern The pattern to be used to format and/or parse values
- * @param string $timezone The timezone to perform the date/time calculations
- */
- public function __construct(string $pattern, string $timezone)
- {
- $this->pattern = $pattern;
- $this->timezone = $timezone;
- $implementedCharsMatch = $this->buildCharsMatch($this->implementedChars);
- $notImplementedCharsMatch = $this->buildCharsMatch($this->notImplementedChars);
- $this->regExp = "/($this->quoteMatch|$implementedCharsMatch|$notImplementedCharsMatch)/";
- $this->transformers = [
- 'M' => new MonthTransformer(),
- 'L' => new MonthTransformer(),
- 'y' => new YearTransformer(),
- 'd' => new DayTransformer(),
- 'q' => new QuarterTransformer(),
- 'Q' => new QuarterTransformer(),
- 'h' => new Hour1201Transformer(),
- 'D' => new DayOfYearTransformer(),
- 'E' => new DayOfWeekTransformer(),
- 'a' => new AmPmTransformer(),
- 'H' => new Hour2400Transformer(),
- 'K' => new Hour1200Transformer(),
- 'k' => new Hour2401Transformer(),
- 'm' => new MinuteTransformer(),
- 's' => new SecondTransformer(),
- 'z' => new TimezoneTransformer(),
- ];
- }
- /**
- * Format a DateTime using ICU dateformat pattern.
- *
- * @return string The formatted value
- */
- public function format(\DateTime $dateTime): string
- {
- $formatted = preg_replace_callback($this->regExp, function ($matches) use ($dateTime) {
- return $this->formatReplace($matches[0], $dateTime);
- }, $this->pattern);
- return $formatted;
- }
- /**
- * Return the formatted ICU value for the matched date characters.
- *
- * @throws NotImplementedException When it encounters a not implemented date character
- */
- private function formatReplace(string $dateChars, \DateTime $dateTime): string
- {
- $length = \strlen($dateChars);
- if ($this->isQuoteMatch($dateChars)) {
- return $this->replaceQuoteMatch($dateChars);
- }
- if (isset($this->transformers[$dateChars[0]])) {
- $transformer = $this->transformers[$dateChars[0]];
- return $transformer->format($dateTime, $length);
- }
- // handle unimplemented characters
- if (false !== strpos($this->notImplementedChars, $dateChars[0])) {
- throw new NotImplementedException(sprintf('Unimplemented date character "%s" in format "%s".', $dateChars[0], $this->pattern));
- }
- return '';
- }
- /**
- * Parse a pattern based string to a timestamp value.
- *
- * @param \DateTime $dateTime A configured DateTime object to use to perform the date calculation
- * @param string $value String to convert to a time value
- *
- * @return int|false The corresponding Unix timestamp
- *
- * @throws \InvalidArgumentException When the value can not be matched with pattern
- */
- public function parse(\DateTime $dateTime, string $value)
- {
- $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern);
- $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/';
- $options = [];
- if (preg_match($reverseMatchingRegExp, $value, $matches)) {
- $matches = $this->normalizeArray($matches);
- foreach ($this->transformers as $char => $transformer) {
- if (isset($matches[$char])) {
- $length = \strlen($matches[$char]['pattern']);
- $options = array_merge($options, $transformer->extractDateOptions($matches[$char]['value'], $length));
- }
- }
- // reset error code and message
- Icu::setError(Icu::U_ZERO_ERROR);
- return $this->calculateUnixTimestamp($dateTime, $options);
- }
- // behave like the intl extension
- Icu::setError(Icu::U_PARSE_ERROR, 'Date parsing failed');
- return false;
- }
- /**
- * Retrieve a regular expression to match with a formatted value.
- *
- * @return string The reverse matching regular expression with named captures being formed by the
- * transformer index in the $transformer array
- */
- private function getReverseMatchingRegExp(string $pattern): string
- {
- $escapedPattern = preg_quote($pattern, '/');
- // ICU 4.8 recognizes slash ("/") in a value to be parsed as a dash ("-") and vice-versa
- // when parsing a date/time value
- $escapedPattern = preg_replace('/\\\[\-|\/]/', '[\/\-]', $escapedPattern);
- $reverseMatchingRegExp = preg_replace_callback($this->regExp, function ($matches) {
- $length = \strlen($matches[0]);
- $transformerIndex = $matches[0][0];
- $dateChars = $matches[0];
- if ($this->isQuoteMatch($dateChars)) {
- return $this->replaceQuoteMatch($dateChars);
- }
- if (isset($this->transformers[$transformerIndex])) {
- $transformer = $this->transformers[$transformerIndex];
- $captureName = str_repeat($transformerIndex, $length);
- return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')';
- }
- return null;
- }, $escapedPattern);
- return $reverseMatchingRegExp;
- }
- /**
- * Check if the first char of a string is a single quote.
- */
- private function isQuoteMatch(string $quoteMatch): bool
- {
- return "'" === $quoteMatch[0];
- }
- /**
- * Replaces single quotes at the start or end of a string with two single quotes.
- */
- private function replaceQuoteMatch(string $quoteMatch): string
- {
- if (preg_match("/^'+$/", $quoteMatch)) {
- return str_replace("''", "'", $quoteMatch);
- }
- return str_replace("''", "'", substr($quoteMatch, 1, -1));
- }
- /**
- * Builds a chars match regular expression.
- */
- private function buildCharsMatch(string $specialChars): string
- {
- $specialCharsArray = str_split($specialChars);
- $specialCharsMatch = implode('|', array_map(function ($char) {
- return $char.'+';
- }, $specialCharsArray));
- return $specialCharsMatch;
- }
- /**
- * Normalize a preg_replace match array, removing the numeric keys and returning an associative array
- * with the value and pattern values for the matched Transformer.
- */
- private function normalizeArray(array $data): array
- {
- $ret = [];
- foreach ($data as $key => $value) {
- if (!\is_string($key)) {
- continue;
- }
- $ret[$key[0]] = [
- 'value' => $value,
- 'pattern' => $key,
- ];
- }
- return $ret;
- }
- /**
- * Calculates the Unix timestamp based on the matched values by the reverse matching regular
- * expression of parse().
- *
- * @return bool|int The calculated timestamp or false if matched date is invalid
- */
- private function calculateUnixTimestamp(\DateTime $dateTime, array $options)
- {
- $options = $this->getDefaultValueForOptions($options);
- $year = $options['year'];
- $month = $options['month'];
- $day = $options['day'];
- $hour = $options['hour'];
- $hourInstance = $options['hourInstance'];
- $minute = $options['minute'];
- $second = $options['second'];
- $marker = $options['marker'];
- $timezone = $options['timezone'];
- // If month is false, return immediately (intl behavior)
- if (false === $month) {
- Icu::setError(Icu::U_PARSE_ERROR, 'Date parsing failed');
- return false;
- }
- // Normalize hour
- if ($hourInstance instanceof HourTransformer) {
- $hour = $hourInstance->normalizeHour($hour, $marker);
- }
- // Set the timezone if different from the default one
- if (null !== $timezone && $timezone !== $this->timezone) {
- $dateTime->setTimezone(new \DateTimeZone($timezone));
- }
- // Normalize yy year
- preg_match_all($this->regExp, $this->pattern, $matches);
- if (\in_array('yy', $matches[0])) {
- $dateTime->setTimestamp(time());
- $year = $year > (int) $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
- }
- $dateTime->setDate($year, $month, $day);
- $dateTime->setTime($hour, $minute, $second);
- return $dateTime->getTimestamp();
- }
- /**
- * Add sensible default values for missing items in the extracted date/time options array. The values
- * are base in the beginning of the Unix era.
- */
- private function getDefaultValueForOptions(array $options): array
- {
- return [
- 'year' => $options['year'] ?? 1970,
- 'month' => $options['month'] ?? 1,
- 'day' => $options['day'] ?? 1,
- 'hour' => $options['hour'] ?? 0,
- 'hourInstance' => $options['hourInstance'] ?? null,
- 'minute' => $options['minute'] ?? 0,
- 'second' => $options['second'] ?? 0,
- 'marker' => $options['marker'] ?? null,
- 'timezone' => $options['timezone'] ?? null,
- ];
- }
- }
|