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

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