1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Intl\DateFormatter;
13
14use Symfony\Component\Intl\Globals\IntlGlobals;
15use Symfony\Component\Intl\DateFormatter\DateFormat\FullTransformer;
16use Symfony\Component\Intl\Exception\MethodNotImplementedException;
17use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException;
18use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException;
19use Symfony\Component\Intl\Locale\Locale;
20
21/**
22 * Replacement for PHP's native {@link \IntlDateFormatter} class.
23 *
24 * The only methods currently supported in this class are:
25 *
26 *  - {@link __construct}
27 *  - {@link create}
28 *  - {@link format}
29 *  - {@link getCalendar}
30 *  - {@link getDateType}
31 *  - {@link getErrorCode}
32 *  - {@link getErrorMessage}
33 *  - {@link getLocale}
34 *  - {@link getPattern}
35 *  - {@link getTimeType}
36 *  - {@link getTimeZoneId}
37 *  - {@link isLenient}
38 *  - {@link parse}
39 *  - {@link setLenient}
40 *  - {@link setPattern}
41 *  - {@link setTimeZoneId}
42 *  - {@link setTimeZone}
43 *
44 * @author Igor Wiedler <igor@wiedler.ch>
45 * @author Bernhard Schussek <bschussek@gmail.com>
46 */
47class IntlDateFormatter
48{
49    /**
50     * The error code from the last operation.
51     *
52     * @var int
53     */
54    protected $errorCode = IntlGlobals::U_ZERO_ERROR;
55
56    /**
57     * The error message from the last operation.
58     *
59     * @var string
60     */
61    protected $errorMessage = 'U_ZERO_ERROR';
62
63    /* date/time format types */
64    const NONE = -1;
65    const FULL = 0;
66    const LONG = 1;
67    const MEDIUM = 2;
68    const SHORT = 3;
69
70    /* calendar formats */
71    const TRADITIONAL = 0;
72    const GREGORIAN = 1;
73
74    /**
75     * Patterns used to format the date when no pattern is provided.
76     *
77     * @var array
78     */
79    private $defaultDateFormats = array(
80        self::NONE => '',
81        self::FULL => 'EEEE, LLLL d, y',
82        self::LONG => 'LLLL d, y',
83        self::MEDIUM => 'LLL d, y',
84        self::SHORT => 'M/d/yy',
85    );
86
87    /**
88     * Patterns used to format the time when no pattern is provided.
89     *
90     * @var array
91     */
92    private $defaultTimeFormats = array(
93        self::FULL => 'h:mm:ss a zzzz',
94        self::LONG => 'h:mm:ss a z',
95        self::MEDIUM => 'h:mm:ss a',
96        self::SHORT => 'h:mm a',
97    );
98
99    /**
100     * @var int
101     */
102    private $datetype;
103
104    /**
105     * @var int
106     */
107    private $timetype;
108
109    /**
110     * @var string
111     */
112    private $pattern;
113
114    /**
115     * @var \DateTimeZone
116     */
117    private $dateTimeZone;
118
119    /**
120     * @var bool
121     */
122    private $uninitializedTimeZoneId = false;
123
124    /**
125     * @var string
126     */
127    private $timeZoneId;
128
129    /**
130     * Constructor.
131     *
132     * @param string $locale   The locale code. The only currently supported locale is "en".
133     * @param int    $datetype Type of date formatting, one of the format type constants
134     * @param int    $timetype Type of time formatting, one of the format type constants
135     * @param string $timezone Timezone identifier
136     * @param int    $calendar Calendar to use for formatting or parsing. The only currently
137     *                         supported value is IntlDateFormatter::GREGORIAN.
138     * @param string $pattern  Optional pattern to use when formatting
139     *
140     * @see http://www.php.net/manual/en/intldateformatter.create.php
141     * @see http://userguide.icu-project.org/formatparse/datetime
142     *
143     * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
144     * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed
145     */
146    public function __construct($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null)
147    {
148        if ('en' !== $locale) {
149            throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported');
150        }
151
152        if (self::GREGORIAN !== $calendar) {
153            throw new MethodArgumentValueNotImplementedException(__METHOD__, 'calendar', $calendar, 'Only the GREGORIAN calendar is supported');
154        }
155
156        $this->datetype = $datetype;
157        $this->timetype = $timetype;
158
159        $this->setPattern($pattern);
160        $this->setTimeZoneId($timezone);
161    }
162
163    /**
164     * Static constructor.
165     *
166     * @param string $locale   The locale code. The only currently supported locale is "en".
167     * @param int    $datetype Type of date formatting, one of the format type constants
168     * @param int    $timetype Type of time formatting, one of the format type constants
169     * @param string $timezone Timezone identifier
170     * @param int    $calendar Calendar to use for formatting or parsing; default is Gregorian.
171     *                         One of the calendar constants.
172     * @param string $pattern  Optional pattern to use when formatting
173     *
174     * @return IntlDateFormatter
175     *
176     * @see http://www.php.net/manual/en/intldateformatter.create.php
177     * @see http://userguide.icu-project.org/formatparse/datetime
178     *
179     * @throws MethodArgumentValueNotImplementedException When $locale different than "en" is passed
180     * @throws MethodArgumentValueNotImplementedException When $calendar different than GREGORIAN is passed
181     */
182    public static function create($locale, $datetype, $timetype, $timezone = null, $calendar = self::GREGORIAN, $pattern = null)
183    {
184        return new self($locale, $datetype, $timetype, $timezone, $calendar, $pattern);
185    }
186
187    /**
188     * Format the date/time value (timestamp) as a string.
189     *
190     * @param int|\DateTime $timestamp The timestamp to format. \DateTime objects
191     *                                 are supported as of PHP 5.3.4.
192     *
193     * @return string|bool The formatted value or false if formatting failed.
194     *
195     * @see http://www.php.net/manual/en/intldateformatter.format.php
196     *
197     * @throws MethodArgumentValueNotImplementedException If one of the formatting characters is not implemented
198     */
199    public function format($timestamp)
200    {
201        // intl allows timestamps to be passed as arrays - we don't
202        if (is_array($timestamp)) {
203            $message = PHP_VERSION_ID >= 50304 ?
204                'Only integer Unix timestamps and DateTime objects are supported' :
205                'Only integer Unix timestamps are supported';
206
207            throw new MethodArgumentValueNotImplementedException(__METHOD__, 'timestamp', $timestamp, $message);
208        }
209
210        // behave like the intl extension
211        $argumentError = null;
212        if (PHP_VERSION_ID < 50304 && !is_int($timestamp)) {
213            $argumentError = 'datefmt_format: takes either an array  or an integer timestamp value ';
214        } elseif (PHP_VERSION_ID >= 50304 && !is_int($timestamp) && !$timestamp instanceof \DateTime) {
215            $argumentError = 'datefmt_format: takes either an array or an integer timestamp value or a DateTime object';
216            if (PHP_VERSION_ID >= 50500 && !is_int($timestamp)) {
217                $argumentError = sprintf('datefmt_format: string \'%s\' is not numeric, which would be required for it to be a valid date', $timestamp);
218            }
219        }
220
221        if (null !== $argumentError) {
222            IntlGlobals::setError(IntlGlobals::U_ILLEGAL_ARGUMENT_ERROR, $argumentError);
223            $this->errorCode = IntlGlobals::getErrorCode();
224            $this->errorMessage = IntlGlobals::getErrorMessage();
225
226            return false;
227        }
228
229        // As of PHP 5.3.4, IntlDateFormatter::format() accepts DateTime instances
230        if (PHP_VERSION_ID >= 50304 && $timestamp instanceof \DateTime) {
231            $timestamp = $timestamp->getTimestamp();
232        }
233
234        $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId());
235        $formatted = $transformer->format($this->createDateTime($timestamp));
236
237        // behave like the intl extension
238        IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR);
239        $this->errorCode = IntlGlobals::getErrorCode();
240        $this->errorMessage = IntlGlobals::getErrorMessage();
241
242        return $formatted;
243    }
244
245    /**
246     * Not supported. Formats an object.
247     *
248     * @param object $object
249     * @param mixed  $format
250     * @param string $locale
251     *
252     * @return string The formatted value
253     *
254     * @see http://www.php.net/manual/en/intldateformatter.formatobject.php
255     *
256     * @throws MethodNotImplementedException
257     */
258    public function formatObject($object, $format = null, $locale = null)
259    {
260        throw new MethodNotImplementedException(__METHOD__);
261    }
262
263    /**
264     * Returns the formatter's calendar.
265     *
266     * @return int The calendar being used by the formatter. Currently always returns
267     *             IntlDateFormatter::GREGORIAN.
268     *
269     * @see http://www.php.net/manual/en/intldateformatter.getcalendar.php
270     */
271    public function getCalendar()
272    {
273        return self::GREGORIAN;
274    }
275
276    /**
277     * Not supported. Returns the formatter's calendar object.
278     *
279     * @return object The calendar's object being used by the formatter
280     *
281     * @see http://www.php.net/manual/en/intldateformatter.getcalendarobject.php
282     *
283     * @throws MethodNotImplementedException
284     */
285    public function getCalendarObject()
286    {
287        throw new MethodNotImplementedException(__METHOD__);
288    }
289
290    /**
291     * Returns the formatter's datetype.
292     *
293     * @return int The current value of the formatter
294     *
295     * @see http://www.php.net/manual/en/intldateformatter.getdatetype.php
296     */
297    public function getDateType()
298    {
299        return $this->datetype;
300    }
301
302    /**
303     * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value.
304     *
305     * @return int The error code from last formatter call
306     *
307     * @see http://www.php.net/manual/en/intldateformatter.geterrorcode.php
308     */
309    public function getErrorCode()
310    {
311        return $this->errorCode;
312    }
313
314    /**
315     * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value.
316     *
317     * @return string The error message from last formatter call
318     *
319     * @see http://www.php.net/manual/en/intldateformatter.geterrormessage.php
320     */
321    public function getErrorMessage()
322    {
323        return $this->errorMessage;
324    }
325
326    /**
327     * Returns the formatter's locale.
328     *
329     * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE)
330     *
331     * @return string The locale used to create the formatter. Currently always
332     *                returns "en".
333     *
334     * @see http://www.php.net/manual/en/intldateformatter.getlocale.php
335     */
336    public function getLocale($type = Locale::ACTUAL_LOCALE)
337    {
338        return 'en';
339    }
340
341    /**
342     * Returns the formatter's pattern.
343     *
344     * @return string The pattern string used by the formatter
345     *
346     * @see http://www.php.net/manual/en/intldateformatter.getpattern.php
347     */
348    public function getPattern()
349    {
350        return $this->pattern;
351    }
352
353    /**
354     * Returns the formatter's time type.
355     *
356     * @return string The time type used by the formatter
357     *
358     * @see http://www.php.net/manual/en/intldateformatter.gettimetype.php
359     */
360    public function getTimeType()
361    {
362        return $this->timetype;
363    }
364
365    /**
366     * Returns the formatter's timezone identifier.
367     *
368     * @return string The timezone identifier used by the formatter
369     *
370     * @see http://www.php.net/manual/en/intldateformatter.gettimezoneid.php
371     */
372    public function getTimeZoneId()
373    {
374        if (!$this->uninitializedTimeZoneId) {
375            return $this->timeZoneId;
376        }
377
378        // In PHP 5.5 default timezone depends on `date_default_timezone_get()` method
379        if (PHP_VERSION_ID >= 50500) {
380            return date_default_timezone_get();
381        }
382    }
383
384    /**
385     * Not supported. Returns the formatter's timezone.
386     *
387     * @return mixed The timezone used by the formatter
388     *
389     * @see http://www.php.net/manual/en/intldateformatter.gettimezone.php
390     *
391     * @throws MethodNotImplementedException
392     */
393    public function getTimeZone()
394    {
395        throw new MethodNotImplementedException(__METHOD__);
396    }
397
398    /**
399     * Returns whether the formatter is lenient.
400     *
401     * @return bool Currently always returns false.
402     *
403     * @see http://www.php.net/manual/en/intldateformatter.islenient.php
404     *
405     * @throws MethodNotImplementedException
406     */
407    public function isLenient()
408    {
409        return false;
410    }
411
412    /**
413     * Not supported. Parse string to a field-based time value.
414     *
415     * @param string $value    String to convert to a time value
416     * @param int    $position Position at which to start the parsing in $value (zero-based).
417     *                         If no error occurs before $value is consumed, $parse_pos will
418     *                         contain -1 otherwise it will contain the position at which parsing
419     *                         ended. If $parse_pos > strlen($value), the parse fails immediately.
420     *
421     * @return string Localtime compatible array of integers: contains 24 hour clock value in tm_hour field
422     *
423     * @see http://www.php.net/manual/en/intldateformatter.localtime.php
424     *
425     * @throws MethodNotImplementedException
426     */
427    public function localtime($value, &$position = 0)
428    {
429        throw new MethodNotImplementedException(__METHOD__);
430    }
431
432    /**
433     * Parse string to a timestamp value.
434     *
435     * @param string $value    String to convert to a time value
436     * @param int    $position Not supported. Position at which to start the parsing in $value (zero-based).
437     *                         If no error occurs before $value is consumed, $parse_pos will
438     *                         contain -1 otherwise it will contain the position at which parsing
439     *                         ended. If $parse_pos > strlen($value), the parse fails immediately.
440     *
441     * @return string Parsed value as a timestamp
442     *
443     * @see http://www.php.net/manual/en/intldateformatter.parse.php
444     *
445     * @throws MethodArgumentNotImplementedException When $position different than null, behavior not implemented
446     */
447    public function parse($value, &$position = null)
448    {
449        // We don't calculate the position when parsing the value
450        if (null !== $position) {
451            throw new MethodArgumentNotImplementedException(__METHOD__, 'position');
452        }
453
454        $dateTime = $this->createDateTime(0);
455        $transformer = new FullTransformer($this->getPattern(), $this->getTimeZoneId());
456
457        $timestamp = $transformer->parse($dateTime, $value);
458
459        // behave like the intl extension. FullTransformer::parse() set the proper error
460        $this->errorCode = IntlGlobals::getErrorCode();
461        $this->errorMessage = IntlGlobals::getErrorMessage();
462
463        return $timestamp;
464    }
465
466    /**
467     * Not supported. Set the formatter's calendar.
468     *
469     * @param string $calendar The calendar to use. Default is IntlDateFormatter::GREGORIAN.
470     *
471     * @return bool true on success or false on failure
472     *
473     * @see http://www.php.net/manual/en/intldateformatter.setcalendar.php
474     *
475     * @throws MethodNotImplementedException
476     */
477    public function setCalendar($calendar)
478    {
479        throw new MethodNotImplementedException(__METHOD__);
480    }
481
482    /**
483     * Set the leniency of the parser.
484     *
485     * Define if the parser is strict or lenient in interpreting inputs that do not match the pattern
486     * exactly. Enabling lenient parsing allows the parser to accept otherwise flawed date or time
487     * patterns, parsing as much as possible to obtain a value. Extra space, unrecognized tokens, or
488     * invalid values ("February 30th") are not accepted.
489     *
490     * @param bool $lenient Sets whether the parser is lenient or not. Currently
491     *                      only false (strict) is supported.
492     *
493     * @return bool true on success or false on failure
494     *
495     * @see http://www.php.net/manual/en/intldateformatter.setlenient.php
496     *
497     * @throws MethodArgumentValueNotImplementedException When $lenient is true
498     */
499    public function setLenient($lenient)
500    {
501        if ($lenient) {
502            throw new MethodArgumentValueNotImplementedException(__METHOD__, 'lenient', $lenient, 'Only the strict parser is supported');
503        }
504
505        return true;
506    }
507
508    /**
509     * Set the formatter's pattern.
510     *
511     * @param string $pattern A pattern string in conformance with the ICU IntlDateFormatter documentation
512     *
513     * @return bool true on success or false on failure
514     *
515     * @see http://www.php.net/manual/en/intldateformatter.setpattern.php
516     * @see http://userguide.icu-project.org/formatparse/datetime
517     */
518    public function setPattern($pattern)
519    {
520        if (null === $pattern) {
521            $pattern = $this->getDefaultPattern();
522        }
523
524        $this->pattern = $pattern;
525
526        return true;
527    }
528
529    /**
530     * Set the formatter's timezone identifier.
531     *
532     * @param string $timeZoneId The time zone ID string of the time zone to use.
533     *                           If NULL or the empty string, the default time zone for the
534     *                           runtime is used.
535     *
536     * @return bool true on success or false on failure
537     *
538     * @see http://www.php.net/manual/en/intldateformatter.settimezoneid.php
539     */
540    public function setTimeZoneId($timeZoneId)
541    {
542        if (null === $timeZoneId) {
543            // In PHP 5.5 if $timeZoneId is null it fallbacks to `date_default_timezone_get()` method
544            if (PHP_VERSION_ID >= 50500) {
545                $timeZoneId = date_default_timezone_get();
546            } else {
547                // TODO: changes were made to ext/intl in PHP 5.4.4 release that need to be investigated since it will
548                // use ini's date.timezone when the time zone is not provided. As a not well tested workaround, uses UTC.
549                // See the first two items of the commit message for more information:
550                // https://github.com/php/php-src/commit/eb346ef0f419b90739aadfb6cc7b7436c5b521d9
551                $timeZoneId = getenv('TZ') ?: 'UTC';
552            }
553
554            $this->uninitializedTimeZoneId = true;
555        }
556
557        // Backup original passed time zone
558        $timeZone = $timeZoneId;
559
560        // Get an Etc/GMT time zone that is accepted for \DateTimeZone
561        if ('GMT' !== $timeZoneId && 0 === strpos($timeZoneId, 'GMT')) {
562            try {
563                $timeZoneId = DateFormat\TimeZoneTransformer::getEtcTimeZoneId($timeZoneId);
564            } catch (\InvalidArgumentException $e) {
565                // Does nothing, will fallback to UTC
566            }
567        }
568
569        try {
570            $this->dateTimeZone = new \DateTimeZone($timeZoneId);
571        } catch (\Exception $e) {
572            $this->dateTimeZone = new \DateTimeZone('UTC');
573        }
574
575        $this->timeZoneId = $timeZone;
576
577        return true;
578    }
579
580    /**
581     * This method was added in PHP 5.5 as replacement for `setTimeZoneId()`.
582     *
583     * @param mixed $timeZone
584     *
585     * @return bool true on success or false on failure
586     *
587     * @see http://www.php.net/manual/en/intldateformatter.settimezone.php
588     */
589    public function setTimeZone($timeZone)
590    {
591        return $this->setTimeZoneId($timeZone);
592    }
593
594    /**
595     * Create and returns a DateTime object with the specified timestamp and with the
596     * current time zone.
597     *
598     * @param int $timestamp
599     *
600     * @return \DateTime
601     */
602    protected function createDateTime($timestamp)
603    {
604        $dateTime = new \DateTime();
605        $dateTime->setTimestamp($timestamp);
606        $dateTime->setTimezone($this->dateTimeZone);
607
608        return $dateTime;
609    }
610
611    /**
612     * Returns a pattern string based in the datetype and timetype values.
613     *
614     * @return string
615     */
616    protected function getDefaultPattern()
617    {
618        $patternParts = array();
619        if (self::NONE !== $this->datetype) {
620            $patternParts[] = $this->defaultDateFormats[$this->datetype];
621        }
622        if (self::NONE !== $this->timetype) {
623            $patternParts[] = $this->defaultTimeFormats[$this->timetype];
624        }
625        $pattern = implode(' ', $patternParts);
626
627        return $pattern;
628    }
629}
630