FestinHegre/vendor/symfony/polyfill-intl-icu/DateFormat/FullTransformer.php
2024-09-26 17:26:04 +02:00

313 lines
10 KiB
PHP

<?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,
];
}
}