Passed
Push — master ( 693dda...155960 )
by Sebastian
02:20
created

NumberInfo::isPixels()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * File containing the {@link NumberInfo} class
4
 *
5
 * @access public
6
 * @package Application Utils
7
 * @subpackage NumberInfo
8
 * @see NumberInfo
9
 */
10
11
namespace AppUtils;
12
13
/**
14
 * Abstraction class for numeric values for elements: offers
15
 * an easy to use API to work with pixel values, percentual
16
 * values and the like.
17
 *
18
 * Usage: use the global function {@link parseNumber()} to
19
 * create a new instance of the class, then use the API to
20
 * work with it.
21
 *
22
 * Examples:
23
 *
24
 * <pre>
25
 * parseNumber(42);
26
 * parseNumber('15%');
27
 * parseNumber('5em');
28
 * </pre>
29
 *
30
 * Hint: {@link parseNumber()} will also recognize number info
31
 * instances, so you can safely pass an existing number
32
 * info to it.
33
 *
34
 * @access public
35
 * @package Application Utils
36
 * @subpackage NumberInfo
37
 * @author Sebastian Mordziol <[email protected]>
38
 */
39
class NumberInfo
40
{
41
   /**
42
    * @var mixed
43
    */
44
    protected $rawValue;
45
    
46
   /**
47
    * @var array
48
    */
49
    protected $info;
50
    
51
   /**
52
    * @var bool
53
    */
54
    protected $empty = false;
55
    
56
   /**
57
    * @var array
58
    */
59
    protected $knownUnits = array(
60
        '%' => true,
61
        'rem' => true,
62
        'px' => false,
63
        'em' => true,
64
        'pt' => true,
65
        'vw' => true,
66
        'vh' => true,
67
        'ex' => true,
68
        'cm' => true,
69
        'mm' => true,
70
        'in' => true,
71
        'pc' => true
72
    );
73
    
74
    public function __construct($value)
75
    {
76
        $this->setValue($value);
77
    }
78
    
79
    /**
80
     * Sets the value of the number, including the units.
81
     *
82
     * @param string|NumberInfo $value e.g. "10", "45px", "100%", ... or an existing NumberInfo instance.
83
     * @return NumberInfo
84
     */
85
    public function setValue($value) : NumberInfo
86
    {
87
        if($value instanceof NumberInfo) {
88
            $value = $value->getValue();
89
        }
90
        
91
        $this->rawValue = $value;
92
        $this->info = $this->parseValue($value);
93
        $this->empty = $this->info['empty'];
94
        
95
        return $this;
96
    }
97
    
98
   /**
99
    * Retrieves the raw, internal information array resulting
100
    * from the parsing of the number.
101
    *  
102
    * @return array
103
    */
104
    public function getRawInfo() : array
105
    {
106
        return $this->info;
107
    }
108
    
109
   /**
110
    * Whether the number was empty (null or empty string).
111
    * @return boolean
112
    */
113
    public function isEmpty() : bool
114
    {
115
        return $this->empty;
116
    }
117
    
118
    public function isPositive() : bool
119
    {
120
        if(!$this->isEmpty()) {
121
            $number = $this->getNumber();
122
            return $number > 0;
123
        }
124
        
125
        return false;
126
    }
127
    
128
    
129
    /**
130
     * Whether the number is 0.
131
     * @return boolean
132
     */
133
    public function isZero() : bool
134
    {
135
        return $this->getNumber() === 0;
136
    }
137
    
138
    public function isZeroOrEmpty() : bool
139
    {
140
        return $this->isEmpty() || $this->isZero();
141
    }
142
    
143
    /**
144
     * Whether the number has a value: this is true if
145
     * it is not empty, and has a non-zero value.
146
     *
147
     * @return boolean
148
     */
149
    public function hasValue() : bool
150
    {
151
        if(!$this->isEmpty() && !$this->isZero()) {
152
            return true;
153
        }
154
        
155
        return false;
156
    }
157
    
158
    /**
159
     * Whether the value is negative.
160
     * @return boolean
161
     */
162
    public function isNegative() : bool
163
    {
164
        return !$this->isEmpty() && $this->getNumber() < 0;
165
    }
166
    
167
    /**
168
     * Changes the stored number, without modifying the units.
169
     * @param int $number
170
     * @return NumberInfo
171
     */
172
    public function setNumber($number)
173
    {
174
        $this->info['number'] = $number;
175
        return $this;
176
    }
177
    
178
    /**
179
     * Whether the number is a pixel value. This is true
180
     * also if the px suffix is omitted.
181
     * @return boolean
182
     */
183
    public function isPixels()
184
    {
185
        return !$this->isEmpty() && $this->getUnits() == 'px';
186
    }
187
    
188
    /**
189
     * Whether the number is a percent value.
190
     * @return boolean
191
     */
192
    public function isPercent()
193
    {
194
        return !$this->isEmpty() && $this->getUnits() == '%';
195
    }
196
    
197
    /**
198
     * Retrieves the numeric value without units.
199
     * @return mixed
200
     */
201
    public function getNumber()
202
    {
203
        return $this->info['number'];
204
    }
205
    
206
    /**
207
     * Checks whether the number is an even number.
208
     * @return boolean
209
     */
210
    public function isEven()
211
    {
212
        return !$this->isEmpty() && !($this->getNumber() & 1);
213
    }
214
    
215
    /**
216
     * Retrieves the units of the number. If no units
217
     * have been initially specified, this will always
218
     * return 'px'.
219
     *
220
     * @return mixed
221
     */
222
    public function getUnits()
223
    {
224
        if(!$this->hasUnits()) {
225
            return 'px';
226
        }
227
        
228
        return $this->info['units'];
229
    }
230
    
231
    /**
232
     * Whether specific units have been specified for the number.
233
     * @return boolean
234
     */
235
    public function hasUnits()
236
    {
237
        return !empty($this->info['units']);
238
    }
239
    
240
    /**
241
     * Retrieves the raw value as is, with or without units depending on how it was given.
242
     * @return number
243
     */
244
    public function getValue()
245
    {
246
        return $this->rawValue;
247
    }
248
    
249
    /**
250
     * Formats the number for use in a HTML attribute. If units were
251
     * specified, only percent are kept. All other units like px and the
252
     * like are stripped.
253
     *
254
     * @return string
255
     */
256
    public function toAttribute()
257
    {
258
        if($this->isEmpty()) {
259
            return null;
260
        }
261
        
262
        if($this->isZero()) {
263
            return '0';
264
        }
265
        
266
        if($this->isPercent()) {
267
            return $this->getNumber().$this->getUnits();
268
        }
269
        
270
        return $this->getNumber();
271
    }
272
    
273
    /**
274
     * Formats the number for use in a CSS statement.
275
     * @return string
276
     */
277
    public function toCSS()
278
    {
279
        if($this->isEmpty()) {
280
            return null;
281
        }
282
        
283
        if($this->isZero()) {
284
            return '0';
285
        }
286
        
287
        return $this->getNumber().$this->getUnits();
288
    }
289
    
290
    public function __toString()
291
    {
292
        if($this->isEmpty()) {
293
            return '';
294
        }
295
        
296
        return (string)$this->getValue();
297
    }
298
    
299
    /**
300
     * Checks if this number is bigger than the specified
301
     * number. Note that this will always return false if
302
     * the numbers do not have the same units.
303
     *
304
     * @param string|number|NumberInfo $number
305
     * @return boolean
306
     */
307
    public function isBiggerThan($number)
308
    {
309
        $number = parseNumber($number);
310
        if($number->getUnits() != $this->getUnits()) {
311
            return false;
312
        }
313
        
314
        return $this->getNumber() > $number->getNumber();
315
    }
316
    
317
    /**
318
     * Checks if this number is smaller than the specified
319
     * number. Note that this will always return false if
320
     * the numbers do not have the same units.
321
     *
322
     * @param string|number|NumberInfo $number
323
     * @return boolean
324
     */
325
    public function isSmallerThan($number)
326
    {
327
        $number = parseNumber($number);
328
        if($number->getUnits() != $this->getUnits()) {
329
            return false;
330
        }
331
        
332
        return $this->getNumber() < $number->getNumber();
333
    }
334
    
335
    public function isBiggerEqual($number)
336
    {
337
        $number = parseNumber($number);
338
        if($number->getUnits() != $this->getUnits()) {
339
            return false;
340
        }
341
        
342
        return $this->getNumber() >= $number->getNumber();
343
    }
344
    
345
    /**
346
     * Adds the specified value to the current value, if
347
     * they are compatible - i.e. they have the same units
348
     * or a percentage.
349
     *
350
     * @param string|NumberInfo $value
351
     * @return NumberInfo
352
     */
353
    public function add($value)
354
    {
355
        if($this->isEmpty()) {
356
            $this->setValue($value);
357
            return $this;
358
        }
359
        
360
        $number = parseNumber($value);
361
        
362
        if($number->getUnits() == $this->getUnits() || !$number->hasUnits())
363
        {
364
            $new = $this->getNumber() + $number->getNumber();
365
            $this->setValue($new.$this->getUnits());
366
        }
367
        
368
        return $this;
369
    }
370
    
371
    /**
372
     * Subtracts the specified value from the current value, if
373
     * they are compatible - i.e. they have the same units, or
374
     * a percentage.
375
     *
376
     * @param string|NumberInfo $value
377
     * @return NumberInfo
378
     */
379
    public function subtract($value)
380
    {
381
        if($this->isEmpty()) {
382
            $this->setValue($value);
383
            return $this;
384
        }
385
        
386
        $number = parseNumber($value);
387
        
388
        if($number->getUnits() == $this->getUnits() || !$number->hasUnits())
389
        {
390
            $new = $this->getNumber() - $number->getNumber();
391
            $this->setValue($new.$this->getUnits());
392
        }
393
        
394
        return $this;
395
    }
396
    
397
    public function subtractPercent($percent)
398
    {
399
        return $this->percentOperation('-', $percent);
400
    }
401
    
402
    /**
403
     * Increases the current value by the specified percent amount.
404
     *
405
     * @param number $percent
406
     * @return NumberInfo
407
     */
408
    public function addPercent($percent)
409
    {
410
        return $this->percentOperation('+', $percent);
411
    }
412
    
413
    protected function percentOperation($operation, $percent)
414
    {
415
        if($this->isZeroOrEmpty()) {
416
            return $this;
417
        }
418
        
419
        $percent = parseNumber($percent);
420
        if($percent->hasUnits() && !$percent->isPercent()) {
421
            return $this;
422
        }
423
        
424
        $number = $this->getNumber();
425
        $value = $number * $percent->getNumber() / 100;
426
        
427
        if($operation == '-') {
428
            $number = $number - $value;
429
        } else {
430
            $number = $number + $value;
431
        }
432
        
433
        if($this->isUnitInteger()) {
434
            $number = intval($number);
435
        }
436
        
437
        $this->setValue($number.$this->getUnits());
438
        
439
        return $this;
440
        
441
    }
442
    
443
    public function isUnitInteger()
444
    {
445
        return $this->isPixels();
446
    }
447
    
448
    public function isUnitDecimal()
449
    {
450
        return $this->isPercent();
451
    }
452
    
453
    /**
454
     * Returns an array with information about the number
455
     * and the units used with the number for use in CSS
456
     * style attributes or HTML attributes.
457
     *
458
     * Examples:
459
     *
460
     * 58 => array(
461
     *     'number' => 58,
462
     *     'units' => null
463
     * )
464
     *
465
     * 58px => array(
466
     *     'number' => 58,
467
     *     'units' => 'px'
468
     * )
469
     *
470
     * 20% => array(
471
     *     'number' => 20,
472
     *     'units' => '%'
473
     * )
474
     *
475
     * @param mixed $value
476
     * @return array
477
     */
478
    private function parseValue($value) : array
479
    {
480
        static $cache = array();
481
        
482
        $key = $this->createValueKey($value);
483
484
        if(array_key_exists($key, $cache)) {
485
            return $cache[$key];
486
        }
487
        
488
        $cache[$key] = array(
489
            'units' => null,
490
            'empty' => false,
491
            'number' => null
492
        );
493
        
494
        if($key === '_EMPTY_') 
495
        {
496
            $cache[$key]['empty'] = true;
497
            return $cache[$key];
498
        }
499
        
500
        if($value === 0 || $value === '0') 
501
        {
502
            $cache[$key]['number'] = 0;
503
            $cache[$key] = $this->filterInfo($cache[$key]);
504
            return $cache[$key];
505
        }
506
        
507
        $test = trim((string)$value);
508
        
509
        if($test === '') 
510
        {
511
            $cache[$key]['empty'] = true;
512
            return $cache[$key];
513
        }
514
        
515
        // replace comma notation (which is only possible if it's a string)
516
        if(is_string($value))
517
        {
518
            $test = $this->preProcess($test, $cache, $value);
519
        }
520
        
521
        // convert to a number if it's numeric
522
        if(is_numeric($test)) 
523
        {
524
            $cache[$key]['number'] = $test * 1;
525
            $cache[$key] = $this->filterInfo($cache[$key]);
526
            return $cache[$key];
527
        }
528
        
529
        // not numeric: there are possibly units specified in the string
530
        $cache[$key] = $this->parseStringValue($test);
531
        
532
        return $cache[$key];
533
    }
534
    
535
   /**
536
    * Parses a string number notation with units included, e.g. 14px, 50%...
537
    * 
538
    * @param string $test
539
    * @return array
540
    */
541
    private function parseStringValue(string $test) : array
542
    {
543
        $number = null;
544
        $units = null;
545
        $empty = false;
546
        
547
        $found = $this->findUnits($test);
548
        if($found !== null) 
549
        {
550
            $number = $found['number'];
551
            $units = $found['units'];
552
        }
553
        
554
        // the filters have to restore the value
555
        if($this->postProcess)
556
        {
557
            $number = $this->postProcess($number, $test);
558
        }
559
        // empty number
560
        else if($number === '' || $number === null || is_bool($number))
561
        {
562
            $number = null;
563
            $empty = true;
564
        }
565
        // found a number
566
        else
567
        {
568
            $number = trim($number);
569
            
570
            // may be an arbitrary string in some cases
571
            if(!is_numeric($number))
572
            {
573
                $number = null;
574
                $empty = true;
575
            }
576
            else
577
            {
578
                $number = $number * 1;
579
            }
580
        }
581
        
582
        $result = array(
583
            'units' => $units,
584
            'number' => $number,
585
            'empty' => $empty
586
        );
587
588
        return $this->filterInfo($result);
589
    }
590
    
591
   /**
592
    * Attempts to determine what kind of units are specified
593
    * in the string. Returns NULL if none could be matched.
594
    * 
595
    * @param string $value
596
    * @return array|NULL
597
    */
598
    private function findUnits(string $value) : ?array
599
    {
600
        $vlength = strlen($value);
601
        $names = array_keys($this->knownUnits);
602
        
603
        foreach($names as $unit)
604
        {
605
            $ulength = strlen($unit);
606
            $start = $vlength-$ulength;
607
            if($start < 0) {
608
                continue;
609
            }
610
            
611
            $search = substr($value, $start, $ulength);
612
            
613
            if($search==$unit) 
614
            {
615
                return array(
616
                    'units' => $unit,
617
                    'number' => substr($value, 0, $start)
618
                );
619
            }
620
        }
621
        
622
        return null;
623
    }
624
    
625
   /**
626
    * Creates the cache key for the specified value.
627
    * 
628
    * @param mixed $value
629
    * @return string
630
    */
631
    private function createValueKey($value) : string
632
    {
633
        if(!is_string($value) && !is_numeric($value))
634
        {
635
            return '_EMPTY_';
636
        }
637
638
        return (string)$value;
639
    }
640
    
641
    protected $postProcess = false;
642
    
643
   /**
644
    * Called if explicitly enabled: allows filtering the 
645
    * number after the detection process has completed.
646
    * 
647
    * @param string|NULL $number The adjusted number
648
    * @param string $originalString The original value before it was parsed
649
    * @return mixed
650
    */
651
    protected function postProcess(?string $number, /** @scrutinizer ignore-unused */ string $originalString)
652
    {
653
        return $number;
654
    }
655
    
656
   /**
657
    * Filters the value before it is parsed, but only if it is a string.
658
    * 
659
    * NOTE: This may be overwritten in a subclass, to allow custom filtering
660
    * the the values. An example of a use case would be a preprocessor for
661
    * variables in a templating system.
662
    * 
663
    * @param string $trimmedString The trimmed value.
664
    * @param array $cache The internal values cache array.
665
    * @param string $originalValue The original value that the NumberInfo was created for.
666
    * @return string
667
    * 
668
    * @see NumberInfo::enablePostProcess()
669
    */
670
    protected function preProcess(string $trimmedString, /** @scrutinizer ignore-unused */ array &$cache, /** @scrutinizer ignore-unused */ string $originalValue) : string
671
    {
672
        return str_replace(',', '.', $trimmedString);
673
    }
674
    
675
   /**
676
    * Enables the post processing so the postProcess method gets called.
677
    * This should be called in the {@link NumberInfo::preProcess()}
678
    * method as needed.
679
    * 
680
    * @return NumberInfo
681
    * @see NumberInfo::postProcess()
682
    */
683
    private function enablePostProcess() : NumberInfo
0 ignored issues
show
Unused Code introduced by
The method enablePostProcess() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
684
    {
685
        $this->postProcess = true;
686
        return $this;
687
    }
688
    
689
   /**
690
    * Filters the number info array to adjust the units
691
    * and number according to the required rules.
692
    * 
693
    * @param array $info
694
    * @return array
695
    */
696
    protected function filterInfo(array $info) : array
697
    {
698
        $useUnits = 'px';
699
        if($info['units'] !== null) {
700
            $useUnits = $info['units'];
701
        }
702
        
703
        // the units are non-decimal: convert decimal values
704
        if($useUnits !== null && $this->knownUnits[$useUnits] === false && !$info['empty'] && is_numeric($info['number']))
705
        {
706
            $info['number'] = intval($info['number']);
707
        }
708
        
709
        return $info;
710
    }
711
}
712