Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

   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       * Returns true if the number has any units
 232       *
 233       * @return bool
 234       */
 235      public function hasUnits()
 236      {
 237          return !$this->unitless();
 238      }
 239  
 240      /**
 241       * Checks whether the number has exactly this unit
 242       *
 243       * @param string $unit
 244       *
 245       * @return bool
 246       */
 247      public function hasUnit($unit)
 248      {
 249          return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
 250      }
 251  
 252      /**
 253       * Returns unit(s) as the product of numerator units divided by the product of denominator units
 254       *
 255       * @return string
 256       */
 257      public function unitStr()
 258      {
 259          if ($this->unitless()) {
 260              return '';
 261          }
 262  
 263          return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
 264      }
 265  
 266      /**
 267       * @param float|int $min
 268       * @param float|int $max
 269       * @param string|null $name
 270       *
 271       * @return float|int
 272       * @throws SassScriptException
 273       */
 274      public function valueInRange($min, $max, $name = null)
 275      {
 276          try {
 277              return Util::checkRange('', new Range($min, $max), $this);
 278          } catch (RangeException $e) {
 279              throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name);
 280          }
 281      }
 282  
 283      /**
 284       * @param float|int $min
 285       * @param float|int $max
 286       * @param string    $name
 287       * @param string    $unit
 288       *
 289       * @return float|int
 290       * @throws SassScriptException
 291       *
 292       * @internal
 293       */
 294      public function valueInRangeWithUnit($min, $max, $name, $unit)
 295      {
 296          try {
 297              return Util::checkRange('', new Range($min, $max), $this);
 298          } catch (RangeException $e) {
 299              throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name);
 300          }
 301      }
 302  
 303      /**
 304       * @param string|null $varName
 305       *
 306       * @return void
 307       */
 308      public function assertNoUnits($varName = null)
 309      {
 310          if ($this->unitless()) {
 311              return;
 312          }
 313  
 314          throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName);
 315      }
 316  
 317      /**
 318       * @param string      $unit
 319       * @param string|null $varName
 320       *
 321       * @return void
 322       */
 323      public function assertUnit($unit, $varName = null)
 324      {
 325          if ($this->hasUnit($unit)) {
 326              return;
 327          }
 328  
 329          throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName);
 330      }
 331  
 332      /**
 333       * @param Number $other
 334       *
 335       * @return void
 336       */
 337      public function assertSameUnitOrUnitless(Number $other)
 338      {
 339          if ($other->unitless()) {
 340              return;
 341          }
 342  
 343          if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) {
 344              return;
 345          }
 346  
 347          throw new SassScriptException(sprintf(
 348              'Incompatible units %s and %s.',
 349              self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 350              self::getUnitString($other->numeratorUnits, $other->denominatorUnits)
 351          ));
 352      }
 353  
 354      /**
 355       * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits.
 356       *
 357       * This does not throw an error if this number is unitless and
 358       * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead,
 359       * it treats all unitless numbers as convertible to and from all units without
 360       * changing the value.
 361       *
 362       * @param string[] $newNumeratorUnits
 363       * @param string[] $newDenominatorUnits
 364       *
 365       * @return Number
 366       *
 367       * @phpstan-param list<string> $newNumeratorUnits
 368       * @phpstan-param list<string> $newDenominatorUnits
 369       *
 370       * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits
 371       */
 372      public function coerce(array $newNumeratorUnits, array $newDenominatorUnits)
 373      {
 374          return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits);
 375      }
 376  
 377      /**
 378       * @param Number $other
 379       *
 380       * @return bool
 381       */
 382      public function isComparableTo(Number $other)
 383      {
 384          if ($this->unitless() || $other->unitless()) {
 385              return true;
 386          }
 387  
 388          try {
 389              $this->greaterThan($other);
 390              return true;
 391          } catch (SassScriptException $e) {
 392              return false;
 393          }
 394      }
 395  
 396      /**
 397       * @param Number $other
 398       *
 399       * @return bool
 400       */
 401      public function lessThan(Number $other)
 402      {
 403          return $this->coerceUnits($other, function ($num1, $num2) {
 404              return $num1 < $num2;
 405          });
 406      }
 407  
 408      /**
 409       * @param Number $other
 410       *
 411       * @return bool
 412       */
 413      public function lessThanOrEqual(Number $other)
 414      {
 415          return $this->coerceUnits($other, function ($num1, $num2) {
 416              return $num1 <= $num2;
 417          });
 418      }
 419  
 420      /**
 421       * @param Number $other
 422       *
 423       * @return bool
 424       */
 425      public function greaterThan(Number $other)
 426      {
 427          return $this->coerceUnits($other, function ($num1, $num2) {
 428              return $num1 > $num2;
 429          });
 430      }
 431  
 432      /**
 433       * @param Number $other
 434       *
 435       * @return bool
 436       */
 437      public function greaterThanOrEqual(Number $other)
 438      {
 439          return $this->coerceUnits($other, function ($num1, $num2) {
 440              return $num1 >= $num2;
 441          });
 442      }
 443  
 444      /**
 445       * @param Number $other
 446       *
 447       * @return Number
 448       */
 449      public function plus(Number $other)
 450      {
 451          return $this->coerceNumber($other, function ($num1, $num2) {
 452              return $num1 + $num2;
 453          });
 454      }
 455  
 456      /**
 457       * @param Number $other
 458       *
 459       * @return Number
 460       */
 461      public function minus(Number $other)
 462      {
 463          return $this->coerceNumber($other, function ($num1, $num2) {
 464              return $num1 - $num2;
 465          });
 466      }
 467  
 468      /**
 469       * @return Number
 470       */
 471      public function unaryMinus()
 472      {
 473          return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits);
 474      }
 475  
 476      /**
 477       * @param Number $other
 478       *
 479       * @return Number
 480       */
 481      public function modulo(Number $other)
 482      {
 483          return $this->coerceNumber($other, function ($num1, $num2) {
 484              if ($num2 == 0) {
 485                  return NAN;
 486              }
 487  
 488              $result = fmod($num1, $num2);
 489  
 490              if ($result == 0) {
 491                  return 0;
 492              }
 493  
 494              if ($num2 < 0 xor $num1 < 0) {
 495                  $result += $num2;
 496              }
 497  
 498              return $result;
 499          });
 500      }
 501  
 502      /**
 503       * @param Number $other
 504       *
 505       * @return Number
 506       */
 507      public function times(Number $other)
 508      {
 509          return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
 510      }
 511  
 512      /**
 513       * @param Number $other
 514       *
 515       * @return Number
 516       */
 517      public function dividedBy(Number $other)
 518      {
 519          if ($other->dimension == 0) {
 520              if ($this->dimension == 0) {
 521                  $value = NAN;
 522              } elseif ($this->dimension > 0) {
 523                  $value = INF;
 524              } else {
 525                  $value = -INF;
 526              }
 527          } else {
 528              $value = $this->dimension / $other->dimension;
 529          }
 530  
 531          return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits);
 532      }
 533  
 534      /**
 535       * @param Number $other
 536       *
 537       * @return bool
 538       */
 539      public function equals(Number $other)
 540      {
 541          // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
 542          if ($this->unitless() !== $other->unitless()) {
 543              return false;
 544          }
 545  
 546          // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF
 547          if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) {
 548              return false;
 549          }
 550  
 551          if ($this->unitless()) {
 552              return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION);
 553          }
 554  
 555          try {
 556              return $this->coerceUnits($other, function ($num1, $num2) {
 557                  return round($num1,self::PRECISION) == round($num2, self::PRECISION);
 558              });
 559          } catch (SassScriptException $e) {
 560              return false;
 561          }
 562      }
 563  
 564      /**
 565       * Output number
 566       *
 567       * @param \ScssPhp\ScssPhp\Compiler $compiler
 568       *
 569       * @return string
 570       */
 571      public function output(Compiler $compiler = null)
 572      {
 573          $dimension = round($this->dimension, self::PRECISION);
 574  
 575          if (is_nan($dimension)) {
 576              return 'NaN';
 577          }
 578  
 579          if ($dimension === INF) {
 580              return 'Infinity';
 581          }
 582  
 583          if ($dimension === -INF) {
 584              return '-Infinity';
 585          }
 586  
 587          if ($compiler) {
 588              $unit = $this->unitStr();
 589          } elseif (isset($this->numeratorUnits[0])) {
 590              $unit = $this->numeratorUnits[0];
 591          } else {
 592              $unit = '';
 593          }
 594  
 595          $dimension = number_format($dimension, self::PRECISION, '.', '');
 596  
 597          return rtrim(rtrim($dimension, '0'), '.') . $unit;
 598      }
 599  
 600      /**
 601       * {@inheritdoc}
 602       */
 603      public function __toString()
 604      {
 605          return $this->output();
 606      }
 607  
 608      /**
 609       * @param Number   $other
 610       * @param callable $operation
 611       *
 612       * @return Number
 613       *
 614       * @phpstan-param callable(int|float, int|float): (int|float) $operation
 615       */
 616      private function coerceNumber(Number $other, $operation)
 617      {
 618          $result = $this->coerceUnits($other, $operation);
 619  
 620          if (!$this->unitless()) {
 621              return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
 622          }
 623  
 624          return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
 625      }
 626  
 627      /**
 628       * @param Number $other
 629       * @param callable $operation
 630       *
 631       * @return mixed
 632       *
 633       * @phpstan-template T
 634       * @phpstan-param callable(int|float, int|float): T $operation
 635       * @phpstan-return T
 636       */
 637      private function coerceUnits(Number $other, $operation)
 638      {
 639          if (!$this->unitless()) {
 640              $num1 = $this->dimension;
 641              $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits);
 642          } else {
 643              $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits);
 644              $num2 = $other->dimension;
 645          }
 646  
 647          return \call_user_func($operation, $num1, $num2);
 648      }
 649  
 650      /**
 651       * @param string[] $numeratorUnits
 652       * @param string[] $denominatorUnits
 653       *
 654       * @return int|float
 655       *
 656       * @phpstan-param list<string> $numeratorUnits
 657       * @phpstan-param list<string> $denominatorUnits
 658       *
 659       * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits
 660       */
 661      private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
 662      {
 663          if (
 664              $this->unitless()
 665              || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0)
 666              || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits)
 667          ) {
 668              return $this->dimension;
 669          }
 670  
 671          $value = $this->dimension;
 672          $oldNumerators = $this->numeratorUnits;
 673  
 674          foreach ($numeratorUnits as $newNumerator) {
 675              foreach ($oldNumerators as $key => $oldNumerator) {
 676                  $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator);
 677  
 678                  if (\is_null($conversionFactor)) {
 679                      continue;
 680                  }
 681  
 682                  $value *= $conversionFactor;
 683                  unset($oldNumerators[$key]);
 684                  continue 2;
 685              }
 686  
 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          $oldDenominators = $this->denominatorUnits;
 695  
 696          foreach ($denominatorUnits as $newDenominator) {
 697              foreach ($oldDenominators as $key => $oldDenominator) {
 698                  $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator);
 699  
 700                  if (\is_null($conversionFactor)) {
 701                      continue;
 702                  }
 703  
 704                  $value /= $conversionFactor;
 705                  unset($oldDenominators[$key]);
 706                  continue 2;
 707              }
 708  
 709              throw new SassScriptException(sprintf(
 710                  'Incompatible units %s and %s.',
 711                  self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 712                  self::getUnitString($numeratorUnits, $denominatorUnits)
 713              ));
 714          }
 715  
 716          if (\count($oldNumerators) || \count($oldDenominators)) {
 717              throw new SassScriptException(sprintf(
 718                  'Incompatible units %s and %s.',
 719                  self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
 720                  self::getUnitString($numeratorUnits, $denominatorUnits)
 721              ));
 722          }
 723  
 724          return $value;
 725      }
 726  
 727      /**
 728       * @param int|float $value
 729       * @param string[] $numerators1
 730       * @param string[] $denominators1
 731       * @param string[] $numerators2
 732       * @param string[] $denominators2
 733       *
 734       * @return Number
 735       *
 736       * @phpstan-param list<string> $numerators1
 737       * @phpstan-param list<string> $denominators1
 738       * @phpstan-param list<string> $numerators2
 739       * @phpstan-param list<string> $denominators2
 740       */
 741      private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
 742      {
 743          $newNumerators = array();
 744  
 745          foreach ($numerators1 as $numerator) {
 746              foreach ($denominators2 as $key => $denominator) {
 747                  $conversionFactor = self::getConversionFactor($numerator, $denominator);
 748  
 749                  if (\is_null($conversionFactor)) {
 750                      continue;
 751                  }
 752  
 753                  $value /= $conversionFactor;
 754                  unset($denominators2[$key]);
 755                  continue 2;
 756              }
 757  
 758              $newNumerators[] = $numerator;
 759          }
 760  
 761          foreach ($numerators2 as $numerator) {
 762              foreach ($denominators1 as $key => $denominator) {
 763                  $conversionFactor = self::getConversionFactor($numerator, $denominator);
 764  
 765                  if (\is_null($conversionFactor)) {
 766                      continue;
 767                  }
 768  
 769                  $value /= $conversionFactor;
 770                  unset($denominators1[$key]);
 771                  continue 2;
 772              }
 773  
 774              $newNumerators[] = $numerator;
 775          }
 776  
 777          $newDenominators = array_values(array_merge($denominators1, $denominators2));
 778  
 779          return new Number($value, $newNumerators, $newDenominators);
 780      }
 781  
 782      /**
 783       * Returns the number of [unit1]s per [unit2].
 784       *
 785       * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
 786       *
 787       * @param string $unit1
 788       * @param string $unit2
 789       *
 790       * @return float|int|null
 791       */
 792      private static function getConversionFactor($unit1, $unit2)
 793      {
 794          if ($unit1 === $unit2) {
 795              return 1;
 796          }
 797  
 798          foreach (static::$unitTable as $unitVariants) {
 799              if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
 800                  return $unitVariants[$unit1] / $unitVariants[$unit2];
 801              }
 802          }
 803  
 804          return null;
 805      }
 806  
 807      /**
 808       * Returns unit(s) as the product of numerator units divided by the product of denominator units
 809       *
 810       * @param string[] $numerators
 811       * @param string[] $denominators
 812       *
 813       * @phpstan-param list<string> $numerators
 814       * @phpstan-param list<string> $denominators
 815       *
 816       * @return string
 817       */
 818      private static function getUnitString(array $numerators, array $denominators)
 819      {
 820          if (!\count($numerators)) {
 821              if (\count($denominators) === 0) {
 822                  return 'no units';
 823              }
 824  
 825              if (\count($denominators) === 1) {
 826                  return $denominators[0] . '^-1';
 827              }
 828  
 829              return '(' . implode('*', $denominators) . ')^-1';
 830          }
 831  
 832          return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
 833      }
 834  }