aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/symfony/intl/Symfony/Component/Intl/DateFormatter/DateFormat/FullTransformer.php
blob: b89db3630e03d2322be59eedf44954ec994aed9e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
<?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\Component\Intl\DateFormatter\DateFormat;

use Symfony\Component\Intl\Exception\NotImplementedException;
use Symfony\Component\Intl\Globals\IntlGlobals;
use Symfony\Component\Intl\DateFormatter\DateFormat\MonthTransformer;

/**
 * Parser and formatter for date formats
 *
 * @author Igor Wiedler <igor@wiedler.ch>
 */
class FullTransformer
{
    private $quoteMatch = "'(?:[^']+|'')*'";
    private $implementedChars = 'MLydQqhDEaHkKmsz';
    private $notImplementedChars = 'GYuwWFgecSAZvVW';
    private $regExp;

    /**
     * @var Transformer[]
     */
    private $transformers;

    private $pattern;
    private $timezone;

    /**
     * Constructor
     *
     * @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($pattern, $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 = array(
            '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(),
        );
    }

    /**
     * Return the array of Transformer objects
     *
     * @return Transformer[] Associative array of Transformer objects (format char => Transformer)
     */
    public function getTransformers()
    {
        return $this->transformers;
    }

    /**
     * Format a DateTime using ICU dateformat pattern
     *
     * @param \DateTime $dateTime A DateTime object to be used to generate the formatted value
     *
     * @return string               The formatted value
     */
    public function format(\DateTime $dateTime)
    {
        $that = $this;

        $formatted = preg_replace_callback($this->regExp, function($matches) use ($that, $dateTime) {
            return $that->formatReplace($matches[0], $dateTime);
        }, $this->pattern);

        return $formatted;
    }

    /**
     * Return the formatted ICU value for the matched date characters
     *
     * @param string   $dateChars The date characters to be replaced with a formatted ICU value
     * @param DateTime $dateTime  A DateTime object to be used to generate the formatted value
     *
     * @return string                   The formatted value
     *
     * @throws NotImplementedException  When it encounters a not implemented date character
     */
    public function formatReplace($dateChars, $dateTime)
    {
        $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));
        }
    }

    /**
     * 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                       The corresponding Unix timestamp
     *
     * @throws \InvalidArgumentException  When the value can not be matched with pattern
     */
    public function parse(\DateTime $dateTime, $value)
    {
        $reverseMatchingRegExp = $this->getReverseMatchingRegExp($this->pattern);
        $reverseMatchingRegExp = '/^'.$reverseMatchingRegExp.'$/';

        $options = array();

        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
            IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR);

            return $this->calculateUnixTimestamp($dateTime, $options);
        }

        // behave like the intl extension
        IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Date parsing failed');

        return false;
    }

    /**
     * Retrieve a regular expression to match with a formatted value.
     *
     * @param string $pattern The pattern to create the reverse matching regular expression
     *
     * @return string            The reverse matching regular expression with named captures being formed by the
     *                           transformer index in the $transformer array
     */
    public function getReverseMatchingRegExp($pattern)
    {
        $that = $this;

        $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) use ($that) {
            $length = strlen($matches[0]);
            $transformerIndex = $matches[0][0];

            $dateChars = $matches[0];
            if ($that->isQuoteMatch($dateChars)) {
                return $that->replaceQuoteMatch($dateChars);
            }

            $transformers = $that->getTransformers();
            if (isset($transformers[$transformerIndex])) {
                $transformer = $transformers[$transformerIndex];
                $captureName = str_repeat($transformerIndex, $length);

                return "(?P<$captureName>".$transformer->getReverseMatchingRegExp($length).')';
            }
        }, $escapedPattern);

        return $reverseMatchingRegExp;
    }

    /**
     * Check if the first char of a string is a single quote
     *
     * @param string $quoteMatch The string to check
     *
     * @return Boolean              true if matches, false otherwise
     */
    public function isQuoteMatch($quoteMatch)
    {
        return ("'" === $quoteMatch[0]);
    }

    /**
     * Replaces single quotes at the start or end of a string with two single quotes
     *
     * @param string $quoteMatch The string to replace the quotes
     *
     * @return string               A string with the single quotes replaced
     */
    public function replaceQuoteMatch($quoteMatch)
    {
        if (preg_match("/^'+$/", $quoteMatch)) {
            return str_replace("''", "'", $quoteMatch);
        }

        return str_replace("''", "'", substr($quoteMatch, 1, -1));
    }

    /**
     * Builds a chars match regular expression
     *
     * @param string $specialChars A string of chars to build the regular expression
     *
     * @return string                 The chars match regular expression
     */
    protected function buildCharsMatch($specialChars)
    {
        $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
     *
     * @param array $data
     *
     * @return array
     */
    protected function normalizeArray(array $data)
    {
        $ret = array();

        foreach ($data as $key => $value) {
            if (!is_string($key)) {
                continue;
            }

            $ret[$key[0]] = array(
                'value' => $value,
                'pattern' => $key
            );
        }

        return $ret;
    }

    /**
     * Calculates the Unix timestamp based on the matched values by the reverse matching regular
     * expression of parse()
     *
     * @param \DateTime $dateTime The DateTime object to be used to calculate the timestamp
     * @param array     $options  An array with the matched values to be used to calculate the timestamp
     *
     * @return Boolean|int        The calculated timestamp or false if matched date is invalid
     */
    protected 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) {
            IntlGlobals::setError(IntlGlobals::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 > $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
     *
     * @param array $options
     *
     * @return array
     */
    private function getDefaultValueForOptions(array $options)
    {
        return array(
            'year'         => isset($options['year']) ? $options['year'] : 1970,
            'month'        => isset($options['month']) ? $options['month'] : 1,
            'day'          => isset($options['day']) ? $options['day'] : 1,
            'hour'         => isset($options['hour']) ? $options['hour'] : 0,
            'hourInstance' => isset($options['hourInstance']) ? $options['hourInstance'] : null,
            'minute'       => isset($options['minute']) ? $options['minute'] : 0,
            'second'       => isset($options['second']) ? $options['second'] : 0,
            'marker'       => isset($options['marker']) ? $options['marker'] : null,
            'timezone'     => isset($options['timezone']) ? $options['timezone'] : null,
        );
    }
}