See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body