vendor/symfony/form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php line 141

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