IntlDateFormatter.php 20 KB

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