Passed
Push — master ( d4a329...711fef )
by Jeroen De
03:36
created

Number::output()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 19
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 33
rs 8.4444
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\Node;
17
use ScssPhp\ScssPhp\Type;
18
19
/**
20
 * Dimension + optional units
21
 *
22
 * {@internal
23
 *     This is a work-in-progress.
24
 *
25
 *     The \ArrayAccess interface is temporary until the migration is complete.
26
 * }}
27
 *
28
 * @author Anthon Pang <[email protected]>
29
 */
30
class Number extends Node implements \ArrayAccess
31
{
32
    const PRECISION = 10;
33
34
    /**
35
     * @var integer
36
     * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
37
     */
38
    public static $precision = self::PRECISION;
39
40
    /**
41
     * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
42
     *
43
     * @var array
44
     */
45
    protected static $unitTable = [
46
        'in' => [
47
            'in' => 1,
48
            'pc' => 6,
49
            'pt' => 72,
50
            'px' => 96,
51
            'cm' => 2.54,
52
            'mm' => 25.4,
53
            'q'  => 101.6,
54
        ],
55
        'turn' => [
56
            'deg'  => 360,
57
            'grad' => 400,
58
            'rad'  => 6.28318530717958647692528676, // 2 * M_PI
59
            'turn' => 1,
60
        ],
61
        's' => [
62
            's'  => 1,
63
            'ms' => 1000,
64
        ],
65
        'Hz' => [
66
            'Hz'  => 1,
67
            'kHz' => 0.001,
68
        ],
69
        'dpi' => [
70
            'dpi'  => 1,
71
            'dpcm' => 1 / 2.54,
72
            'dppx' => 1 / 96,
73
        ],
74
    ];
75
76
    /**
77
     * @var integer|float
78
     */
79
    public $dimension;
80
81
    /**
82
     * @var array
83
     */
84
    public $units;
85
86
    /**
87
     * Initialize number
88
     *
89
     * @param mixed $dimension
90
     * @param mixed $initialUnit
91
     */
92
    public function __construct($dimension, $initialUnit)
93
    {
94
        $this->type      = Type::T_NUMBER;
95
        $this->dimension = $dimension;
96
        $this->units     = \is_array($initialUnit)
97
            ? $initialUnit
98
            : ($initialUnit ? [$initialUnit => 1]
99
                            : []);
100
    }
101
102
    /**
103
     * Coerce number to target units
104
     *
105
     * @param array $units
106
     *
107
     * @return \ScssPhp\ScssPhp\Node\Number
108
     */
109
    public function coerce($units)
110
    {
111
        if ($this->unitless()) {
112
            return new Number($this->dimension, $units);
113
        }
114
115
        $dimension = $this->dimension;
116
117
        if (\count($units)) {
118
            $baseUnit = array_keys($units);
119
            $baseUnit = reset($baseUnit);
120
            $baseUnit = $this->findBaseUnit($baseUnit);
121
            if ($baseUnit && isset(static::$unitTable[$baseUnit])) {
122
                foreach (static::$unitTable[$baseUnit] as $unit => $conv) {
123
                    $from       = isset($this->units[$unit]) ? $this->units[$unit] : 0;
124
                    $to         = isset($units[$unit]) ? $units[$unit] : 0;
125
                    $factor     = pow($conv, $from - $to);
126
                    $dimension /= $factor;
127
                }
128
            }
129
        }
130
131
        return new Number($dimension, $units);
132
    }
133
134
    /**
135
     * Normalize number
136
     *
137
     * @return \ScssPhp\ScssPhp\Node\Number
138
     */
139
    public function normalize()
140
    {
141
        $dimension = $this->dimension;
142
        $units     = [];
143
144
        $this->normalizeUnits($dimension, $units);
145
146
        return new Number($dimension, $units);
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function offsetExists($offset)
153
    {
154
        if ($offset === -3) {
155
            return ! \is_null($this->sourceColumn);
156
        }
157
158
        if ($offset === -2) {
159
            return ! \is_null($this->sourceLine);
160
        }
161
162
        if (
163
            $offset === -1 ||
164
            $offset === 0 ||
165
            $offset === 1 ||
166
            $offset === 2
167
        ) {
168
            return true;
169
        }
170
171
        return false;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function offsetGet($offset)
178
    {
179
        switch ($offset) {
180
            case -3:
181
                return $this->sourceColumn;
182
183
            case -2:
184
                return $this->sourceLine;
185
186
            case -1:
187
                return $this->sourceIndex;
188
189
            case 0:
190
                return $this->type;
191
192
            case 1:
193
                return $this->dimension;
194
195
            case 2:
196
                return $this->units;
197
        }
198
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203
    public function offsetSet($offset, $value)
204
    {
205
        if ($offset === 1) {
206
            $this->dimension = $value;
207
        } elseif ($offset === 2) {
208
            $this->units = $value;
209
        } elseif ($offset == -1) {
210
            $this->sourceIndex = $value;
211
        } elseif ($offset == -2) {
212
            $this->sourceLine = $value;
213
        } elseif ($offset == -3) {
214
            $this->sourceColumn = $value;
215
        }
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function offsetUnset($offset)
222
    {
223
        if ($offset === 1) {
224
            $this->dimension = null;
225
        } elseif ($offset === 2) {
226
            $this->units = null;
227
        } elseif ($offset === -1) {
228
            $this->sourceIndex = null;
229
        } elseif ($offset === -2) {
230
            $this->sourceLine = null;
231
        } elseif ($offset === -3) {
232
            $this->sourceColumn = null;
233
        }
234
    }
235
236
    /**
237
     * Returns true if the number is unitless
238
     *
239
     * @return boolean
240
     */
241
    public function unitless()
242
    {
243
        return ! array_sum($this->units);
244
    }
245
246
    /**
247
     * Test if a number can be normalized in a base unit
248
     * ie if its units are homogeneous
249
     *
250
     * @return boolean
251
     */
252
    public function isNormalizable()
253
    {
254
        if ($this->unitless()) {
255
            return false;
256
        }
257
258
        $baseUnit = null;
259
260
        foreach ($this->units as $unit => $exp) {
261
            $b = $this->findBaseUnit($unit);
262
263
            if (\is_null($baseUnit)) {
264
                $baseUnit = $b;
265
            }
266
267
            if (\is_null($b) or $b !== $baseUnit) {
268
                return false;
269
            }
270
        }
271
272
        return $baseUnit;
273
    }
274
275
    /**
276
     * Returns unit(s) as the product of numerator units divided by the product of denominator units
277
     *
278
     * @return string
279
     */
280
    public function unitStr()
281
    {
282
        $numerators   = [];
283
        $denominators = [];
284
285
        foreach ($this->units as $unit => $unitSize) {
286
            if ($unitSize > 0) {
287
                $numerators = array_pad($numerators, \count($numerators) + $unitSize, $unit);
288
                continue;
289
            }
290
291
            if ($unitSize < 0) {
292
                $denominators = array_pad($denominators, \count($denominators) - $unitSize, $unit);
293
                continue;
294
            }
295
        }
296
297
        return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
298
    }
299
300
    /**
301
     * Output number
302
     *
303
     * @param \ScssPhp\ScssPhp\Compiler $compiler
304
     *
305
     * @return string
306
     */
307
    public function output(Compiler $compiler = null)
308
    {
309
        $dimension = round($this->dimension, self::PRECISION);
310
311
        $units = array_filter($this->units, function ($unitSize) {
312
            return $unitSize;
313
        });
314
315
        if (\count($units) > 1 && array_sum($units) === 0) {
316
            $dimension = $this->dimension;
317
            $units     = [];
318
319
            $this->normalizeUnits($dimension, $units);
320
321
            $dimension = round($dimension, self::PRECISION);
322
            $units     = array_filter($units, function ($unitSize) {
323
                return $unitSize;
324
            });
325
        }
326
327
        $unitSize = array_sum($units);
328
329
        if ($compiler && ($unitSize > 1 || $unitSize < 0 || \count($units) > 1)) {
330
            $this->units = $units;
331
            $unit = $this->unitStr();
332
        } else {
333
            reset($units);
334
            $unit = key($units);
335
        }
336
337
        $dimension = number_format($dimension, self::PRECISION, '.', '');
338
339
        return (self::PRECISION ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345
    public function __toString()
346
    {
347
        return $this->output();
348
    }
349
350
    /**
351
     * Normalize units
352
     *
353
     * @param integer|float $dimension
354
     * @param array         $units
355
     * @param string        $baseUnit
356
     */
357
    private function normalizeUnits(&$dimension, &$units, $baseUnit = null)
358
    {
359
        $dimension = $this->dimension;
360
        $units     = [];
361
362
        foreach ($this->units as $unit => $exp) {
363
            if (! $baseUnit) {
364
                $baseUnit = $this->findBaseUnit($unit);
365
            }
366
367
            if ($baseUnit && isset(static::$unitTable[$baseUnit][$unit])) {
368
                $factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
369
370
                $unit = $baseUnit;
371
                $dimension /= $factor;
372
            }
373
374
            $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
375
        }
376
    }
377
378
    /**
379
     * Find the base unit family for a given unit
380
     *
381
     * @param string $unit
382
     *
383
     * @return string|null
384
     */
385
    private function findBaseUnit($unit)
386
    {
387
        foreach (static::$unitTable as $baseUnit => $unitVariants) {
388
            if (isset($unitVariants[$unit])) {
389
                return $baseUnit;
390
            }
391
        }
392
393
        return null;
394
    }
395
}
396