Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 and 403]

   1  <?php
   2  
   3  /**
   4   * SCSSPHP
   5   *
   6   * @copyright 2012-2020 Leaf Corcoran
   7   *
   8   * @license http://opensource.org/licenses/MIT MIT
   9   *
  10   * @link http://scssphp.github.io/scssphp
  11   */
  12  
  13  namespace ScssPhp\ScssPhp\Node;
  14  
  15  use ScssPhp\ScssPhp\Base\Range;
  16  use ScssPhp\ScssPhp\Compiler;
  17  use ScssPhp\ScssPhp\Exception\RangeException;
  18  use ScssPhp\ScssPhp\Exception\SassScriptException;
  19  use ScssPhp\ScssPhp\Node;
  20  use ScssPhp\ScssPhp\Type;
  21  use ScssPhp\ScssPhp\Util;
  22  
  23  /**
  24   * Dimension + optional units
  25   *
  26   * {@internal
  27   *     This is a work-in-progress.
  28   *
  29   *     The \ArrayAccess interface is temporary until the migration is complete.
  30   * }}
  31   *
  32   * @author Anthon Pang <anthon.pang@gmail.com>
  33   *
  34   * @template-implements \ArrayAccess<int, mixed>
  35   */
  36  class Number extends Node implements \ArrayAccess
  37  {
  38      const PRECISION = 10;
  39  
  40      /**
  41       * @var int
  42       * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
  43       */
  44      public static $precision = self::PRECISION;
  45  
  46      /**
  47       * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
  48       *
  49       * @var array
  50       * @phpstan-var array<string, array<string, float|int>>
  51       */
  52      protected static $unitTable = [
  53          'in' => [
  54              'in' => 1,
  55              'pc' => 6,
  56              'pt' => 72,
  57              'px' => 96,
  58              'cm' => 2.54,
  59              'mm' => 25.4,
  60              'q'  => 101.6,
  61          ],
  62          'turn' => [
  63              'deg'  => 360,
  64              'grad' => 400,
  65              'rad'  => 6.28318530717958647692528676, // 2 * M_PI
  66              'turn' => 1,
  67          ],
  68          's' => [
  69              's'  => 1,
  70              'ms' => 1000,
  71          ],
  72          'Hz' => [
  73              'Hz'  => 1,
  74              'kHz' => 0.001,
  75          ],
  76          'dpi' => [
  77              'dpi'  => 1,
  78              'dpcm' => 1 / 2.54,
  79              'dppx' => 1 / 96,
  80          ],
  81      ];
  82  
  83      /**
  84       * @var int|float
  85       */
  86      private $dimension;
  87  
  88      /**
  89       * @var string[]
  90       * @phpstan-var list<string>
  91       */
  92      private $numeratorUnits;
  93  
  94      /**
  95       * @var string[]
  96       * @phpstan-var list<string>
  97       */
  98      private $denominatorUnits;
  99  
 100      /**
 101       * Initialize number
 102       *
 103       * @param int|float       $dimension
 104       * @param string[]|string $numeratorUnits
 105       * @param string[]        $denominatorUnits
 106       *
 107       * @phpstan-param list<string>|string $numeratorUnits
 108       * @phpstan-param list<string>        $denominatorUnits
 109       */
 110      public function __construct($dimension, $numeratorUnits, array $denominatorUnits = [])
 111      {
 112          if (is_string($numeratorUnits)) {
 113              $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : [];
 114          } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) {
 115              // TODO get rid of this once `$number[2]` is not used anymore
 116              $denominatorUnits = $numeratorUnits['denominator_units'];
 117              $numeratorUnits = $numeratorUnits['numerator_units'];
 118          }
 119  
 120          $this->dimension = $dimension;
 121          $this->numeratorUnits = $numeratorUnits;
 122          $this->denominatorUnits = $denominatorUnits;
 123      }
 124  
 125      /**
 126       * @return float|int
 127       */
 128      public function getDimension()
 129      {
 130          return $this->dimension;
 131      }
 132  
 133      /**
 134       * @return string[]
 135       */
 136      public function getNumeratorUnits()
 137      {
 138          return $this->numeratorUnits;
 139      }
 140  
 141      /**
 142       * @return string[]
 143       */
 144      public function getDenominatorUnits()
 145      {
 146          return $this->denominatorUnits;
 147      }
 148  
 149      /**
 150       * @return bool
 151       */
 152      #[\ReturnTypeWillChange]
 153      public function offsetExists($offset)
 154      {
 155          if ($offset === -3) {
 156              return ! \is_null($this->sourceColumn);
 157          }
 158  
 159          if ($offset === -2) {
 160              return ! \is_null($this->sourceLine);
 161          }
 162  
 163          if (
 164              $offset === -1 ||
 165              $offset === 0 ||
 166              $offset === 1 ||
 167              $offset === 2
 168          ) {
 169              return true;
 170          }
 171  
 172          return false;
 173      }
 174  
 175      /**
 176       * @return mixed
 177       */
 178      #[\ReturnTypeWillChange]
 179      public function offsetGet($offset)
 180      {
 181          switch ($offset) {
 182              case -3:
 183                  return $this->sourceColumn;
 184  
 185              case -2:
 186                  return $this->sourceLine;
 187  
 188              case -1:
 189                  return $this->sourceIndex;
 190  
 191              case 0:
 192                  return Type::T_NUMBER;
 193  
 194              case 1:
 195                  return $this->dimension;
 196  
 197              case 2:
 198                  return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits);
 199          }
 200      }
 201  
 202      /**
 203       * @return void
 204       */
 205      #[\ReturnTypeWillChange]
 206      public function offsetSet($offset, $value)
 207      {
 208          throw new \BadMethodCallException('Number is immutable');
 209      }
 210  
 211      /**
 212       * @return void
 213       */
 214      #[\ReturnTypeWillChange]
 215      public function offsetUnset($offset)
 216      {
 217          throw new \BadMethodCallException('Number is immutable');
 218      }
 219  
 220      /**
 221       * Returns true if the number is unitless
 222       *
 223       * @return bool
 224       */
 225      public function unitless()
 226      {
 227          return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0;
 228      }
 229  
 230      /**
 231       * Checks whether the number has exactly this unit
 232       *
 233       * @param string $unit
 234       *
 235       * @return bool
 236       */
 237      public function hasUnit($unit)
 238      {
 239          return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
 240      }
 241  
 242      /**
 243       * Returns unit(s) as the product of numerator units divided by the product of denominator units
 244       *
 245       * @return string
 246       */
 247      public function unitStr()
 248      {
 249          if ($this->unitless()) {
 250              return '';
 251          }
 252  
 253          return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
 254      }
 255  
 256      /**
 257       * @param float|int $min
 258       * @param float|int $max
 259       * @param string|null $name
 260       *
 261       * @return float|int
 262       * @throws SassScriptException
 263       */
 264      public function valueInRange($min, $max, $name = null)
 265      {
 266          try {
 267              return Util::checkRange('', new Range($min, $max), $this);
 268          } catch (RangeException $e) {
 269              throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s', $this, $min, $this->unitStr(), $max), $name);
 270          }
 271      }
 272  
 273      /**
 274       * @param string|null $varName
 275       *
 276       * @return void
 277       */
 278      public function assertNoUnits($varName = null)
 279      {
 280          if ($this->unitless()) {
 281              return;
 282          }
 283  
 284          throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName);
 285      }
 286  
 287      /**
 288       * @param string      $unit
 289       * @param string|null $varName
 290       *
 291       * @return void
 292       */
 293      public function assertUnit($unit, $varName = null)
 294      {
 295          if ($this->hasUnit($unit)) {
 296              return;
 297          }
 298  
 299          throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName);
 300      }
 301  
 302      /**
 303       * @param Number $other
 304       *
 305       * @return void
 306       */
 307      public function assertSameUnitOrUnitless(Number $other)
 308      {
 309          if ($other->unitless()) {
 310              return;
 311          }
 312  
 313          if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) {
 314              return;
 315          }
 316  
 317          throw new SassScriptException(sprintf(
 318              'Incompatible units %s and %s.',
 319              self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 320              self::getUnitString($other->numeratorUnits, $other->denominatorUnits)
 321          ));
 322      }
 323  
 324      /**
 325       * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits.
 326       *
 327       * This does not throw an error if this number is unitless and
 328       * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead,
 329       * it treats all unitless numbers as convertible to and from all units without
 330       * changing the value.
 331       *
 332       * @param string[] $newNumeratorUnits
 333       * @param string[] $newDenominatorUnits
 334       *
 335       * @return Number
 336       *
 337       * @phpstan-param list<string> $newNumeratorUnits
 338       * @phpstan-param list<string> $newDenominatorUnits
 339       *
 340       * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits
 341       */
 342      public function coerce(array $newNumeratorUnits, array $newDenominatorUnits)
 343      {
 344          return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits);
 345      }
 346  
 347      /**
 348       * @param Number $other
 349       *
 350       * @return bool
 351       */
 352      public function isComparableTo(Number $other)
 353      {
 354          if ($this->unitless() || $other->unitless()) {
 355              return true;
 356          }
 357  
 358          try {
 359              $this->greaterThan($other);
 360              return true;
 361          } catch (SassScriptException $e) {
 362              return false;
 363          }
 364      }
 365  
 366      /**
 367       * @param Number $other
 368       *
 369       * @return bool
 370       */
 371      public function lessThan(Number $other)
 372      {
 373          return $this->coerceUnits($other, function ($num1, $num2) {
 374              return $num1 < $num2;
 375          });
 376      }
 377  
 378      /**
 379       * @param Number $other
 380       *
 381       * @return bool
 382       */
 383      public function lessThanOrEqual(Number $other)
 384      {
 385          return $this->coerceUnits($other, function ($num1, $num2) {
 386              return $num1 <= $num2;
 387          });
 388      }
 389  
 390      /**
 391       * @param Number $other
 392       *
 393       * @return bool
 394       */
 395      public function greaterThan(Number $other)
 396      {
 397          return $this->coerceUnits($other, function ($num1, $num2) {
 398              return $num1 > $num2;
 399          });
 400      }
 401  
 402      /**
 403       * @param Number $other
 404       *
 405       * @return bool
 406       */
 407      public function greaterThanOrEqual(Number $other)
 408      {
 409          return $this->coerceUnits($other, function ($num1, $num2) {
 410              return $num1 >= $num2;
 411          });
 412      }
 413  
 414      /**
 415       * @param Number $other
 416       *
 417       * @return Number
 418       */
 419      public function plus(Number $other)
 420      {
 421          return $this->coerceNumber($other, function ($num1, $num2) {
 422              return $num1 + $num2;
 423          });
 424      }
 425  
 426      /**
 427       * @param Number $other
 428       *
 429       * @return Number
 430       */
 431      public function minus(Number $other)
 432      {
 433          return $this->coerceNumber($other, function ($num1, $num2) {
 434              return $num1 - $num2;
 435          });
 436      }
 437  
 438      /**
 439       * @return Number
 440       */
 441      public function unaryMinus()
 442      {
 443          return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits);
 444      }
 445  
 446      /**
 447       * @param Number $other
 448       *
 449       * @return Number
 450       */
 451      public function modulo(Number $other)
 452      {
 453          return $this->coerceNumber($other, function ($num1, $num2) {
 454              if ($num2 == 0) {
 455                  return NAN;
 456              }
 457  
 458              $result = fmod($num1, $num2);
 459  
 460              if ($result == 0) {
 461                  return 0;
 462              }
 463  
 464              if ($num2 < 0 xor $num1 < 0) {
 465                  $result += $num2;
 466              }
 467  
 468              return $result;
 469          });
 470      }
 471  
 472      /**
 473       * @param Number $other
 474       *
 475       * @return Number
 476       */
 477      public function times(Number $other)
 478      {
 479          return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
 480      }
 481  
 482      /**
 483       * @param Number $other
 484       *
 485       * @return Number
 486       */
 487      public function dividedBy(Number $other)
 488      {
 489          if ($other->dimension == 0) {
 490              if ($this->dimension == 0) {
 491                  $value = NAN;
 492              } elseif ($this->dimension > 0) {
 493                  $value = INF;
 494              } else {
 495                  $value = -INF;
 496              }
 497          } else {
 498              $value = $this->dimension / $other->dimension;
 499          }
 500  
 501          return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits);
 502      }
 503  
 504      /**
 505       * @param Number $other
 506       *
 507       * @return bool
 508       */
 509      public function equals(Number $other)
 510      {
 511          // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
 512          if ($this->unitless() !== $other->unitless()) {
 513              return false;
 514          }
 515  
 516          // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF
 517          if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) {
 518              return false;
 519          }
 520  
 521          if ($this->unitless()) {
 522              return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION);
 523          }
 524  
 525          try {
 526              return $this->coerceUnits($other, function ($num1, $num2) {
 527                  return round($num1,self::PRECISION) == round($num2, self::PRECISION);
 528              });
 529          } catch (SassScriptException $e) {
 530              return false;
 531          }
 532      }
 533  
 534      /**
 535       * Output number
 536       *
 537       * @param \ScssPhp\ScssPhp\Compiler $compiler
 538       *
 539       * @return string
 540       */
 541      public function output(Compiler $compiler = null)
 542      {
 543          $dimension = round($this->dimension, self::PRECISION);
 544  
 545          if (is_nan($dimension)) {
 546              return 'NaN';
 547          }
 548  
 549          if ($dimension === INF) {
 550              return 'Infinity';
 551          }
 552  
 553          if ($dimension === -INF) {
 554              return '-Infinity';
 555          }
 556  
 557          if ($compiler) {
 558              $unit = $this->unitStr();
 559          } elseif (isset($this->numeratorUnits[0])) {
 560              $unit = $this->numeratorUnits[0];
 561          } else {
 562              $unit = '';
 563          }
 564  
 565          $dimension = number_format($dimension, self::PRECISION, '.', '');
 566  
 567          return rtrim(rtrim($dimension, '0'), '.') . $unit;
 568      }
 569  
 570      /**
 571       * {@inheritdoc}
 572       */
 573      public function __toString()
 574      {
 575          return $this->output();
 576      }
 577  
 578      /**
 579       * @param Number   $other
 580       * @param callable $operation
 581       *
 582       * @return Number
 583       *
 584       * @phpstan-param callable(int|float, int|float): (int|float) $operation
 585       */
 586      private function coerceNumber(Number $other, $operation)
 587      {
 588          $result = $this->coerceUnits($other, $operation);
 589  
 590          if (!$this->unitless()) {
 591              return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
 592          }
 593  
 594          return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
 595      }
 596  
 597      /**
 598       * @param Number $other
 599       * @param callable $operation
 600       *
 601       * @return mixed
 602       *
 603       * @phpstan-template T
 604       * @phpstan-param callable(int|float, int|float): T $operation
 605       * @phpstan-return T
 606       */
 607      private function coerceUnits(Number $other, $operation)
 608      {
 609          if (!$this->unitless()) {
 610              $num1 = $this->dimension;
 611              $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits);
 612          } else {
 613              $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits);
 614              $num2 = $other->dimension;
 615          }
 616  
 617          return \call_user_func($operation, $num1, $num2);
 618      }
 619  
 620      /**
 621       * @param string[] $numeratorUnits
 622       * @param string[] $denominatorUnits
 623       *
 624       * @return int|float
 625       *
 626       * @phpstan-param list<string> $numeratorUnits
 627       * @phpstan-param list<string> $denominatorUnits
 628       *
 629       * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits
 630       */
 631      private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
 632      {
 633          if (
 634              $this->unitless()
 635              || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0)
 636              || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits)
 637          ) {
 638              return $this->dimension;
 639          }
 640  
 641          $value = $this->dimension;
 642          $oldNumerators = $this->numeratorUnits;
 643  
 644          foreach ($numeratorUnits as $newNumerator) {
 645              foreach ($oldNumerators as $key => $oldNumerator) {
 646                  $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator);
 647  
 648                  if (\is_null($conversionFactor)) {
 649                      continue;
 650                  }
 651  
 652                  $value *= $conversionFactor;
 653                  unset($oldNumerators[$key]);
 654                  continue 2;
 655              }
 656  
 657              throw new SassScriptException(sprintf(
 658                  'Incompatible units %s and %s.',
 659                  self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 660                  self::getUnitString($numeratorUnits, $denominatorUnits)
 661              ));
 662          }
 663  
 664          $oldDenominators = $this->denominatorUnits;
 665  
 666          foreach ($denominatorUnits as $newDenominator) {
 667              foreach ($oldDenominators as $key => $oldDenominator) {
 668                  $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator);
 669  
 670                  if (\is_null($conversionFactor)) {
 671                      continue;
 672                  }
 673  
 674                  $value /= $conversionFactor;
 675                  unset($oldDenominators[$key]);
 676                  continue 2;
 677              }
 678  
 679              throw new SassScriptException(sprintf(
 680                  'Incompatible units %s and %s.',
 681                  self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 682                  self::getUnitString($numeratorUnits, $denominatorUnits)
 683              ));
 684          }
 685  
 686          if (\count($oldNumerators) || \count($oldDenominators)) {
 687              throw new SassScriptException(sprintf(
 688                  'Incompatible units %s and %s.',
 689                  self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 690                  self::getUnitString($numeratorUnits, $denominatorUnits)
 691              ));
 692          }
 693  
 694          return $value;
 695      }
 696  
 697      /**
 698       * @param int|float $value
 699       * @param string[] $numerators1
 700       * @param string[] $denominators1
 701       * @param string[] $numerators2
 702       * @param string[] $denominators2
 703       *
 704       * @return Number
 705       *
 706       * @phpstan-param list<string> $numerators1
 707       * @phpstan-param list<string> $denominators1
 708       * @phpstan-param list<string> $numerators2
 709       * @phpstan-param list<string> $denominators2
 710       */
 711      private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
 712      {
 713          $newNumerators = array();
 714  
 715          foreach ($numerators1 as $numerator) {
 716              foreach ($denominators2 as $key => $denominator) {
 717                  $conversionFactor = self::getConversionFactor($numerator, $denominator);
 718  
 719                  if (\is_null($conversionFactor)) {
 720                      continue;
 721                  }
 722  
 723                  $value /= $conversionFactor;
 724                  unset($denominators2[$key]);
 725                  continue 2;
 726              }
 727  
 728              $newNumerators[] = $numerator;
 729          }
 730  
 731          foreach ($numerators2 as $numerator) {
 732              foreach ($denominators1 as $key => $denominator) {
 733                  $conversionFactor = self::getConversionFactor($numerator, $denominator);
 734  
 735                  if (\is_null($conversionFactor)) {
 736                      continue;
 737                  }
 738  
 739                  $value /= $conversionFactor;
 740                  unset($denominators1[$key]);
 741                  continue 2;
 742              }
 743  
 744              $newNumerators[] = $numerator;
 745          }
 746  
 747          $newDenominators = array_values(array_merge($denominators1, $denominators2));
 748  
 749          return new Number($value, $newNumerators, $newDenominators);
 750      }
 751  
 752      /**
 753       * Returns the number of [unit1]s per [unit2].
 754       *
 755       * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
 756       *
 757       * @param string $unit1
 758       * @param string $unit2
 759       *
 760       * @return float|int|null
 761       */
 762      private static function getConversionFactor($unit1, $unit2)
 763      {
 764          if ($unit1 === $unit2) {
 765              return 1;
 766          }
 767  
 768          foreach (static::$unitTable as $unitVariants) {
 769              if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
 770                  return $unitVariants[$unit1] / $unitVariants[$unit2];
 771              }
 772          }
 773  
 774          return null;
 775      }
 776  
 777      /**
 778       * Returns unit(s) as the product of numerator units divided by the product of denominator units
 779       *
 780       * @param string[] $numerators
 781       * @param string[] $denominators
 782       *
 783       * @phpstan-param list<string> $numerators
 784       * @phpstan-param list<string> $denominators
 785       *
 786       * @return string
 787       */
 788      private static function getUnitString(array $numerators, array $denominators)
 789      {
 790          if (!\count($numerators)) {
 791              if (\count($denominators) === 0) {
 792                  return 'no units';
 793              }
 794  
 795              if (\count($denominators) === 1) {
 796                  return $denominators[0] . '^-1';
 797              }
 798  
 799              return '(' . implode('*', $denominators) . ')^-1';
 800          }
 801  
 802          return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
 803      }
 804  }