vendor/easycorp/easyadmin-bundle/src/Intl/IntlFormatter.php line 120

Open in your IDE?
  1. <?php
  2. namespace EasyCorp\Bundle\EasyAdminBundle\Intl;
  3. use Twig\Error\RuntimeError;
  4. /**
  5. * Copied from https://github.com/twigphp/intl-extra/blob/2.x/src/IntlExtension.php
  6. * (c) Fabien Potencier - MIT License.
  7. *
  8. * @author Fabien Potencier <fabien@symfony.com>
  9. */
  10. final class IntlFormatter
  11. {
  12. private const DATE_FORMATS = [
  13. 'none' => \IntlDateFormatter::NONE,
  14. 'short' => \IntlDateFormatter::SHORT,
  15. 'medium' => \IntlDateFormatter::MEDIUM,
  16. 'long' => \IntlDateFormatter::LONG,
  17. 'full' => \IntlDateFormatter::FULL,
  18. ];
  19. private const NUMBER_TYPES = [
  20. 'default' => \NumberFormatter::TYPE_DEFAULT,
  21. 'int32' => \NumberFormatter::TYPE_INT32,
  22. 'int64' => \NumberFormatter::TYPE_INT64,
  23. 'double' => \NumberFormatter::TYPE_DOUBLE,
  24. 'currency' => \NumberFormatter::TYPE_CURRENCY,
  25. ];
  26. private const NUMBER_STYLES = [
  27. 'decimal' => \NumberFormatter::DECIMAL,
  28. 'currency' => \NumberFormatter::CURRENCY,
  29. 'percent' => \NumberFormatter::PERCENT,
  30. 'scientific' => \NumberFormatter::SCIENTIFIC,
  31. 'spellout' => \NumberFormatter::SPELLOUT,
  32. 'ordinal' => \NumberFormatter::ORDINAL,
  33. 'duration' => \NumberFormatter::DURATION,
  34. ];
  35. private const NUMBER_ATTRIBUTES = [
  36. 'grouping_used' => \NumberFormatter::GROUPING_USED,
  37. 'decimal_always_shown' => \NumberFormatter::DECIMAL_ALWAYS_SHOWN,
  38. 'max_integer_digit' => \NumberFormatter::MAX_INTEGER_DIGITS,
  39. 'min_integer_digit' => \NumberFormatter::MIN_INTEGER_DIGITS,
  40. 'integer_digit' => \NumberFormatter::INTEGER_DIGITS,
  41. 'max_fraction_digit' => \NumberFormatter::MAX_FRACTION_DIGITS,
  42. 'min_fraction_digit' => \NumberFormatter::MIN_FRACTION_DIGITS,
  43. 'fraction_digit' => \NumberFormatter::FRACTION_DIGITS,
  44. 'multiplier' => \NumberFormatter::MULTIPLIER,
  45. 'grouping_size' => \NumberFormatter::GROUPING_SIZE,
  46. 'rounding_mode' => \NumberFormatter::ROUNDING_MODE,
  47. 'rounding_increment' => \NumberFormatter::ROUNDING_INCREMENT,
  48. 'format_width' => \NumberFormatter::FORMAT_WIDTH,
  49. 'padding_position' => \NumberFormatter::PADDING_POSITION,
  50. 'secondary_grouping_size' => \NumberFormatter::SECONDARY_GROUPING_SIZE,
  51. 'significant_digits_used' => \NumberFormatter::SIGNIFICANT_DIGITS_USED,
  52. 'min_significant_digits_used' => \NumberFormatter::MIN_SIGNIFICANT_DIGITS,
  53. 'max_significant_digits_used' => \NumberFormatter::MAX_SIGNIFICANT_DIGITS,
  54. 'lenient_parse' => \NumberFormatter::LENIENT_PARSE,
  55. ];
  56. private const NUMBER_ROUNDING_ATTRIBUTES = [
  57. 'ceiling' => \NumberFormatter::ROUND_CEILING,
  58. 'floor' => \NumberFormatter::ROUND_FLOOR,
  59. 'down' => \NumberFormatter::ROUND_DOWN,
  60. 'up' => \NumberFormatter::ROUND_UP,
  61. 'halfeven' => \NumberFormatter::ROUND_HALFEVEN,
  62. 'halfdown' => \NumberFormatter::ROUND_HALFDOWN,
  63. 'halfup' => \NumberFormatter::ROUND_HALFUP,
  64. ];
  65. private const NUMBER_PADDING_ATTRIBUTES = [
  66. 'before_prefix' => \NumberFormatter::PAD_BEFORE_PREFIX,
  67. 'after_prefix' => \NumberFormatter::PAD_AFTER_PREFIX,
  68. 'before_suffix' => \NumberFormatter::PAD_BEFORE_SUFFIX,
  69. 'after_suffix' => \NumberFormatter::PAD_AFTER_SUFFIX,
  70. ];
  71. private const NUMBER_TEXT_ATTRIBUTES = [
  72. 'positive_prefix' => \NumberFormatter::POSITIVE_PREFIX,
  73. 'positive_suffix' => \NumberFormatter::POSITIVE_SUFFIX,
  74. 'negative_prefix' => \NumberFormatter::NEGATIVE_PREFIX,
  75. 'negative_suffix' => \NumberFormatter::NEGATIVE_SUFFIX,
  76. 'padding_character' => \NumberFormatter::PADDING_CHARACTER,
  77. 'currency_mode' => \NumberFormatter::CURRENCY_CODE,
  78. 'default_ruleset' => \NumberFormatter::DEFAULT_RULESET,
  79. 'public_rulesets' => \NumberFormatter::PUBLIC_RULESETS,
  80. ];
  81. private const NUMBER_SYMBOLS = [
  82. 'decimal_separator' => \NumberFormatter::DECIMAL_SEPARATOR_SYMBOL,
  83. 'grouping_separator' => \NumberFormatter::GROUPING_SEPARATOR_SYMBOL,
  84. 'pattern_separator' => \NumberFormatter::PATTERN_SEPARATOR_SYMBOL,
  85. 'percent' => \NumberFormatter::PERCENT_SYMBOL,
  86. 'zero_digit' => \NumberFormatter::ZERO_DIGIT_SYMBOL,
  87. 'digit' => \NumberFormatter::DIGIT_SYMBOL,
  88. 'minus_sign' => \NumberFormatter::MINUS_SIGN_SYMBOL,
  89. 'plus_sign' => \NumberFormatter::PLUS_SIGN_SYMBOL,
  90. 'currency' => \NumberFormatter::CURRENCY_SYMBOL,
  91. 'intl_currency' => \NumberFormatter::INTL_CURRENCY_SYMBOL,
  92. 'monetary_separator' => \NumberFormatter::MONETARY_SEPARATOR_SYMBOL,
  93. 'exponential' => \NumberFormatter::EXPONENTIAL_SYMBOL,
  94. 'permill' => \NumberFormatter::PERMILL_SYMBOL,
  95. 'pad_escape' => \NumberFormatter::PAD_ESCAPE_SYMBOL,
  96. 'infinity' => \NumberFormatter::INFINITY_SYMBOL,
  97. 'nan' => \NumberFormatter::NAN_SYMBOL,
  98. 'significant_digit' => \NumberFormatter::SIGNIFICANT_DIGIT_SYMBOL,
  99. 'monetary_grouping_separator' => \NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL,
  100. ];
  101. private $dateFormatters = [];
  102. private $numberFormatters = [];
  103. private $numberFormatterPrototype;
  104. public function formatCurrency($amount, string $currency, array $attrs = [], string $locale = null): string
  105. {
  106. $formatter = $this->createNumberFormatter($locale, 'currency', $attrs);
  107. if (false === $formattedCurrency = $formatter->formatCurrency($amount, $currency)) {
  108. throw new RuntimeError('Unable to format the given number as a currency.');
  109. }
  110. return $formattedCurrency;
  111. }
  112. public function formatNumber($number, array $attrs = [], string $style = 'decimal', string $type = 'default', string $locale = null): string
  113. {
  114. if (!isset(self::NUMBER_TYPES[$type])) {
  115. throw new RuntimeError(sprintf('The type "%s" does not exist, known types are: "%s".', $type, implode('", "', array_keys(self::NUMBER_TYPES))));
  116. }
  117. $formatter = $this->createNumberFormatter($locale, $style, $attrs);
  118. if (false === $ret = $formatter->format($number, self::NUMBER_TYPES[$type])) {
  119. throw new RuntimeError('Unable to format the given number.');
  120. }
  121. return $ret;
  122. }
  123. /**
  124. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  125. */
  126. public function formatDateTime(?\DateTimeInterface $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): ?string
  127. {
  128. if (null === $date = $this->convertDate($date, $timezone)) {
  129. return null;
  130. }
  131. $formatter = $this->createDateFormatter($locale, $dateFormat, $timeFormat, $pattern, $date->getTimezone(), $calendar);
  132. $formattedDateTime = $formatter->format($date);
  133. return false !== $formattedDateTime ? $formattedDateTime : null;
  134. }
  135. /**
  136. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  137. */
  138. public function formatDate(?\DateTimeInterface $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): ?string
  139. {
  140. return $this->formatDateTime($date, $dateFormat, 'none', $pattern, $timezone, $calendar, $locale);
  141. }
  142. /**
  143. * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged
  144. */
  145. public function formatTime(?\DateTimeInterface $date, ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', string $locale = null): ?string
  146. {
  147. return $this->formatDateTime($date, 'none', $timeFormat, $pattern, $timezone, $calendar, $locale);
  148. }
  149. private function createDateFormatter(?string $locale, ?string $dateFormat, ?string $timeFormat, string $pattern = '', \DateTimeZone $timezone = null, string $calendar = 'gregorian'): \IntlDateFormatter
  150. {
  151. if (null !== $dateFormat && !isset(self::DATE_FORMATS[$dateFormat])) {
  152. throw new RuntimeError(sprintf('The date format "%s" does not exist, known formats are: "%s".', $dateFormat, implode('", "', array_keys(self::DATE_FORMATS))));
  153. }
  154. if (null !== $timeFormat && !isset(self::DATE_FORMATS[$timeFormat])) {
  155. throw new RuntimeError(sprintf('The time format "%s" does not exist, known formats are: "%s".', $timeFormat, implode('", "', array_keys(self::DATE_FORMATS))));
  156. }
  157. if (null === $locale) {
  158. $locale = \Locale::getDefault();
  159. }
  160. $calendar = 'gregorian' === $calendar ? \IntlDateFormatter::GREGORIAN : \IntlDateFormatter::TRADITIONAL;
  161. $dateFormatValue = self::DATE_FORMATS[$dateFormat] ?? self::DATE_FORMATS['full'];
  162. $timeFormatValue = self::DATE_FORMATS[$timeFormat] ?? self::DATE_FORMATS['full'];
  163. $hash = $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezone->getName().'|'.$calendar.'|'.$pattern;
  164. if (!isset($this->dateFormatters[$hash])) {
  165. $this->dateFormatters[$hash] = new \IntlDateFormatter($locale, $dateFormatValue, $timeFormatValue, $timezone, $calendar, $pattern);
  166. }
  167. return $this->dateFormatters[$hash];
  168. }
  169. private function createNumberFormatter(?string $locale, string $style, array $attrs = []): \NumberFormatter
  170. {
  171. if (!isset(self::NUMBER_STYLES[$style])) {
  172. throw new RuntimeError(sprintf('The style "%s" does not exist, known styles are: "%s".', $style, implode('", "', array_keys(self::NUMBER_STYLES))));
  173. }
  174. if (null === $locale) {
  175. $locale = \Locale::getDefault();
  176. }
  177. // textAttrs and symbols can only be set on the prototype as there is probably no
  178. // use case for setting it on each call.
  179. $textAttrs = [];
  180. $symbols = [];
  181. if ($this->numberFormatterPrototype) {
  182. foreach (self::NUMBER_ATTRIBUTES as $name => $const) {
  183. if (!isset($attrs[$name])) {
  184. $value = $this->numberFormatterPrototype->getAttribute($const);
  185. if ('rounding_mode' === $name) {
  186. $value = array_flip(self::NUMBER_ROUNDING_ATTRIBUTES)[$value];
  187. } elseif ('padding_position' === $name) {
  188. $value = array_flip(self::NUMBER_PADDING_ATTRIBUTES)[$value];
  189. }
  190. $attrs[$name] = $value;
  191. }
  192. }
  193. foreach (self::NUMBER_TEXT_ATTRIBUTES as $name => $const) {
  194. $textAttrs[$name] = $this->numberFormatterPrototype->getTextAttribute($const);
  195. }
  196. foreach (self::NUMBER_SYMBOLS as $name => $const) {
  197. $symbols[$name] = $this->numberFormatterPrototype->getSymbol($const);
  198. }
  199. }
  200. ksort($attrs);
  201. $hash = $locale.'|'.$style.'|'.json_encode($attrs).'|'.json_encode($textAttrs).'|'.json_encode($symbols);
  202. if (!isset($this->numberFormatters[$hash])) {
  203. $this->numberFormatters[$hash] = new \NumberFormatter($locale, self::NUMBER_STYLES[$style]);
  204. }
  205. foreach ($attrs as $name => $value) {
  206. if (!isset(self::NUMBER_ATTRIBUTES[$name])) {
  207. throw new RuntimeError(sprintf('The number formatter attribute "%s" does not exist, known attributes are: "%s".', $name, implode('", "', array_keys(self::NUMBER_ATTRIBUTES))));
  208. }
  209. if ('rounding_mode' === $name) {
  210. if (!isset(self::NUMBER_ROUNDING_ATTRIBUTES[$value])) {
  211. throw new RuntimeError(sprintf('The number formatter rounding mode "%s" does not exist, known modes are: "%s".', $value, implode('", "', array_keys(self::NUMBER_ROUNDING_ATTRIBUTES))));
  212. }
  213. $value = self::NUMBER_ROUNDING_ATTRIBUTES[$value];
  214. } elseif ('padding_position' === $name) {
  215. if (!isset(self::NUMBER_PADDING_ATTRIBUTES[$value])) {
  216. throw new RuntimeError(sprintf('The number formatter padding position "%s" does not exist, known positions are: "%s".', $value, implode('", "', array_keys(self::NUMBER_PADDING_ATTRIBUTES))));
  217. }
  218. $value = self::NUMBER_PADDING_ATTRIBUTES[$value];
  219. }
  220. $this->numberFormatters[$hash]->setAttribute(self::NUMBER_ATTRIBUTES[$name], $value);
  221. }
  222. foreach ($textAttrs as $name => $value) {
  223. $this->numberFormatters[$hash]->setTextAttribute(self::NUMBER_TEXT_ATTRIBUTES[$name], $value);
  224. }
  225. foreach ($symbols as $name => $value) {
  226. $this->numberFormatters[$hash]->setSymbol(self::NUMBER_SYMBOLS[$name], $value);
  227. }
  228. return $this->numberFormatters[$hash];
  229. }
  230. private function convertDate(?\DateTimeInterface $date, $timezone = null): ?\DateTimeInterface
  231. {
  232. if (null === $date) {
  233. return null;
  234. }
  235. if (null === $timezone) {
  236. $timezone = new \DateTimeZone(date_default_timezone_get());
  237. } elseif (!$timezone instanceof \DateTimeZone) {
  238. $timezone = new \DateTimeZone($timezone);
  239. }
  240. if ($date instanceof \DateTimeImmutable) {
  241. return false !== $timezone ? $date->setTimezone($timezone) : $date;
  242. }
  243. $date = clone $date;
  244. $date->setTimezone($timezone);
  245. return $date;
  246. }
  247. }