IntlDateFormatter.php 17 KB

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