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