Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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\Compiler; 16 use ScssPhp\ScssPhp\Exception\SassScriptException; 17 use ScssPhp\ScssPhp\Node; 18 use ScssPhp\ScssPhp\Type; 19 20 /** 21 * Dimension + optional units 22 * 23 * {@internal 24 * This is a work-in-progress. 25 * 26 * The \ArrayAccess interface is temporary until the migration is complete. 27 * }} 28 * 29 * @author Anthon Pang <anthon.pang@gmail.com> 30 */ 31 class Number extends Node implements \ArrayAccess 32 { 33 const PRECISION = 10; 34 35 /** 36 * @var integer 37 * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore. 38 */ 39 public static $precision = self::PRECISION; 40 41 /** 42 * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ 43 * 44 * @var array 45 */ 46 protected static $unitTable = [ 47 'in' => [ 48 'in' => 1, 49 'pc' => 6, 50 'pt' => 72, 51 'px' => 96, 52 'cm' => 2.54, 53 'mm' => 25.4, 54 'q' => 101.6, 55 ], 56 'turn' => [ 57 'deg' => 360, 58 'grad' => 400, 59 'rad' => 6.28318530717958647692528676, // 2 * M_PI 60 'turn' => 1, 61 ], 62 's' => [ 63 's' => 1, 64 'ms' => 1000, 65 ], 66 'Hz' => [ 67 'Hz' => 1, 68 'kHz' => 0.001, 69 ], 70 'dpi' => [ 71 'dpi' => 1, 72 'dpcm' => 1 / 2.54, 73 'dppx' => 1 / 96, 74 ], 75 ]; 76 77 /** 78 * @var integer|float 79 */ 80 private $dimension; 81 82 /** 83 * @var string[] 84 * @phpstan-var list<string> 85 */ 86 private $numeratorUnits; 87 88 /** 89 * @var string[] 90 * @phpstan-var list<string> 91 */ 92 private $denominatorUnits; 93 94 /** 95 * Initialize number 96 * 97 * @param integer|float $dimension 98 * @param string[]|string $numeratorUnits 99 * @param string[] $denominatorUnits 100 * 101 * @phpstan-param list<string>|string $numeratorUnits 102 * @phpstan-param list<string> $denominatorUnits 103 */ 104 public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) 105 { 106 if (is_string($numeratorUnits)) { 107 $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : []; 108 } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) { 109 // TODO get rid of this once `$number[2]` is not used anymore 110 $denominatorUnits = $numeratorUnits['denominator_units']; 111 $numeratorUnits = $numeratorUnits['numerator_units']; 112 } 113 114 $this->dimension = $dimension; 115 $this->numeratorUnits = $numeratorUnits; 116 $this->denominatorUnits = $denominatorUnits; 117 } 118 119 /** 120 * @return float|int 121 */ 122 public function getDimension() 123 { 124 return $this->dimension; 125 } 126 127 /** 128 * @return string[] 129 */ 130 public function getNumeratorUnits() 131 { 132 return $this->numeratorUnits; 133 } 134 135 /** 136 * @return string[] 137 */ 138 public function getDenominatorUnits() 139 { 140 return $this->denominatorUnits; 141 } 142 143 /** 144 * {@inheritdoc} 145 */ 146 public function offsetExists($offset) 147 { 148 if ($offset === -3) { 149 return ! \is_null($this->sourceColumn); 150 } 151 152 if ($offset === -2) { 153 return ! \is_null($this->sourceLine); 154 } 155 156 if ( 157 $offset === -1 || 158 $offset === 0 || 159 $offset === 1 || 160 $offset === 2 161 ) { 162 return true; 163 } 164 165 return false; 166 } 167 168 /** 169 * {@inheritdoc} 170 */ 171 public function offsetGet($offset) 172 { 173 switch ($offset) { 174 case -3: 175 return $this->sourceColumn; 176 177 case -2: 178 return $this->sourceLine; 179 180 case -1: 181 return $this->sourceIndex; 182 183 case 0: 184 return Type::T_NUMBER; 185 186 case 1: 187 return $this->dimension; 188 189 case 2: 190 return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits); 191 } 192 } 193 194 /** 195 * {@inheritdoc} 196 */ 197 public function offsetSet($offset, $value) 198 { 199 throw new \BadMethodCallException('Number is immutable'); 200 } 201 202 /** 203 * {@inheritdoc} 204 */ 205 public function offsetUnset($offset) 206 { 207 throw new \BadMethodCallException('Number is immutable'); 208 } 209 210 /** 211 * Returns true if the number is unitless 212 * 213 * @return boolean 214 */ 215 public function unitless() 216 { 217 return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; 218 } 219 220 /** 221 * Checks whether the number has exactly this unit 222 * 223 * @param string $unit 224 * 225 * @return bool 226 */ 227 public function hasUnit($unit) 228 { 229 return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit; 230 } 231 232 /** 233 * Returns unit(s) as the product of numerator units divided by the product of denominator units 234 * 235 * @return string 236 */ 237 public function unitStr() 238 { 239 if ($this->unitless()) { 240 return ''; 241 } 242 243 return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); 244 } 245 246 /** 247 * @param string|null $varName 248 * 249 * @return void 250 */ 251 public function assertNoUnits($varName = null) 252 { 253 if ($this->unitless()) { 254 return; 255 } 256 257 throw SassScriptException::forArgument(sprintf('Expected %s to have no units', $this), $varName); 258 } 259 260 /** 261 * @param Number $other 262 * 263 * @return void 264 */ 265 public function assertSameUnitOrUnitless(Number $other) 266 { 267 if ($other->unitless()) { 268 return; 269 } 270 271 if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) { 272 return; 273 } 274 275 throw new SassScriptException(sprintf( 276 'Incompatible units %s and %s.', 277 self::getUnitString($this->numeratorUnits, $this->denominatorUnits), 278 self::getUnitString($other->numeratorUnits, $other->denominatorUnits) 279 )); 280 } 281 282 /** 283 * @param Number $other 284 * 285 * @return bool 286 */ 287 public function isComparableTo(Number $other) 288 { 289 if ($this->unitless() || $other->unitless()) { 290 return true; 291 } 292 293 try { 294 $this->greaterThan($other); 295 return true; 296 } catch (SassScriptException $e) { 297 return false; 298 } 299 } 300 301 /** 302 * @param Number $other 303 * 304 * @return bool 305 */ 306 public function lessThan(Number $other) 307 { 308 return $this->coerceUnits($other, function ($num1, $num2) { 309 return $num1 < $num2; 310 }); 311 } 312 313 /** 314 * @param Number $other 315 * 316 * @return bool 317 */ 318 public function lessThanOrEqual(Number $other) 319 { 320 return $this->coerceUnits($other, function ($num1, $num2) { 321 return $num1 <= $num2; 322 }); 323 } 324 325 /** 326 * @param Number $other 327 * 328 * @return bool 329 */ 330 public function greaterThan(Number $other) 331 { 332 return $this->coerceUnits($other, function ($num1, $num2) { 333 return $num1 > $num2; 334 }); 335 } 336 337 /** 338 * @param Number $other 339 * 340 * @return bool 341 */ 342 public function greaterThanOrEqual(Number $other) 343 { 344 return $this->coerceUnits($other, function ($num1, $num2) { 345 return $num1 >= $num2; 346 }); 347 } 348 349 /** 350 * @param Number $other 351 * 352 * @return Number 353 */ 354 public function plus(Number $other) 355 { 356 return $this->coerceNumber($other, function ($num1, $num2) { 357 return $num1 + $num2; 358 }); 359 } 360 361 /** 362 * @param Number $other 363 * 364 * @return Number 365 */ 366 public function minus(Number $other) 367 { 368 return $this->coerceNumber($other, function ($num1, $num2) { 369 return $num1 - $num2; 370 }); 371 } 372 373 /** 374 * @return Number 375 */ 376 public function unaryMinus() 377 { 378 return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits); 379 } 380 381 /** 382 * @param Number $other 383 * 384 * @return Number 385 */ 386 public function modulo(Number $other) 387 { 388 return $this->coerceNumber($other, function ($num1, $num2) { 389 if ($num2 == 0) { 390 return NAN; 391 } 392 393 $result = fmod($num1, $num2); 394 395 if ($result == 0) { 396 return 0; 397 } 398 399 if ($num2 < 0 xor $num1 < 0) { 400 $result += $num2; 401 } 402 403 return $result; 404 }); 405 } 406 407 /** 408 * @param Number $other 409 * 410 * @return Number 411 */ 412 public function times(Number $other) 413 { 414 return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits); 415 } 416 417 /** 418 * @param Number $other 419 * 420 * @return Number 421 */ 422 public function dividedBy(Number $other) 423 { 424 if ($other->dimension == 0) { 425 if ($this->dimension == 0) { 426 $value = NAN; 427 } elseif ($this->dimension > 0) { 428 $value = INF; 429 } else { 430 $value = -INF; 431 } 432 } else { 433 $value = $this->dimension / $other->dimension; 434 } 435 436 return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits); 437 } 438 439 /** 440 * @param Number $other 441 * 442 * @return bool 443 */ 444 public function equals(Number $other) 445 { 446 // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here. 447 if ($this->unitless() !== $other->unitless()) { 448 return false; 449 } 450 451 // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF 452 if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) { 453 return false; 454 } 455 456 if ($this->unitless()) { 457 return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION); 458 } 459 460 try { 461 return $this->coerceUnits($other, function ($num1, $num2) { 462 return round($num1,self::PRECISION) == round($num2, self::PRECISION); 463 }); 464 } catch (SassScriptException $e) { 465 return false; 466 } 467 } 468 469 /** 470 * Output number 471 * 472 * @param \ScssPhp\ScssPhp\Compiler $compiler 473 * 474 * @return string 475 */ 476 public function output(Compiler $compiler = null) 477 { 478 $dimension = round($this->dimension, self::PRECISION); 479 480 if (is_nan($dimension)) { 481 return 'NaN'; 482 } 483 484 if ($dimension === INF) { 485 return 'Infinity'; 486 } 487 488 if ($dimension === -INF) { 489 return '-Infinity'; 490 } 491 492 if ($compiler) { 493 $unit = $this->unitStr(); 494 } elseif (isset($this->numeratorUnits[0])) { 495 $unit = $this->numeratorUnits[0]; 496 } else { 497 $unit = ''; 498 } 499 500 $dimension = number_format($dimension, self::PRECISION, '.', ''); 501 502 return rtrim(rtrim($dimension, '0'), '.') . $unit; 503 } 504 505 /** 506 * {@inheritdoc} 507 */ 508 public function __toString() 509 { 510 return $this->output(); 511 } 512 513 /** 514 * @param Number $other 515 * @param callable $operation 516 * 517 * @return Number 518 * 519 * @phpstan-param callable(int|float, int|float): (int|float) $operation 520 */ 521 private function coerceNumber(Number $other, $operation) 522 { 523 $result = $this->coerceUnits($other, $operation); 524 525 if (!$this->unitless()) { 526 return new Number($result, $this->numeratorUnits, $this->denominatorUnits); 527 } 528 529 return new Number($result, $other->numeratorUnits, $other->denominatorUnits); 530 } 531 532 /** 533 * @param Number $other 534 * @param callable $operation 535 * 536 * @return mixed 537 * 538 * @phpstan-template T 539 * @phpstan-param callable(int|float, int|float): T $operation 540 * @phpstan-return T 541 */ 542 private function coerceUnits(Number $other, $operation) 543 { 544 if (!$this->unitless()) { 545 $num1 = $this->dimension; 546 $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits); 547 } else { 548 $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits); 549 $num2 = $other->dimension; 550 } 551 552 return \call_user_func($operation, $num1, $num2); 553 } 554 555 /** 556 * @param string[] $numeratorUnits 557 * @param string[] $denominatorUnits 558 * 559 * @return int|float 560 * 561 * @phpstan-param list<string> $numeratorUnits 562 * @phpstan-param list<string> $denominatorUnits 563 */ 564 private function valueInUnits(array $numeratorUnits, array $denominatorUnits) 565 { 566 if ( 567 $this->unitless() 568 || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0) 569 || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits) 570 ) { 571 return $this->dimension; 572 } 573 574 $value = $this->dimension; 575 $oldNumerators = $this->numeratorUnits; 576 577 foreach ($numeratorUnits as $newNumerator) { 578 foreach ($oldNumerators as $key => $oldNumerator) { 579 $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); 580 581 if (\is_null($conversionFactor)) { 582 continue; 583 } 584 585 $value *= $conversionFactor; 586 unset($oldNumerators[$key]); 587 continue 2; 588 } 589 590 throw new SassScriptException(sprintf( 591 'Incompatible units %s and %s.', 592 self::getUnitString($this->numeratorUnits, $this->denominatorUnits), 593 self::getUnitString($numeratorUnits, $denominatorUnits) 594 )); 595 } 596 597 $oldDenominators = $this->denominatorUnits; 598 599 foreach ($denominatorUnits as $newDenominator) { 600 foreach ($oldDenominators as $key => $oldDenominator) { 601 $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); 602 603 if (\is_null($conversionFactor)) { 604 continue; 605 } 606 607 $value /= $conversionFactor; 608 unset($oldDenominators[$key]); 609 continue 2; 610 } 611 612 throw new SassScriptException(sprintf( 613 'Incompatible units %s and %s.', 614 self::getUnitString($this->numeratorUnits, $this->denominatorUnits), 615 self::getUnitString($numeratorUnits, $denominatorUnits) 616 )); 617 } 618 619 if (\count($oldNumerators) || \count($oldDenominators)) { 620 throw new SassScriptException(sprintf( 621 'Incompatible units %s and %s.', 622 self::getUnitString($this->numeratorUnits, $this->denominatorUnits), 623 self::getUnitString($numeratorUnits, $denominatorUnits) 624 )); 625 } 626 627 return $value; 628 } 629 630 /** 631 * @param int|float $value 632 * @param string[] $numerators1 633 * @param string[] $denominators1 634 * @param string[] $numerators2 635 * @param string[] $denominators2 636 * 637 * @return Number 638 * 639 * @phpstan-param list<string> $numerators1 640 * @phpstan-param list<string> $denominators1 641 * @phpstan-param list<string> $numerators2 642 * @phpstan-param list<string> $denominators2 643 */ 644 private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2) 645 { 646 $newNumerators = array(); 647 648 foreach ($numerators1 as $numerator) { 649 foreach ($denominators2 as $key => $denominator) { 650 $conversionFactor = self::getConversionFactor($numerator, $denominator); 651 652 if (\is_null($conversionFactor)) { 653 continue; 654 } 655 656 $value /= $conversionFactor; 657 unset($denominators2[$key]); 658 continue 2; 659 } 660 661 $newNumerators[] = $numerator; 662 } 663 664 foreach ($numerators2 as $numerator) { 665 foreach ($denominators1 as $key => $denominator) { 666 $conversionFactor = self::getConversionFactor($numerator, $denominator); 667 668 if (\is_null($conversionFactor)) { 669 continue; 670 } 671 672 $value /= $conversionFactor; 673 unset($denominators1[$key]); 674 continue 2; 675 } 676 677 $newNumerators[] = $numerator; 678 } 679 680 $newDenominators = array_values(array_merge($denominators1, $denominators2)); 681 682 return new Number($value, $newNumerators, $newDenominators); 683 } 684 685 /** 686 * Returns the number of [unit1]s per [unit2]. 687 * 688 * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. 689 * 690 * @param string $unit1 691 * @param string $unit2 692 * 693 * @return float|int|null 694 */ 695 private static function getConversionFactor($unit1, $unit2) 696 { 697 if ($unit1 === $unit2) { 698 return 1; 699 } 700 701 foreach (static::$unitTable as $unitVariants) { 702 if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { 703 return $unitVariants[$unit1] / $unitVariants[$unit2]; 704 } 705 } 706 707 return null; 708 } 709 710 /** 711 * Returns unit(s) as the product of numerator units divided by the product of denominator units 712 * 713 * @param string[] $numerators 714 * @param string[] $denominators 715 * 716 * @phpstan-param list<string> $numerators 717 * @phpstan-param list<string> $denominators 718 * 719 * @return string 720 */ 721 private static function getUnitString(array $numerators, array $denominators) 722 { 723 if (!\count($numerators)) { 724 if (\count($denominators) === 0) { 725 return 'no units'; 726 } 727 728 if (\count($denominators) === 1) { 729 return $denominators[0] . '^-1'; 730 } 731 732 return '(' . implode('*', $denominators) . ')^-1'; 733 } 734 735 return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); 736 } 737 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body