Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
<?php
>
/** * SCSSPHP *
< * @copyright 2012-2019 Leaf Corcoran
> * @copyright 2012-2020 Leaf Corcoran
* * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Node;
> use ScssPhp\ScssPhp\Base\Range;
use ScssPhp\ScssPhp\Compiler;
> use ScssPhp\ScssPhp\Exception\RangeException; use ScssPhp\ScssPhp\Node; > use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Type;
> use ScssPhp\ScssPhp\Util;
/** * Dimension + optional units * * {@internal * This is a work-in-progress. * * The \ArrayAccess interface is temporary until the migration is complete. * }} * * @author Anthon Pang <anthon.pang@gmail.com>
> * */ > * @template-implements \ArrayAccess<int, mixed>
class Number extends Node implements \ArrayAccess {
> const PRECISION = 10; /** >
< * @var integer
> * @var int > * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
*/
< static public $precision = 10;
> public static $precision = self::PRECISION;
/** * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ * * @var array
> * @phpstan-var array<string, array<string, float|int>>
*/
< static protected $unitTable = [
> protected static $unitTable = [
'in' => [ 'in' => 1, 'pc' => 6, 'pt' => 72, 'px' => 96, 'cm' => 2.54, 'mm' => 25.4, 'q' => 101.6, ], 'turn' => [ 'deg' => 360, 'grad' => 400, 'rad' => 6.28318530717958647692528676, // 2 * M_PI 'turn' => 1, ], 's' => [ 's' => 1, 'ms' => 1000, ], 'Hz' => [ 'Hz' => 1, 'kHz' => 0.001, ], 'dpi' => [ 'dpi' => 1,
< 'dpcm' => 2.54, < 'dppx' => 96,
> 'dpcm' => 1 / 2.54, > 'dppx' => 1 / 96,
], ]; /**
< * @var integer|float
> * @var int|float
*/
< public $dimension;
> private $dimension;
/**
< * @var array
> * @var string[] > * @phpstan-var list<string> > */ > private $numeratorUnits; > > /** > * @var string[] > * @phpstan-var list<string>
*/
< public $units;
> private $denominatorUnits;
/** * Initialize number *
< * @param mixed $dimension < * @param mixed $initialUnit < */ < public function __construct($dimension, $initialUnit) < { < $this->type = Type::T_NUMBER;
> * @param int|float $dimension > * @param string[]|string $numeratorUnits > * @param string[] $denominatorUnits > * > * @phpstan-param list<string>|string $numeratorUnits > * @phpstan-param list<string> $denominatorUnits > */ > public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) > { > if (is_string($numeratorUnits)) { > $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : []; > } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) { > // TODO get rid of this once `$number[2]` is not used anymore > $denominatorUnits = $numeratorUnits['denominator_units']; > $numeratorUnits = $numeratorUnits['numerator_units']; > } >
$this->dimension = $dimension;
< $this->units = is_array($initialUnit) < ? $initialUnit < : ($initialUnit ? [$initialUnit => 1] < : []);
> $this->numeratorUnits = $numeratorUnits; > $this->denominatorUnits = $denominatorUnits;
} /**
< * Coerce number to target units < * < * @param array $units < * < * @return \ScssPhp\ScssPhp\Node\Number
> * @return float|int
*/
< public function coerce($units)
> public function getDimension()
{
< if ($this->unitless()) { < return new Number($this->dimension, $units); < } < < $dimension = $this->dimension; < < foreach (static::$unitTable['in'] as $unit => $conv) { < $from = isset($this->units[$unit]) ? $this->units[$unit] : 0; < $to = isset($units[$unit]) ? $units[$unit] : 0; < $factor = pow($conv, $from - $to); < $dimension /= $factor;
> return $this->dimension;
}
< return new Number($dimension, $units);
> /** > * @return string[] > */ > public function getNumeratorUnits() > { > return $this->numeratorUnits;
} /**
< * Normalize number < * < * @return \ScssPhp\ScssPhp\Node\Number
> * @return string[]
*/
< public function normalize()
> public function getDenominatorUnits()
{
< $dimension = $this->dimension; < $units = []; < < $this->normalizeUnits($dimension, $units, 'in'); < < return new Number($dimension, $units);
> return $this->denominatorUnits;
} /**
< * {@inheritdoc}
> * @return bool
*/
> #[\ReturnTypeWillChange]
public function offsetExists($offset) { if ($offset === -3) {
< return ! is_null($this->sourceColumn);
> return ! \is_null($this->sourceColumn);
} if ($offset === -2) {
< return ! is_null($this->sourceLine);
> return ! \is_null($this->sourceLine);
}
< if ($offset === -1 ||
> if ( > $offset === -1 ||
$offset === 0 || $offset === 1 || $offset === 2 ) { return true; } return false; } /**
< * {@inheritdoc}
> * @return mixed
*/
> #[\ReturnTypeWillChange]
public function offsetGet($offset) { switch ($offset) { case -3: return $this->sourceColumn; case -2: return $this->sourceLine; case -1: return $this->sourceIndex; case 0:
< return $this->type;
> return Type::T_NUMBER;
case 1: return $this->dimension; case 2:
< return $this->units;
> return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits);
} } /**
< * {@inheritdoc}
> * @return void
*/
> #[\ReturnTypeWillChange]
public function offsetSet($offset, $value) {
< if ($offset === 1) { < $this->dimension = $value; < } elseif ($offset === 2) { < $this->units = $value; < } elseif ($offset == -1) { < $this->sourceIndex = $value; < } elseif ($offset == -2) { < $this->sourceLine = $value; < } elseif ($offset == -3) { < $this->sourceColumn = $value; < }
> throw new \BadMethodCallException('Number is immutable');
} /**
< * {@inheritdoc}
> * @return void
*/
> #[\ReturnTypeWillChange]
public function offsetUnset($offset) {
< if ($offset === 1) { < $this->dimension = null; < } elseif ($offset === 2) { < $this->units = null; < } elseif ($offset === -1) { < $this->sourceIndex = null; < } elseif ($offset === -2) { < $this->sourceLine = null; < } elseif ($offset === -3) { < $this->sourceColumn = null; < }
> throw new \BadMethodCallException('Number is immutable');
} /** * Returns true if the number is unitless *
< * @return boolean
> * @return bool
*/ public function unitless() {
< return ! array_sum($this->units);
> return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; > } > > /** > * Returns true if the number has any units > * > * @return bool > */ > public function hasUnits() > { > return !$this->unitless(); > } > > /** > * Checks whether the number has exactly this unit > * > * @param string $unit > * > * @return bool > */ > public function hasUnit($unit) > { > return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
} /** * Returns unit(s) as the product of numerator units divided by the product of denominator units * * @return string */ public function unitStr() {
< $numerators = []; < $denominators = [];
> if ($this->unitless()) { > return ''; > }
< foreach ($this->units as $unit => $unitSize) { < if ($unitSize > 0) { < $numerators = array_pad($numerators, count($numerators) + $unitSize, $unit); < continue;
> return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
}
< if ($unitSize < 0) { < $denominators = array_pad($denominators, count($denominators) + $unitSize, $unit); < continue;
> /** > * @param float|int $min > * @param float|int $max > * @param string|null $name > * > * @return float|int > * @throws SassScriptException > */ > public function valueInRange($min, $max, $name = null) > { > try { > return Util::checkRange('', new Range($min, $max), $this); > } catch (RangeException $e) { > throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name); > } > } > > /** > * @param float|int $min > * @param float|int $max > * @param string $name > * @param string $unit > * > * @return float|int > * @throws SassScriptException > * > * @internal > */ > public function valueInRangeWithUnit($min, $max, $name, $unit) > { > try { > return Util::checkRange('', new Range($min, $max), $this); > } catch (RangeException $e) { > throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name); > } > } > > /** > * @param string|null $varName > * > * @return void > */ > public function assertNoUnits($varName = null) > { > if ($this->unitless()) { > return; > } > > throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName); > } > > /** > * @param string $unit > * @param string|null $varName > * > * @return void > */ > public function assertUnit($unit, $varName = null) > { > if ($this->hasUnit($unit)) { > return; > } > > throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName); > } > > /** > * @param Number $other > * > * @return void > */ > public function assertSameUnitOrUnitless(Number $other) > { > if ($other->unitless()) { > return; > } > > if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) { > return; > } > > throw new SassScriptException(sprintf( > 'Incompatible units %s and %s.', > self::getUnitString($this->numeratorUnits, $this->denominatorUnits), > self::getUnitString($other->numeratorUnits, $other->denominatorUnits) > )); > } > > /** > * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. > * > * This does not throw an error if this number is unitless and > * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead, > * it treats all unitless numbers as convertible to and from all units without > * changing the value. > * > * @param string[] $newNumeratorUnits > * @param string[] $newDenominatorUnits > * > * @return Number > * > * @phpstan-param list<string> $newNumeratorUnits > * @phpstan-param list<string> $newDenominatorUnits > * > * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits > */ > public function coerce(array $newNumeratorUnits, array $newDenominatorUnits) > { > return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits); > } > > /** > * @param Number $other > * > * @return bool > */ > public function isComparableTo(Number $other) > { > if ($this->unitless() || $other->unitless()) { > return true; > } > > try { > $this->greaterThan($other); > return true; > } catch (SassScriptException $e) { > return false; > } > } > > /** > * @param Number $other > * > * @return bool > */ > public function lessThan(Number $other) > { > return $this->coerceUnits($other, function ($num1, $num2) { > return $num1 < $num2; > }); > } > > /** > * @param Number $other > * > * @return bool > */ > public function lessThanOrEqual(Number $other) > { > return $this->coerceUnits($other, function ($num1, $num2) { > return $num1 <= $num2; > }); > } > > /** > * @param Number $other > * > * @return bool > */ > public function greaterThan(Number $other) > { > return $this->coerceUnits($other, function ($num1, $num2) { > return $num1 > $num2; > }); > } > > /** > * @param Number $other > * > * @return bool > */ > public function greaterThanOrEqual(Number $other) > { > return $this->coerceUnits($other, function ($num1, $num2) { > return $num1 >= $num2; > }); > } > > /** > * @param Number $other > * > * @return Number > */ > public function plus(Number $other) > { > return $this->coerceNumber($other, function ($num1, $num2) { > return $num1 + $num2; > }); > } > > /** > * @param Number $other > * > * @return Number > */ > public function minus(Number $other) > { > return $this->coerceNumber($other, function ($num1, $num2) { > return $num1 - $num2; > }); > } > > /** > * @return Number > */ > public function unaryMinus() > { > return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits); > } > > /** > * @param Number $other > * > * @return Number > */ > public function modulo(Number $other) > { > return $this->coerceNumber($other, function ($num1, $num2) { > if ($num2 == 0) { > return NAN; > } > > $result = fmod($num1, $num2); > > if ($result == 0) { > return 0; > } > > if ($num2 < 0 xor $num1 < 0) { > $result += $num2; > } > > return $result; > });
}
> } > /** > * @param Number $other return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : ''); > * } > * @return Number > */ /** > public function times(Number $other) * Output number > { * > return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
< return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : '');
> /** > * @param Number $other > * > * @return Number > */ > public function dividedBy(Number $other) > { > if ($other->dimension == 0) { > if ($this->dimension == 0) { > $value = NAN; > } elseif ($this->dimension > 0) { > $value = INF; > } else { > $value = -INF; > } > } else { > $value = $this->dimension / $other->dimension; > } > > return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits); > } > > /** > * @param Number $other > * > * @return bool > */ > public function equals(Number $other) > { > // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here. > if ($this->unitless() !== $other->unitless()) { > return false; > } > > // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF > if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) { > return false; > } > > if ($this->unitless()) { > return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION); > } > > try { > return $this->coerceUnits($other, function ($num1, $num2) { > return round($num1,self::PRECISION) == round($num2, self::PRECISION); > }); > } catch (SassScriptException $e) { > return false; > }
* * @return string */ public function output(Compiler $compiler = null) {
< $dimension = round($this->dimension, static::$precision);
> $dimension = round($this->dimension, self::PRECISION);
< $units = array_filter($this->units, function ($unitSize) { < return $unitSize; < });
> if (is_nan($dimension)) { > return 'NaN'; > }
< if (count($units) > 1 && array_sum($units) === 0) { < $dimension = $this->dimension; < $units = []; < < $this->normalizeUnits($dimension, $units, 'in'); < < $dimension = round($dimension, static::$precision); < $units = array_filter($units, function ($unitSize) { < return $unitSize; < });
> if ($dimension === INF) { > return 'Infinity';
}
< $unitSize = array_sum($units);
> if ($dimension === -INF) { > return '-Infinity'; > }
< if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) { < $compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value.");
> if ($compiler) { > $unit = $this->unitStr(); > } elseif (isset($this->numeratorUnits[0])) { > $unit = $this->numeratorUnits[0]; > } else { > $unit = '';
}
< reset($units); < $unit = key($units); < $dimension = number_format($dimension, static::$precision, '.', '');
> $dimension = number_format($dimension, self::PRECISION, '.', '');
< return (static::$precision ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
> return rtrim(rtrim($dimension, '0'), '.') . $unit;
} /** * {@inheritdoc} */ public function __toString() { return $this->output(); } /**
< * Normalize units
> * @param Number $other > * @param callable $operation
*
< * @param integer|float $dimension < * @param array $units < * @param string $baseUnit
> * @return Number > * > * @phpstan-param callable(int|float, int|float): (int|float) $operation
*/
< private function normalizeUnits(&$dimension, &$units, $baseUnit = 'in')
> private function coerceNumber(Number $other, $operation)
{
< $dimension = $this->dimension; < $units = [];
> $result = $this->coerceUnits($other, $operation);
< foreach ($this->units as $unit => $exp) { < if (isset(static::$unitTable[$baseUnit][$unit])) { < $factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
> if (!$this->unitless()) { > return new Number($result, $this->numeratorUnits, $this->denominatorUnits); > }
< $unit = $baseUnit; < $dimension /= $factor;
> return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
}
< $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
> /** > * @param Number $other > * @param callable $operation > * > * @return mixed > * > * @phpstan-template T > * @phpstan-param callable(int|float, int|float): T $operation > * @phpstan-return T > */ > private function coerceUnits(Number $other, $operation) > { > if (!$this->unitless()) { > $num1 = $this->dimension; > $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits); > } else { > $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits); > $num2 = $other->dimension; > } > > return \call_user_func($operation, $num1, $num2); > } > > /** > * @param string[] $numeratorUnits > * @param string[] $denominatorUnits > * > * @return int|float > * > * @phpstan-param list<string> $numeratorUnits > * @phpstan-param list<string> $denominatorUnits > * > * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits > */ > private function valueInUnits(array $numeratorUnits, array $denominatorUnits) > { > if ( > $this->unitless() > || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0) > || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits) > ) { > return $this->dimension;
}
> } > $value = $this->dimension; } > $oldNumerators = $this->numeratorUnits; > > foreach ($numeratorUnits as $newNumerator) { > foreach ($oldNumerators as $key => $oldNumerator) { > $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); > > if (\is_null($conversionFactor)) { > continue; > } > > $value *= $conversionFactor; > unset($oldNumerators[$key]); > continue 2; > } > > throw new SassScriptException(sprintf( > 'Incompatible units %s and %s.', > self::getUnitString($this->numeratorUnits, $this->denominatorUnits), > self::getUnitString($numeratorUnits, $denominatorUnits) > )); > } > > $oldDenominators = $this->denominatorUnits; > > foreach ($denominatorUnits as $newDenominator) { > foreach ($oldDenominators as $key => $oldDenominator) { > $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); > > if (\is_null($conversionFactor)) { > continue; > } > > $value /= $conversionFactor; > unset($oldDenominators[$key]); > continue 2; > } > > throw new SassScriptException(sprintf( > 'Incompatible units %s and %s.', > self::getUnitString($this->numeratorUnits, $this->denominatorUnits), > self::getUnitString($numeratorUnits, $denominatorUnits) > )); > } > > if (\count($oldNumerators) || \count($oldDenominators)) { > throw new SassScriptException(sprintf( > 'Incompatible units %s and %s.', > self::getUnitString($this->numeratorUnits, $this->denominatorUnits), > self::getUnitString($numeratorUnits, $denominatorUnits) > )); > } > > return $value; > } > > /** > * @param int|float $value > * @param string[] $numerators1 > * @param string[] $denominators1 > * @param string[] $numerators2 > * @param string[] $denominators2 > * > * @return Number > * > * @phpstan-param list<string> $numerators1 > * @phpstan-param list<string> $denominators1 > * @phpstan-param list<string> $numerators2 > * @phpstan-param list<string> $denominators2 > */ > private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2) > { > $newNumerators = array(); > > foreach ($numerators1 as $numerator) { > foreach ($denominators2 as $key => $denominator) { > $conversionFactor = self::getConversionFactor($numerator, $denominator); > > if (\is_null($conversionFactor)) { > continue; > } > > $value /= $conversionFactor; > unset($denominators2[$key]); > continue 2; > } > > $newNumerators[] = $numerator; > } > > foreach ($numerators2 as $numerator) { > foreach ($denominators1 as $key => $denominator) { > $conversionFactor = self::getConversionFactor($numerator, $denominator); > > if (\is_null($conversionFactor)) { > continue; > } > > $value /= $conversionFactor; > unset($denominators1[$key]); > continue 2; > } > > $newNumerators[] = $numerator; > } > > $newDenominators = array_values(array_merge($denominators1, $denominators2)); > > return new Number($value, $newNumerators, $newDenominators); > } > > /** > * Returns the number of [unit1]s per [unit2]. > * > * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. > * > * @param string $unit1 > * @param string $unit2 > * > * @return float|int|null > */ > private static function getConversionFactor($unit1, $unit2) > { > if ($unit1 === $unit2) { > return 1; > } > > foreach (static::$unitTable as $unitVariants) { > if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { > return $unitVariants[$unit1] / $unitVariants[$unit2]; > } > } > > return null; > } > > /** > * Returns unit(s) as the product of numerator units divided by the product of denominator units > * > * @param string[] $numerators > * @param string[] $denominators > * > * @phpstan-param list<string> $numerators > * @phpstan-param list<string> $denominators > * > * @return string > */ > private static function getUnitString(array $numerators, array $denominators) > { > if (!\count($numerators)) { > if (\count($denominators) === 0) { > return 'no units'; > } > > if (\count($denominators) === 1) { > return $denominators[0] . '^-1'; > } > > return '(' . implode('*', $denominators) . ')^-1'; > } > > return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');