/src/Templating/Helper/NumberHelper.php

https://github.com/sonata-project/SonataIntlBundle · PHP · 367 lines · 137 code · 55 blank · 175 comment · 11 complexity · f488e3abe41b2ba76909e651f34e4559 MD5 · raw file

  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of the Sonata Project package.
  5. *
  6. * (c) Thomas Rabaix <thomas.rabaix@sonata-project.org>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Sonata\IntlBundle\Templating\Helper;
  12. use Sonata\IntlBundle\Locale\LocaleDetectorInterface;
  13. /**
  14. * NumberHelper displays culture information.
  15. *
  16. * @author Thomas Rabaix <thomas.rabaix@ekino.com>
  17. * @author Stefano Arlandini <sarlandini@alice.it>
  18. */
  19. class NumberHelper extends BaseHelper
  20. {
  21. /**
  22. * @var array The default attributes to apply to the \NumberFormatter instance
  23. */
  24. protected $attributes = [];
  25. /**
  26. * @var array The default text attributes to apply to the \NumberFormatter instance
  27. */
  28. protected $textAttributes = [];
  29. /**
  30. * @var array The symbols used by \NumberFormatter
  31. */
  32. protected $symbols = [];
  33. /**
  34. * @param string $charset The output charset of the helper
  35. * @param LocaleDetectorInterface $localeDetector A locale detector instance
  36. * @param array $attributes The default attributes to apply to the \NumberFormatter instance
  37. * @param array $textAttributes The default text attributes to apply to the \NumberFormatter instance
  38. * @param array $symbols The default symbols to apply to the \NumberFormatter instance
  39. */
  40. public function __construct($charset, LocaleDetectorInterface $localeDetector, array $attributes = [], array $textAttributes = [], array $symbols = [])
  41. {
  42. parent::__construct($charset, $localeDetector);
  43. $this->attributes = $attributes;
  44. $this->textAttributes = $textAttributes;
  45. $this->symbols = $symbols;
  46. }
  47. /**
  48. * Formats a number as percent according to the specified locale and
  49. * \NumberFormatter attributes.
  50. *
  51. * @param string|float|int $number The number to format
  52. * @param array $attributes The attributes used by \NumberFormatter
  53. * @param array $textAttributes The text attributes used by \NumberFormatter
  54. * @param string|null $locale The locale used to format the number
  55. *
  56. * @return string The formatted number
  57. */
  58. public function formatPercent($number, array $attributes = [], array $textAttributes = [], $locale = null)
  59. {
  60. $methodArgs = array_pad(\func_get_args(), 5, null);
  61. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[3], $methodArgs[4]);
  62. $number = $this->parseNumericValue($number);
  63. return $this->format($number, \NumberFormatter::PERCENT, $attributes, $textAttributes, $symbols, $locale);
  64. }
  65. /**
  66. * Formats a number as duration according to the specified locale and
  67. * \NumberFormatter attributes.
  68. *
  69. * @param string|float|int $number The number to format
  70. * @param array $attributes The attributes used by \NumberFormatter
  71. * @param array $textAttributes The text attributes used by \NumberFormatter
  72. * @param string|null $locale The locale used to format the number
  73. *
  74. * @return string The formatted number
  75. */
  76. public function formatDuration($number, array $attributes = [], array $textAttributes = [], $locale = null)
  77. {
  78. $methodArgs = array_pad(\func_get_args(), 5, null);
  79. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[3], $methodArgs[4]);
  80. $number = $this->parseNumericValue($number);
  81. return $this->format($number, \NumberFormatter::DURATION, $attributes, $textAttributes, $symbols, $locale);
  82. }
  83. /**
  84. * Formats a number as decimal according to the specified locale and
  85. * \NumberFormatter attributes.
  86. *
  87. * @param string|float|int $number The number to format
  88. * @param array $attributes The attributes used by \NumberFormatter
  89. * @param array $textAttributes The text attributes used by \NumberFormatter
  90. * @param string|null $locale The locale used to format the number
  91. *
  92. * @return string The formatted number
  93. */
  94. public function formatDecimal($number, array $attributes = [], array $textAttributes = [], $locale = null)
  95. {
  96. $methodArgs = array_pad(\func_get_args(), 5, null);
  97. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[3], $methodArgs[4]);
  98. $number = $this->parseNumericValue($number);
  99. return $this->format($number, \NumberFormatter::DECIMAL, $attributes, $textAttributes, $symbols, $locale);
  100. }
  101. /**
  102. * Formats a number as spellout according to the specified locale and
  103. * \NumberFormatter attributes.
  104. *
  105. * @param string|float|int $number The number to format
  106. * @param array $attributes The attributes used by \NumberFormatter
  107. * @param array $textAttributes The text attributes used by \NumberFormatter
  108. * @param string|null $locale The locale used to format the number
  109. *
  110. * @return string The formatted number
  111. */
  112. public function formatSpellout($number, array $attributes = [], array $textAttributes = [], $locale = null)
  113. {
  114. $methodArgs = array_pad(\func_get_args(), 5, null);
  115. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[3], $methodArgs[4]);
  116. return $this->format($number, \NumberFormatter::SPELLOUT, $attributes, $textAttributes, $symbols, $locale);
  117. }
  118. /**
  119. * Formats a number as currency according to the specified locale and
  120. * \NumberFormatter attributes.
  121. *
  122. * @param string|float|int $number The number to format
  123. * @param string $currency The currency in which format the number
  124. * @param array $attributes The attributes used by \NumberFormatter
  125. * @param array $textAttributes The text attributes used by \NumberFormatter
  126. * @param string|null $locale The locale used to format the number
  127. *
  128. * @return string The formatted number
  129. */
  130. public function formatCurrency($number, $currency, array $attributes = [], array $textAttributes = [], $locale = null)
  131. {
  132. $methodArgs = array_pad(\func_get_args(), 6, null);
  133. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[4], $methodArgs[5]);
  134. $formatter = $this->getFormatter($locale ?? $this->localeDetector->getLocale(), \NumberFormatter::CURRENCY, $attributes, $textAttributes, $symbols);
  135. $number = $this->parseNumericValue($number);
  136. return $this->fixCharset($formatter->formatCurrency($number, $currency));
  137. }
  138. /**
  139. * Formats a number in scientific notation according to the specified
  140. * locale and \NumberFormatter attributes.
  141. *
  142. * @param string|float|int $number The number to format
  143. * @param array $attributes The attributes used by \NumberFormatter
  144. * @param array $textAttributes The text attributes used by \NumberFormatter
  145. * @param string|null $locale The locale used to format the number
  146. *
  147. * @return string The formatted number
  148. */
  149. public function formatScientific($number, array $attributes = [], array $textAttributes = [], $locale = null)
  150. {
  151. $methodArgs = array_pad(\func_get_args(), 5, null);
  152. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[3], $methodArgs[4]);
  153. $number = $this->parseNumericValue($number);
  154. return $this->format($number, \NumberFormatter::SCIENTIFIC, $attributes, $textAttributes, $symbols, $locale);
  155. }
  156. /**
  157. * Formats a number as ordinal according to the specified locale and
  158. * \NumberFormatter attributes.
  159. *
  160. * @param string|float|int $number The number to format
  161. * @param array $attributes The attributes used by \NumberFormatter
  162. * @param array $textAttributes The text attributes used by \NumberFormatter
  163. * @param string|null $locale The locale used to format the number
  164. *
  165. * @return string The formatted number
  166. */
  167. public function formatOrdinal($number, array $attributes = [], array $textAttributes = [], $locale = null)
  168. {
  169. $methodArgs = array_pad(\func_get_args(), 5, null);
  170. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[3], $methodArgs[4]);
  171. $number = $this->parseNumericValue($number);
  172. return $this->format($number, \NumberFormatter::ORDINAL, $attributes, $textAttributes, $symbols, $locale);
  173. }
  174. /**
  175. * Formats a number according to the specified locale and \NumberFormatter
  176. * attributes.
  177. *
  178. * @param string|float|int $number The number to format
  179. * @param int $style
  180. * @param array $attributes The attributes used by the formatter
  181. * @param array $textAttributes The text attributes used by the formatter
  182. * @param string|null $locale The locale used to format the number
  183. *
  184. * @return string
  185. */
  186. public function format($number, $style, array $attributes = [], array $textAttributes = [], $locale = null)
  187. {
  188. $methodArgs = array_pad(\func_get_args(), 6, null);
  189. [$locale, $symbols] = $this->normalizeMethodSignature($methodArgs[4], $methodArgs[5]);
  190. $formatter = $this->getFormatter($locale ?? $this->localeDetector->getLocale(), $style, $attributes, $textAttributes, $symbols);
  191. return $this->fixCharset($formatter->format($number));
  192. }
  193. /**
  194. * Normalizes the given arguments according to the new function signature.
  195. * It asserts if neither the new nor old signature matches. This function
  196. * is public just to prevent code duplication inside the Twig Extension.
  197. *
  198. * @param mixed $symbols The symbols used by the formatter
  199. * @param mixed $locale The locale
  200. *
  201. * @throws \BadMethodCallException If the arguments does not match any signature
  202. *
  203. * @return array Arguments list normalized to the new method signature
  204. *
  205. * @internal
  206. */
  207. public function normalizeMethodSignature($symbols, $locale)
  208. {
  209. $oldSignature = (null === $symbols || \is_string($symbols)) && null === $locale;
  210. $newSignature = \is_array($symbols) && (\is_string($locale) || null === $locale);
  211. // Confirm possible signatures
  212. if (!$oldSignature && !$newSignature) {
  213. throw new \BadMethodCallException('Neither new nor old signature matches, please provide the correct arguments');
  214. }
  215. if ($oldSignature) {
  216. // If the old signature matches, we pass an empty array as symbols
  217. // argument and the symbols value as the locale argument.
  218. return [$symbols, []];
  219. }
  220. return [$locale, $symbols];
  221. }
  222. /**
  223. * @return string
  224. */
  225. public function getName()
  226. {
  227. return 'sonata_intl_number';
  228. }
  229. /**
  230. * Gets an instance of \NumberFormatter set with the given attributes and
  231. * style.
  232. *
  233. * @param string $culture The culture used by \NumberFormatter
  234. * @param int $style The style used by \NumberFormatter
  235. * @param array $attributes The attributes used by \NumberFormatter
  236. * @param array $textAttributes The text attributes used by \NumberFormatter
  237. * @param array $symbols The symbols used by \NumberFormatter
  238. *
  239. * @return \NumberFormatter
  240. */
  241. protected function getFormatter($culture, $style, $attributes = [], $textAttributes = [], $symbols = [])
  242. {
  243. $attributes = $this->parseAttributes(array_merge($this->attributes, $attributes));
  244. $textAttributes = $this->parseAttributes(array_merge($this->textAttributes, $textAttributes));
  245. $symbols = $this->parseAttributes(array_merge($this->symbols, $symbols));
  246. $formatter = new \NumberFormatter($culture, $style);
  247. self::checkInternalClass($formatter, \NumberFormatter::class, [
  248. 'culture' => $culture,
  249. 'style' => $style,
  250. ]);
  251. foreach ($attributes as $attribute => $value) {
  252. $formatter->setAttribute($attribute, $value);
  253. }
  254. foreach ($textAttributes as $attribute => $value) {
  255. $formatter->setTextAttribute($attribute, $value);
  256. }
  257. foreach ($symbols as $attribute => $value) {
  258. $formatter->setSymbol($attribute, $value);
  259. }
  260. return $formatter;
  261. }
  262. /**
  263. * Converts keys of attributes array to values of \NumberFormatter constants.
  264. *
  265. * @param array $attributes The list of attributes
  266. *
  267. * @throws \InvalidArgumentException If any attribute does not match any constant
  268. *
  269. * @return array List of \NumberFormatter constants
  270. */
  271. protected function parseAttributes(array $attributes)
  272. {
  273. $result = [];
  274. foreach ($attributes as $attribute => $value) {
  275. $result[$this->parseConstantValue($attribute)] = $value;
  276. }
  277. return $result;
  278. }
  279. /**
  280. * Parse the given value trying to get a match with a \NumberFormatter constant.
  281. *
  282. * @param string $attribute The constant's name
  283. *
  284. * @throws \InvalidArgumentException If the value does not match any constant
  285. *
  286. * @return mixed The \NumberFormatter constant
  287. */
  288. protected function parseConstantValue($attribute)
  289. {
  290. $attribute = strtoupper($attribute);
  291. $constantName = 'NumberFormatter::'.$attribute;
  292. if (!\defined($constantName)) {
  293. throw new \InvalidArgumentException(sprintf('NumberFormatter has no constant "%s".', $attribute));
  294. }
  295. return \constant($constantName);
  296. }
  297. /**
  298. * @param string|int|float $number
  299. *
  300. * @return int|float
  301. */
  302. private function parseNumericValue($number)
  303. {
  304. if (\is_float($number) || \is_int($number)) {
  305. return $number;
  306. }
  307. if (is_numeric($number)) {
  308. return (float) $number;
  309. }
  310. throw new \TypeError('Number must be either a float, an integer or a numeric string');
  311. }
  312. }