<?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) : '');