Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / api / scssphp / scssphp / src / Node / Number.php
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 private $numeratorUnits;
83 private $denominatorUnits;
84
85 /**
86 * Initialize number
87 *
88 * @param integer|float $dimension
89 * @param string[]|string $numeratorUnits
90 * @param string[] $denominatorUnits
91 */
92 public function __construct($dimension, $numeratorUnits, array $denominatorUnits = [])
93 {
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'];
100 }
101
102 $this->dimension = $dimension;
103 $this->numeratorUnits = $numeratorUnits;
104 $this->denominatorUnits = $denominatorUnits;
105 }
106
107 /**
108 * @return float|int
109 */
110 public function getDimension()
111 {
112 return $this->dimension;
113 }
114
115 /**
116 * @return string[]
117 */
118 public function getNumeratorUnits()
119 {
120 return $this->numeratorUnits;
121 }
122
123 /**
124 * @return string[]
125 */
126 public function getDenominatorUnits()
127 {
128 return $this->denominatorUnits;
129 }
130
131 /**
132 * {@inheritdoc}
133 */
134 public function offsetExists($offset)
135 {
136 if ($offset === -3) {
137 return ! \is_null($this->sourceColumn);
138 }
139
140 if ($offset === -2) {
141 return ! \is_null($this->sourceLine);
142 }
143
144 if (
145 $offset === -1 ||
146 $offset === 0 ||
147 $offset === 1 ||
148 $offset === 2
149 ) {
150 return true;
151 }
152
153 return false;
154 }
155
156 /**
157 * {@inheritdoc}
158 */
159 public function offsetGet($offset)
160 {
161 switch ($offset) {
162 case -3:
163 return $this->sourceColumn;
164
165 case -2:
166 return $this->sourceLine;
167
168 case -1:
169 return $this->sourceIndex;
170
171 case 0:
172 return Type::T_NUMBER;
173
174 case 1:
175 return $this->dimension;
176
177 case 2:
178 return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits);
179 }
180 }
181
182 /**
183 * {@inheritdoc}
184 */
185 public function offsetSet($offset, $value)
186 {
187 throw new \BadMethodCallException('Number is immutable');
188 }
189
190 /**
191 * {@inheritdoc}
192 */
193 public function offsetUnset($offset)
194 {
195 throw new \BadMethodCallException('Number is immutable');
196 }
197
198 /**
199 * Returns true if the number is unitless
200 *
201 * @return boolean
202 */
203 public function unitless()
204 {
205 return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0;
206 }
207
208 /**
209 * Checks whether the number has exactly this unit
210 *
211 * @param string $unit
212 *
213 * @return bool
214 */
215 public function hasUnit($unit)
216 {
217 return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
218 }
219
220 /**
221 * Returns unit(s) as the product of numerator units divided by the product of denominator units
222 *
223 * @return string
224 */
225 public function unitStr()
226 {
227 if ($this->unitless()) {
228 return '';
229 }
230
231 return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
232 }
233
234 public function assertNoUnits($varName = null)
235 {
236 if ($this->unitless()) {
237 return;
238 }
239
240 $varDisplay = !\is_null($varName) ? "\${$varName}: " : '';
241
242 throw new SassScriptException(sprintf('%sExpected %s to have no units', $varDisplay, $this));
243 }
244
245 public function assertSameUnitOrUnitless(Number $other)
246 {
247 if ($other->unitless()) {
248 return;
249 }
250
251 if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) {
252 return;
253 }
254
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)
259 ));
260 }
261
262 /**
263 * @param Number $other
264 *
265 * @return bool
266 */
267 public function isComparableTo(Number $other)
268 {
269 if ($this->unitless() || $other->unitless()) {
270 return true;
271 }
272
273 try {
274 $this->greaterThan($other);
275 return true;
276 } catch (SassScriptException $e) {
277 return false;
278 }
279 }
280
281 /**
282 * @param Number $other
283 *
284 * @return bool
285 */
286 public function lessThan(Number $other)
287 {
288 return $this->coerceUnits($other, function ($num1, $num2) {
289 return $num1 < $num2;
290 });
291 }
292
293 /**
294 * @param Number $other
295 *
296 * @return bool
297 */
298 public function lessThanOrEqual(Number $other)
299 {
300 return $this->coerceUnits($other, function ($num1, $num2) {
301 return $num1 <= $num2;
302 });
303 }
304
305 /**
306 * @param Number $other
307 *
308 * @return bool
309 */
310 public function greaterThan(Number $other)
311 {
312 return $this->coerceUnits($other, function ($num1, $num2) {
313 return $num1 > $num2;
314 });
315 }
316
317 /**
318 * @param Number $other
319 *
320 * @return bool
321 */
322 public function greaterThanOrEqual(Number $other)
323 {
324 return $this->coerceUnits($other, function ($num1, $num2) {
325 return $num1 >= $num2;
326 });
327 }
328
329 /**
330 * @param Number $other
331 *
332 * @return Number
333 */
334 public function plus(Number $other)
335 {
336 return $this->coerceNumber($other, function ($num1, $num2) {
337 return $num1 + $num2;
338 });
339 }
340
341 /**
342 * @param Number $other
343 *
344 * @return Number
345 */
346 public function minus(Number $other)
347 {
348 return $this->coerceNumber($other, function ($num1, $num2) {
349 return $num1 - $num2;
350 });
351 }
352
353 /**
354 * @return Number
355 */
356 public function unaryMinus()
357 {
358 return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits);
359 }
360
361 /**
362 * @param Number $other
363 *
364 * @return Number
365 */
366 public function modulo(Number $other)
367 {
368 return $this->coerceNumber($other, function ($num1, $num2) {
369 if ($num2 == 0) {
370 return NAN;
371 }
372
373 return $num1 % $num2;
374 });
375 }
376
377 /**
378 * @param Number $other
379 *
380 * @return Number
381 */
382 public function times(Number $other)
383 {
384 return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
385 }
386
387 /**
388 * @param Number $other
389 *
390 * @return Number
391 */
392 public function dividedBy(Number $other)
393 {
394 if ($other->dimension == 0) {
395 if ($this->dimension == 0) {
396 $value = NAN;
397 } elseif ($this->dimension > 0) {
398 $value = INF;
399 } else {
400 $value = -INF;
401 }
402 } else {
403 $value = $this->dimension / $other->dimension;
404 }
405
406 return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits);
407 }
408
409 /**
410 * @param Number $other
411 *
412 * @return bool
413 */
414 public function equals(Number $other)
415 {
416 // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
417 if ($this->unitless() !== $other->unitless()) {
418 return false;
419 }
420
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)) {
423 return false;
424 }
425
426 if ($this->unitless()) {
427 return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION);
428 }
429
430 try {
431 return $this->coerceUnits($other, function ($num1, $num2) {
432 return round($num1,self::PRECISION) == round($num2, self::PRECISION);
433 });
434 } catch (SassScriptException $e) {
435 return false;
436 }
437 }
438
439 /**
440 * Output number
441 *
442 * @param \ScssPhp\ScssPhp\Compiler $compiler
443 *
444 * @return string
445 */
446 public function output(Compiler $compiler = null)
447 {
448 $dimension = round($this->dimension, self::PRECISION);
449
450 if (is_nan($dimension)) {
451 return 'NaN';
452 }
453
454 if ($dimension === INF) {
455 return 'Infinity';
456 }
457
458 if ($dimension === -INF) {
459 return '-Infinity';
460 }
461
462 if ($compiler) {
463 $unit = $this->unitStr();
464 } elseif (isset($this->numeratorUnits[0])) {
465 $unit = $this->numeratorUnits[0];
466 } else {
467 $unit = '';
468 }
469
470 $dimension = number_format($dimension, self::PRECISION, '.', '');
471
472 return rtrim(rtrim($dimension, '0'), '.') . $unit;
473 }
474
475 /**
476 * {@inheritdoc}
477 */
478 public function __toString()
479 {
480 return $this->output();
481 }
482
483 /**
484 * @param Number $other
485 * @param callable $operation
486 *
487 * @return Number
488 *
489 * @phpstan-param callable(int|float, int|float): int|float $operation
490 */
491 private function coerceNumber(Number $other, $operation)
492 {
493 $result = $this->coerceUnits($other, $operation);
494
495 if (!$this->unitless()) {
496 return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
497 }
498
499 return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
500 }
501
502 /**
503 * @param Number $other
504 * @param callable $operation
505 *
506 * @return mixed
507 *
508 * @phpstan-template T
509 * @phpstan-param callable(int|float, int|float): T $operation
510 * @phpstan-return T
511 */
512 private function coerceUnits(Number $other, $operation)
513 {
514 if (!$this->unitless()) {
515 $num1 = $this->dimension;
516 $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits);
517 } else {
518 $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits);
519 $num2 = $other->dimension;
520 }
521
522 return \call_user_func($operation, $num1, $num2);
523 }
524
525 /**
526 * @param string[] $numeratorUnits
527 * @param string[] $denominatorUnits
528 *
529 * @return int|float
530 */
531 private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
532 {
533 if (
534 $this->unitless()
535 || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0)
536 || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits)
537 ) {
538 return $this->dimension;
539 }
540
541 $value = $this->dimension;
542 $oldNumerators = $this->numeratorUnits;
543
544 foreach ($numeratorUnits as $newNumerator) {
545 foreach ($oldNumerators as $key => $oldNumerator) {
546 $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator);
547
548 if (\is_null($conversionFactor)) {
549 continue;
550 }
551
552 $value *= $conversionFactor;
553 unset($oldNumerators[$key]);
554 continue 2;
555 }
556
557 throw new SassScriptException(sprintf(
558 'Incompatible units %s and %s.',
559 self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
560 self::getUnitString($numeratorUnits, $denominatorUnits)
561 ));
562 }
563
564 $oldDenominators = $this->denominatorUnits;
565
566 foreach ($denominatorUnits as $newDenominator) {
567 foreach ($oldDenominators as $key => $oldDenominator) {
568 $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator);
569
570 if (\is_null($conversionFactor)) {
571 continue;
572 }
573
574 $value /= $conversionFactor;
575 unset($oldDenominators[$key]);
576 continue 2;
577 }
578
579 throw new SassScriptException(sprintf(
580 'Incompatible units %s and %s.',
581 self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
582 self::getUnitString($numeratorUnits, $denominatorUnits)
583 ));
584 }
585
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)
591 ));
592 }
593
594 return $value;
595 }
596
597 /**
598 * @param int|float $value
599 * @param string[] $numerators1
600 * @param string[] $denominators1
601 * @param string[] $numerators2
602 * @param string[] $denominators2
603 *
604 * @return Number
605 */
606 private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
607 {
608 $newNumerators = array();
609
610 foreach ($numerators1 as $numerator) {
611 foreach ($denominators2 as $key => $denominator) {
612 $conversionFactor = self::getConversionFactor($numerator, $denominator);
613
614 if (\is_null($conversionFactor)) {
615 continue;
616 }
617
618 $value /= $conversionFactor;
619 unset($denominators2[$key]);
620 continue 2;
621 }
622
623 $newNumerators[] = $numerator;
624 }
625
626 foreach ($numerators2 as $numerator) {
627 foreach ($denominators1 as $key => $denominator) {
628 $conversionFactor = self::getConversionFactor($numerator, $denominator);
629
630 if (\is_null($conversionFactor)) {
631 continue;
632 }
633
634 $value /= $conversionFactor;
635 unset($denominators1[$key]);
636 continue 2;
637 }
638
639 $newNumerators[] = $numerator;
640 }
641
642 $newDenominators = array_values(array_merge($denominators1, $denominators2));
643
644 return new Number($value, $newNumerators, $newDenominators);
645 }
646
647 /**
648 * Returns the number of [unit1]s per [unit2].
649 *
650 * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
651 *
652 * @param string $unit1
653 * @param string $unit2
654 *
655 * @return float|int|null
656 */
657 private static function getConversionFactor($unit1, $unit2)
658 {
659 if ($unit1 === $unit2) {
660 return 1;
661 }
662
663 foreach (static::$unitTable as $unitVariants) {
664 if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
665 return $unitVariants[$unit1] / $unitVariants[$unit2];
666 }
667 }
668
669 return null;
670 }
671
672 /**
673 * Returns unit(s) as the product of numerator units divided by the product of denominator units
674 *
675 * @param string[] $numerators
676 * @param string[] $denominators
677 *
678 * @return string
679 */
680 private static function getUnitString(array $numerators, array $denominators)
681 {
682 if (!\count($numerators)) {
683 if (\count($denominators) === 0) {
684 return 'no units';
685 }
686
687 if (\count($denominators) === 1) {
688 return $denominators[0] . '^-1';
689 }
690
691 return '(' . implode('*', $denominators) . ')^-1';
692 }
693
694 return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
695 }
696 }