6 * @copyright 2012-2020 Leaf Corcoran
8 * @license http://opensource.org/licenses/MIT MIT
10 * @link http://scssphp.github.io/scssphp
13 namespace ScssPhp\ScssPhp\Node
;
15 use ScssPhp\ScssPhp\Compiler
;
16 use ScssPhp\ScssPhp\Exception\SassScriptException
;
17 use ScssPhp\ScssPhp\Node
;
18 use ScssPhp\ScssPhp\Type
;
21 * Dimension + optional units
24 * This is a work-in-progress.
26 * The \ArrayAccess interface is temporary until the migration is complete.
29 * @author Anthon Pang <anthon.pang@gmail.com>
31 class Number
extends Node
implements \ArrayAccess
37 * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
39 public static $precision = self
::PRECISION
;
42 * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
46 protected static $unitTable = [
59 'rad' => 6.28318530717958647692528676, // 2 * M_PI
82 private $numeratorUnits;
83 private $denominatorUnits;
88 * @param integer|float $dimension
89 * @param string[]|string $numeratorUnits
90 * @param string[] $denominatorUnits
92 public function __construct($dimension, $numeratorUnits, array $denominatorUnits = [])
94 if (is_string($numeratorUnits)) {
95 $numeratorUnits = $numeratorUnits ?
[$numeratorUnits] : [];
96 } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) {
97 // TODO get rid of this once `$number[2]` is not used anymore
98 $denominatorUnits = $numeratorUnits['denominator_units'];
99 $numeratorUnits = $numeratorUnits['numerator_units'];
102 $this->dimension
= $dimension;
103 $this->numeratorUnits
= $numeratorUnits;
104 $this->denominatorUnits
= $denominatorUnits;
110 public function getDimension()
112 return $this->dimension
;
118 public function getNumeratorUnits()
120 return $this->numeratorUnits
;
126 public function getDenominatorUnits()
128 return $this->denominatorUnits
;
134 public function offsetExists($offset)
136 if ($offset === -3) {
137 return ! \
is_null($this->sourceColumn
);
140 if ($offset === -2) {
141 return ! \
is_null($this->sourceLine
);
159 public function offsetGet($offset)
163 return $this->sourceColumn
;
166 return $this->sourceLine
;
169 return $this->sourceIndex
;
172 return Type
::T_NUMBER
;
175 return $this->dimension
;
178 return array('numerator_units' => $this->numeratorUnits
, 'denominator_units' => $this->denominatorUnits
);
185 public function offsetSet($offset, $value)
187 throw new \
BadMethodCallException('Number is immutable');
193 public function offsetUnset($offset)
195 throw new \
BadMethodCallException('Number is immutable');
199 * Returns true if the number is unitless
203 public function unitless()
205 return \
count($this->numeratorUnits
) === 0 && \
count($this->denominatorUnits
) === 0;
209 * Checks whether the number has exactly this unit
211 * @param string $unit
215 public function hasUnit($unit)
217 return \
count($this->numeratorUnits
) === 1 && \
count($this->denominatorUnits
) === 0 && $this->numeratorUnits
[0] === $unit;
221 * Returns unit(s) as the product of numerator units divided by the product of denominator units
225 public function unitStr()
227 if ($this->unitless()) {
231 return self
::getUnitString($this->numeratorUnits
, $this->denominatorUnits
);
234 public function assertNoUnits($varName = null)
236 if ($this->unitless()) {
240 $varDisplay = !\
is_null($varName) ?
"\${$varName}: " : '';
242 throw new SassScriptException(sprintf('%sExpected %s to have no units', $varDisplay, $this));
245 public function assertSameUnitOrUnitless(Number
$other)
247 if ($other->unitless()) {
251 if ($this->numeratorUnits
=== $other->numeratorUnits
&& $this->denominatorUnits
=== $other->denominatorUnits
) {
255 throw new SassScriptException(sprintf(
256 'Incompatible units %s and %s.',
257 self
::getUnitString($this->numeratorUnits
, $this->denominatorUnits
),
258 self
::getUnitString($other->numeratorUnits
, $other->denominatorUnits
)
263 * @param Number $other
267 public function isComparableTo(Number
$other)
269 if ($this->unitless() ||
$other->unitless()) {
274 $this->greaterThan($other);
276 } catch (SassScriptException
$e) {
282 * @param Number $other
286 public function lessThan(Number
$other)
288 return $this->coerceUnits($other, function ($num1, $num2) {
289 return $num1 < $num2;
294 * @param Number $other
298 public function lessThanOrEqual(Number
$other)
300 return $this->coerceUnits($other, function ($num1, $num2) {
301 return $num1 <= $num2;
306 * @param Number $other
310 public function greaterThan(Number
$other)
312 return $this->coerceUnits($other, function ($num1, $num2) {
313 return $num1 > $num2;
318 * @param Number $other
322 public function greaterThanOrEqual(Number
$other)
324 return $this->coerceUnits($other, function ($num1, $num2) {
325 return $num1 >= $num2;
330 * @param Number $other
334 public function plus(Number
$other)
336 return $this->coerceNumber($other, function ($num1, $num2) {
337 return $num1 +
$num2;
342 * @param Number $other
346 public function minus(Number
$other)
348 return $this->coerceNumber($other, function ($num1, $num2) {
349 return $num1 - $num2;
356 public function unaryMinus()
358 return new Number(-$this->dimension
, $this->numeratorUnits
, $this->denominatorUnits
);
362 * @param Number $other
366 public function modulo(Number
$other)
368 return $this->coerceNumber($other, function ($num1, $num2) {
373 return $num1 %
$num2;
378 * @param Number $other
382 public function times(Number
$other)
384 return $this->multiplyUnits($this->dimension
* $other->dimension
, $this->numeratorUnits
, $this->denominatorUnits
, $other->numeratorUnits
, $other->denominatorUnits
);
388 * @param Number $other
392 public function dividedBy(Number
$other)
394 if ($other->dimension
== 0) {
395 if ($this->dimension
== 0) {
397 } elseif ($this->dimension
> 0) {
403 $value = $this->dimension
/ $other->dimension
;
406 return $this->multiplyUnits($value, $this->numeratorUnits
, $this->denominatorUnits
, $other->denominatorUnits
, $other->numeratorUnits
);
410 * @param Number $other
414 public function equals(Number
$other)
416 // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
417 if ($this->unitless() !== $other->unitless()) {
421 // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF
422 if (is_nan($this->dimension
) ||
is_nan($other->dimension
) ||
!is_finite($this->dimension
) ||
!is_finite($other->dimension
)) {
426 if ($this->unitless()) {
427 return round($this->dimension
, self
::PRECISION
) == round($other->dimension
, self
::PRECISION
);
431 return $this->coerceUnits($other, function ($num1, $num2) {
432 return round($num1,self
::PRECISION
) == round($num2, self
::PRECISION
);
434 } catch (SassScriptException
$e) {
442 * @param \ScssPhp\ScssPhp\Compiler $compiler
446 public function output(Compiler
$compiler = null)
448 $dimension = round($this->dimension
, self
::PRECISION
);
450 if (is_nan($dimension)) {
454 if ($dimension === INF
) {
458 if ($dimension === -INF
) {
463 $unit = $this->unitStr();
464 } elseif (isset($this->numeratorUnits
[0])) {
465 $unit = $this->numeratorUnits
[0];
470 $dimension = number_format($dimension, self
::PRECISION
, '.', '');
472 return rtrim(rtrim($dimension, '0'), '.') . $unit;
478 public function __toString()
480 return $this->output();
484 * @param Number $other
485 * @param callable $operation
489 * @phpstan-param callable(int|float, int|float): int|float $operation
491 private function coerceNumber(Number
$other, $operation)
493 $result = $this->coerceUnits($other, $operation);
495 if (!$this->unitless()) {
496 return new Number($result, $this->numeratorUnits
, $this->denominatorUnits
);
499 return new Number($result, $other->numeratorUnits
, $other->denominatorUnits
);
503 * @param Number $other
504 * @param callable $operation
508 * @phpstan-template T
509 * @phpstan-param callable(int|float, int|float): T $operation
512 private function coerceUnits(Number
$other, $operation)
514 if (!$this->unitless()) {
515 $num1 = $this->dimension
;
516 $num2 = $other->valueInUnits($this->numeratorUnits
, $this->denominatorUnits
);
518 $num1 = $this->valueInUnits($other->numeratorUnits
, $other->denominatorUnits
);
519 $num2 = $other->dimension
;
522 return \
call_user_func($operation, $num1, $num2);
526 * @param string[] $numeratorUnits
527 * @param string[] $denominatorUnits
531 private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
535 ||
(\
count($numeratorUnits) === 0 && \
count($denominatorUnits) === 0)
536 ||
($this->numeratorUnits
=== $numeratorUnits && $this->denominatorUnits
=== $denominatorUnits)
538 return $this->dimension
;
541 $value = $this->dimension
;
542 $oldNumerators = $this->numeratorUnits
;
544 foreach ($numeratorUnits as $newNumerator) {
545 foreach ($oldNumerators as $key => $oldNumerator) {
546 $conversionFactor = self
::getConversionFactor($newNumerator, $oldNumerator);
548 if (\
is_null($conversionFactor)) {
552 $value *= $conversionFactor;
553 unset($oldNumerators[$key]);
557 throw new SassScriptException(sprintf(
558 'Incompatible units %s and %s.',
559 self
::getUnitString($this->numeratorUnits
, $this->denominatorUnits
),
560 self
::getUnitString($numeratorUnits, $denominatorUnits)
564 $oldDenominators = $this->denominatorUnits
;
566 foreach ($denominatorUnits as $newDenominator) {
567 foreach ($oldDenominators as $key => $oldDenominator) {
568 $conversionFactor = self
::getConversionFactor($newDenominator, $oldDenominator);
570 if (\
is_null($conversionFactor)) {
574 $value /= $conversionFactor;
575 unset($oldDenominators[$key]);
579 throw new SassScriptException(sprintf(
580 'Incompatible units %s and %s.',
581 self
::getUnitString($this->numeratorUnits
, $this->denominatorUnits
),
582 self
::getUnitString($numeratorUnits, $denominatorUnits)
586 if (\
count($oldNumerators) || \
count($oldDenominators)) {
587 throw new SassScriptException(sprintf(
588 'Incompatible units %s and %s.',
589 self
::getUnitString($this->numeratorUnits
, $this->denominatorUnits
),
590 self
::getUnitString($numeratorUnits, $denominatorUnits)
598 * @param int|float $value
599 * @param string[] $numerators1
600 * @param string[] $denominators1
601 * @param string[] $numerators2
602 * @param string[] $denominators2
606 private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
608 $newNumerators = array();
610 foreach ($numerators1 as $numerator) {
611 foreach ($denominators2 as $key => $denominator) {
612 $conversionFactor = self
::getConversionFactor($numerator, $denominator);
614 if (\
is_null($conversionFactor)) {
618 $value /= $conversionFactor;
619 unset($denominators2[$key]);
623 $newNumerators[] = $numerator;
626 foreach ($numerators2 as $numerator) {
627 foreach ($denominators1 as $key => $denominator) {
628 $conversionFactor = self
::getConversionFactor($numerator, $denominator);
630 if (\
is_null($conversionFactor)) {
634 $value /= $conversionFactor;
635 unset($denominators1[$key]);
639 $newNumerators[] = $numerator;
642 $newDenominators = array_values(array_merge($denominators1, $denominators2));
644 return new Number($value, $newNumerators, $newDenominators);
648 * Returns the number of [unit1]s per [unit2].
650 * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
652 * @param string $unit1
653 * @param string $unit2
655 * @return float|int|null
657 private static function getConversionFactor($unit1, $unit2)
659 if ($unit1 === $unit2) {
663 foreach (static::$unitTable as $unitVariants) {
664 if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
665 return $unitVariants[$unit1] / $unitVariants[$unit2];
673 * Returns unit(s) as the product of numerator units divided by the product of denominator units
675 * @param string[] $numerators
676 * @param string[] $denominators
680 private static function getUnitString(array $numerators, array $denominators)
682 if (!\
count($numerators)) {
683 if (\
count($denominators) === 0) {
687 if (\
count($denominators) === 1) {
688 return $denominators[0] . '^-1';
691 return '(' . implode('*', $denominators) . ')^-1';
694 return implode('*', $numerators) . (\
count($denominators) ?
'/' . implode('*', $denominators) : '');