IntlDateFormatter.php 20 KB

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