Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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