Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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