PageRenderTime 43ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php

http://github.com/symfony/symfony
PHP | 273 lines | 147 code | 47 blank | 79 comment | 29 complexity | 57d707d2fca6a20d083b431abc3c2d77 MD5 | raw file
  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\Form\Extension\Core\DataTransformer;
  11. use Symfony\Component\Form\DataTransformerInterface;
  12. use Symfony\Component\Form\Exception\TransformationFailedException;
  13. /**
  14. * Transforms between a number type and a localized number with grouping
  15. * (each thousand) and comma separators.
  16. *
  17. * @author Bernhard Schussek <bschussek@gmail.com>
  18. * @author Florian Eckerstorfer <florian@eckerstorfer.org>
  19. */
  20. class NumberToLocalizedStringTransformer implements DataTransformerInterface
  21. {
  22. /**
  23. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_CEILING instead.
  24. */
  25. const ROUND_CEILING = \NumberFormatter::ROUND_CEILING;
  26. /**
  27. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_FLOOR instead.
  28. */
  29. const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR;
  30. /**
  31. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_UP instead.
  32. */
  33. const ROUND_UP = \NumberFormatter::ROUND_UP;
  34. /**
  35. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_DOWN instead.
  36. */
  37. const ROUND_DOWN = \NumberFormatter::ROUND_DOWN;
  38. /**
  39. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFEVEN instead.
  40. */
  41. const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN;
  42. /**
  43. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFUP instead.
  44. */
  45. const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP;
  46. /**
  47. * @deprecated since Symfony 5.1, use \NumberFormatter::ROUND_HALFDOWN instead.
  48. */
  49. const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN;
  50. protected $grouping;
  51. protected $roundingMode;
  52. private $scale;
  53. private $locale;
  54. public function __construct(int $scale = null, ?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_HALFUP, string $locale = null)
  55. {
  56. if (null === $grouping) {
  57. $grouping = false;
  58. }
  59. if (null === $roundingMode) {
  60. $roundingMode = \NumberFormatter::ROUND_HALFUP;
  61. }
  62. $this->scale = $scale;
  63. $this->grouping = $grouping;
  64. $this->roundingMode = $roundingMode;
  65. $this->locale = $locale;
  66. }
  67. /**
  68. * Transforms a number type into localized number.
  69. *
  70. * @param int|float $value Number value
  71. *
  72. * @return string Localized value
  73. *
  74. * @throws TransformationFailedException if the given value is not numeric
  75. * or if the value can not be transformed
  76. */
  77. public function transform($value)
  78. {
  79. if (null === $value) {
  80. return '';
  81. }
  82. if (!is_numeric($value)) {
  83. throw new TransformationFailedException('Expected a numeric.');
  84. }
  85. $formatter = $this->getNumberFormatter();
  86. $value = $formatter->format($value);
  87. if (intl_is_failure($formatter->getErrorCode())) {
  88. throw new TransformationFailedException($formatter->getErrorMessage());
  89. }
  90. // Convert non-breaking and narrow non-breaking spaces to normal ones
  91. $value = str_replace(["\xc2\xa0", "\xe2\x80\xaf"], ' ', $value);
  92. return $value;
  93. }
  94. /**
  95. * Transforms a localized number into an integer or float.
  96. *
  97. * @param string $value The localized value
  98. *
  99. * @return int|float The numeric value
  100. *
  101. * @throws TransformationFailedException if the given value is not a string
  102. * or if the value can not be transformed
  103. */
  104. public function reverseTransform($value)
  105. {
  106. if (!\is_string($value)) {
  107. throw new TransformationFailedException('Expected a string.');
  108. }
  109. if ('' === $value) {
  110. return null;
  111. }
  112. if (\in_array($value, ['NaN', 'NAN', 'nan'], true)) {
  113. throw new TransformationFailedException('"NaN" is not a valid number.');
  114. }
  115. $position = 0;
  116. $formatter = $this->getNumberFormatter();
  117. $groupSep = $formatter->getSymbol(\NumberFormatter::GROUPING_SEPARATOR_SYMBOL);
  118. $decSep = $formatter->getSymbol(\NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
  119. if ('.' !== $decSep && (!$this->grouping || '.' !== $groupSep)) {
  120. $value = str_replace('.', $decSep, $value);
  121. }
  122. if (',' !== $decSep && (!$this->grouping || ',' !== $groupSep)) {
  123. $value = str_replace(',', $decSep, $value);
  124. }
  125. if (false !== strpos($value, $decSep)) {
  126. $type = \NumberFormatter::TYPE_DOUBLE;
  127. } else {
  128. $type = PHP_INT_SIZE === 8
  129. ? \NumberFormatter::TYPE_INT64
  130. : \NumberFormatter::TYPE_INT32;
  131. }
  132. $result = $formatter->parse($value, $type, $position);
  133. if (intl_is_failure($formatter->getErrorCode())) {
  134. throw new TransformationFailedException($formatter->getErrorMessage());
  135. }
  136. if ($result >= PHP_INT_MAX || $result <= -PHP_INT_MAX) {
  137. throw new TransformationFailedException('I don\'t have a clear idea what infinity looks like.');
  138. }
  139. $result = $this->castParsedValue($result);
  140. if (false !== $encoding = mb_detect_encoding($value, null, true)) {
  141. $length = mb_strlen($value, $encoding);
  142. $remainder = mb_substr($value, $position, $length, $encoding);
  143. } else {
  144. $length = \strlen($value);
  145. $remainder = substr($value, $position, $length);
  146. }
  147. // After parsing, position holds the index of the character where the
  148. // parsing stopped
  149. if ($position < $length) {
  150. // Check if there are unrecognized characters at the end of the
  151. // number (excluding whitespace characters)
  152. $remainder = trim($remainder, " \t\n\r\0\x0b\xc2\xa0");
  153. if ('' !== $remainder) {
  154. throw new TransformationFailedException(sprintf('The number contains unrecognized characters: "%s".', $remainder));
  155. }
  156. }
  157. // NumberFormatter::parse() does not round
  158. return $this->round($result);
  159. }
  160. /**
  161. * Returns a preconfigured \NumberFormatter instance.
  162. *
  163. * @return \NumberFormatter
  164. */
  165. protected function getNumberFormatter()
  166. {
  167. $formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL);
  168. if (null !== $this->scale) {
  169. $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
  170. $formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
  171. }
  172. $formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
  173. return $formatter;
  174. }
  175. /**
  176. * @internal
  177. */
  178. protected function castParsedValue($value)
  179. {
  180. if (\is_int($value) && $value === (int) $float = (float) $value) {
  181. return $float;
  182. }
  183. return $value;
  184. }
  185. /**
  186. * Rounds a number according to the configured scale and rounding mode.
  187. *
  188. * @param int|float $number A number
  189. *
  190. * @return int|float The rounded number
  191. */
  192. private function round($number)
  193. {
  194. if (null !== $this->scale && null !== $this->roundingMode) {
  195. // shift number to maintain the correct scale during rounding
  196. $roundingCoef = pow(10, $this->scale);
  197. // string representation to avoid rounding errors, similar to bcmul()
  198. $number = (string) ($number * $roundingCoef);
  199. switch ($this->roundingMode) {
  200. case \NumberFormatter::ROUND_CEILING:
  201. $number = ceil($number);
  202. break;
  203. case \NumberFormatter::ROUND_FLOOR:
  204. $number = floor($number);
  205. break;
  206. case \NumberFormatter::ROUND_UP:
  207. $number = $number > 0 ? ceil($number) : floor($number);
  208. break;
  209. case \NumberFormatter::ROUND_DOWN:
  210. $number = $number > 0 ? floor($number) : ceil($number);
  211. break;
  212. case \NumberFormatter::ROUND_HALFEVEN:
  213. $number = round($number, 0, PHP_ROUND_HALF_EVEN);
  214. break;
  215. case \NumberFormatter::ROUND_HALFUP:
  216. $number = round($number, 0, PHP_ROUND_HALF_UP);
  217. break;
  218. case \NumberFormatter::ROUND_HALFDOWN:
  219. $number = round($number, 0, PHP_ROUND_HALF_DOWN);
  220. break;
  221. }
  222. $number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
  223. }
  224. return $number;
  225. }
  226. }