Passed
Push — master ( 707ddf...aa8b2f )
by Sebastian
04:56
created

NumberInfo   F

Complexity

Total Complexity 95

Size/Duplication

Total Lines 692
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 95
eloc 196
c 3
b 0
f 0
dl 0
loc 692
rs 2

39 Methods

Rating   Name   Duplication   Size   Complexity  
A isPositive() 0 8 2
A setValue() 0 11 2
A getRawInfo() 0 3 1
A isEmpty() 0 3 1
A __construct() 0 3 1
A setNumber() 0 4 1
A isBiggerThan() 0 8 2
A isNegative() 0 3 2
A hasDecimals() 0 5 1
A isSmallerThan() 0 8 2
A isPixels() 0 3 2
A createValueKey() 0 8 3
A getValue() 0 3 1
A addPercent() 0 3 1
A hasUnits() 0 3 1
A isUnitInteger() 0 3 1
A isUnitDecimal() 0 3 1
A postProcess() 0 3 1
A getNumber() 0 9 2
A subtractPercent() 0 3 1
A add() 0 16 4
B parseValue() 0 55 8
A isBiggerEqual() 0 8 2
A isPercent() 0 3 2
A findUnits() 0 25 4
A subtract() 0 16 4
A toAttribute() 0 15 4
A filterInfo() 0 14 6
A preProcess() 0 3 1
A isEven() 0 3 2
A __toString() 0 7 2
A toCSS() 0 11 3
A hasValue() 0 7 3
B parseStringValue() 0 48 7
A percentOperation() 0 27 6
A getUnits() 0 11 3
A isZeroOrEmpty() 0 3 2
A isZero() 0 7 2
A enablePostProcess() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like NumberInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use NumberInfo, and based on these observations, apply Extract Interface, too.

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
        if($this->isEmpty()) {
136
            return false;
137
        }
138
139
        return (float)$this->getNumber() === 0.0;
140
    }
141
    
142
    public function isZeroOrEmpty() : bool
143
    {
144
        return $this->isEmpty() || $this->isZero();
145
    }
146
    
147
    /**
148
     * Whether the number has a value: this is true if
149
     * it is not empty, and has a non-zero value.
150
     *
151
     * @return boolean
152
     */
153
    public function hasValue() : bool
154
    {
155
        if(!$this->isEmpty() && !$this->isZero()) {
156
            return true;
157
        }
158
        
159
        return false;
160
    }
161
    
162
    /**
163
     * Whether the value is negative.
164
     * @return boolean
165
     */
166
    public function isNegative() : bool
167
    {
168
        return !$this->isEmpty() && $this->getNumber() < 0;
169
    }
170
    
171
    /**
172
     * Changes the stored number, without modifying the units.
173
     * @param float|int $number
174
     * @return NumberInfo
175
     */
176
    public function setNumber($number)
177
    {
178
        $this->info['number'] = floatval($number);
179
        return $this;
180
    }
181
    
182
    /**
183
     * Whether the number is a pixel value. This is true
184
     * also if the px suffix is omitted.
185
     * @return boolean
186
     */
187
    public function isPixels()
188
    {
189
        return !$this->isEmpty() && $this->getUnits() == 'px';
190
    }
191
    
192
    /**
193
     * Whether the number is a percent value.
194
     * @return boolean
195
     */
196
    public function isPercent()
197
    {
198
        return !$this->isEmpty() && $this->getUnits() == '%';
199
    }
200
    
201
    /**
202
     * Retrieves the numeric value without units.
203
     * @return float|int
204
     */
205
    public function getNumber()
206
    {
207
        $number = (float)$this->info['number'];
208
209
        if($this->hasDecimals()) {
210
            return $number;
211
        }
212
213
        return intval($number);
214
    }
215
216
    public function hasDecimals() : bool
217
    {
218
        $number = (float)$this->info['number'];
219
220
        return floor($number) !== $number;
221
    }
222
    
223
    /**
224
     * Checks whether the number is an even number.
225
     * @return boolean
226
     */
227
    public function isEven()
228
    {
229
        return !$this->isEmpty() && !($this->getNumber() & 1);
230
    }
231
    
232
    /**
233
     * Retrieves the units of the number. If no units
234
     * have been initially specified, this will always
235
     * return 'px'.
236
     *
237
     * @return mixed
238
     */
239
    public function getUnits()
240
    {
241
        if($this->isEmpty()) {
242
            return '';
243
        }
244
245
        if(!$this->hasUnits()) {
246
            return 'px';
247
        }
248
        
249
        return $this->info['units'];
250
    }
251
    
252
    /**
253
     * Whether specific units have been specified for the number.
254
     * @return boolean
255
     */
256
    public function hasUnits()
257
    {
258
        return !empty($this->info['units']);
259
    }
260
    
261
    /**
262
     * Retrieves the raw value as is, with or without units depending on how it was given.
263
     * @return number
264
     */
265
    public function getValue()
266
    {
267
        return $this->rawValue;
268
    }
269
    
270
    /**
271
     * Formats the number for use in a HTML attribute. If units were
272
     * specified, only percent are kept. All other units like px and the
273
     * like are stripped.
274
     *
275
     * @return string
276
     */
277
    public function toAttribute() : string
278
    {
279
        if($this->isEmpty()) {
280
            return '';
281
        }
282
        
283
        if($this->isZero()) {
284
            return '0';
285
        }
286
        
287
        if($this->isPercent()) {
288
            return $this->getNumber().$this->getUnits();
289
        }
290
        
291
        return (string)$this->getNumber();
292
    }
293
    
294
    /**
295
     * Formats the number for use in a CSS statement.
296
     * @return string
297
     */
298
    public function toCSS() : string
299
    {
300
        if($this->isEmpty()) {
301
            return '';
302
        }
303
        
304
        if($this->isZero()) {
305
            return '0';
306
        }
307
        
308
        return $this->getNumber().$this->getUnits();
309
    }
310
    
311
    public function __toString()
312
    {
313
        if($this->isEmpty()) {
314
            return '';
315
        }
316
        
317
        return (string)$this->getValue();
318
    }
319
    
320
    /**
321
     * Checks if this number is bigger than the specified
322
     * number. Note that this will always return false if
323
     * the numbers do not have the same units.
324
     *
325
     * @param string|number|NumberInfo $number
326
     * @return boolean
327
     */
328
    public function isBiggerThan($number)
329
    {
330
        $number = parseNumber($number);
331
        if($number->getUnits() != $this->getUnits()) {
332
            return false;
333
        }
334
        
335
        return $this->getNumber() > $number->getNumber();
336
    }
337
    
338
    /**
339
     * Checks if this number is smaller than the specified
340
     * number. Note that this will always return false if
341
     * the numbers do not have the same units.
342
     *
343
     * @param string|number|NumberInfo $number
344
     * @return boolean
345
     */
346
    public function isSmallerThan($number)
347
    {
348
        $number = parseNumber($number);
349
        if($number->getUnits() != $this->getUnits()) {
350
            return false;
351
        }
352
        
353
        return $this->getNumber() < $number->getNumber();
354
    }
355
    
356
    public function isBiggerEqual($number)
357
    {
358
        $number = parseNumber($number);
359
        if($number->getUnits() != $this->getUnits()) {
360
            return false;
361
        }
362
        
363
        return $this->getNumber() >= $number->getNumber();
364
    }
365
    
366
    /**
367
     * Adds the specified value to the current value, if
368
     * they are compatible - i.e. they have the same units
369
     * or a percentage.
370
     *
371
     * @param string|NumberInfo $value
372
     * @return NumberInfo
373
     */
374
    public function add($value)
375
    {
376
        if($this->isEmpty()) {
377
            $this->setValue($value);
378
            return $this;
379
        }
380
        
381
        $number = parseNumber($value);
382
        
383
        if($number->getUnits() == $this->getUnits() || !$number->hasUnits())
384
        {
385
            $new = $this->getNumber() + $number->getNumber();
386
            $this->setValue($new.$this->getUnits());
387
        }
388
        
389
        return $this;
390
    }
391
    
392
    /**
393
     * Subtracts the specified value from the current value, if
394
     * they are compatible - i.e. they have the same units, or
395
     * a percentage.
396
     *
397
     * @param string|NumberInfo $value
398
     * @return NumberInfo
399
     */
400
    public function subtract($value)
401
    {
402
        if($this->isEmpty()) {
403
            $this->setValue($value);
404
            return $this;
405
        }
406
        
407
        $number = parseNumber($value);
408
        
409
        if($number->getUnits() == $this->getUnits() || !$number->hasUnits())
410
        {
411
            $new = $this->getNumber() - $number->getNumber();
412
            $this->setValue($new.$this->getUnits());
413
        }
414
        
415
        return $this;
416
    }
417
    
418
    public function subtractPercent($percent)
419
    {
420
        return $this->percentOperation('-', $percent);
421
    }
422
    
423
    /**
424
     * Increases the current value by the specified percent amount.
425
     *
426
     * @param number $percent
427
     * @return NumberInfo
428
     */
429
    public function addPercent($percent)
430
    {
431
        return $this->percentOperation('+', $percent);
432
    }
433
    
434
    protected function percentOperation($operation, $percent)
435
    {
436
        if($this->isZeroOrEmpty()) {
437
            return $this;
438
        }
439
        
440
        $percent = parseNumber($percent);
441
        if($percent->hasUnits() && !$percent->isPercent()) {
442
            return $this;
443
        }
444
        
445
        $number = $this->getNumber();
446
        $value = $number * $percent->getNumber() / 100;
447
        
448
        if($operation == '-') {
449
            $number = $number - $value;
450
        } else {
451
            $number = $number + $value;
452
        }
453
        
454
        if($this->isUnitInteger()) {
455
            $number = intval($number);
456
        }
457
        
458
        $this->setValue($number.$this->getUnits());
459
        
460
        return $this;
461
        
462
    }
463
    
464
    public function isUnitInteger()
465
    {
466
        return $this->isPixels();
467
    }
468
    
469
    public function isUnitDecimal()
470
    {
471
        return $this->isPercent();
472
    }
473
    
474
    /**
475
     * Returns an array with information about the number
476
     * and the units used with the number for use in CSS
477
     * style attributes or HTML attributes.
478
     *
479
     * Examples:
480
     *
481
     * 58 => array(
482
     *     'number' => 58,
483
     *     'units' => null
484
     * )
485
     *
486
     * 58px => array(
487
     *     'number' => 58,
488
     *     'units' => 'px'
489
     * )
490
     *
491
     * 20% => array(
492
     *     'number' => 20,
493
     *     'units' => '%'
494
     * )
495
     *
496
     * @param mixed $value
497
     * @return array
498
     */
499
    private function parseValue($value) : array
500
    {
501
        static $cache = array();
502
        
503
        $key = $this->createValueKey($value);
504
505
        if(array_key_exists($key, $cache)) {
506
            return $cache[$key];
507
        }
508
        
509
        $cache[$key] = array(
510
            'units' => null,
511
            'empty' => false,
512
            'number' => null
513
        );
514
        
515
        if($key === '_EMPTY_') 
516
        {
517
            $cache[$key]['empty'] = true;
518
            return $cache[$key];
519
        }
520
        
521
        if($value === 0 || $value === '0') 
522
        {
523
            $cache[$key]['number'] = 0;
524
            $cache[$key] = $this->filterInfo($cache[$key]);
525
            return $cache[$key];
526
        }
527
        
528
        $test = trim((string)$value);
529
        
530
        if($test === '') 
531
        {
532
            $cache[$key]['empty'] = true;
533
            return $cache[$key];
534
        }
535
        
536
        // replace comma notation (which is only possible if it's a string)
537
        if(is_string($value))
538
        {
539
            $test = $this->preProcess($test, $cache, $value);
540
        }
541
        
542
        // convert to a number if it's numeric
543
        if(is_numeric($test)) 
544
        {
545
            $cache[$key]['number'] = (float)$test * 1;
546
            $cache[$key] = $this->filterInfo($cache[$key]);
547
            return $cache[$key];
548
        }
549
        
550
        // not numeric: there are possibly units specified in the string
551
        $cache[$key] = $this->parseStringValue($test);
552
        
553
        return $cache[$key];
554
    }
555
    
556
   /**
557
    * Parses a string number notation with units included, e.g. 14px, 50%...
558
    * 
559
    * @param string $test
560
    * @return array
561
    */
562
    private function parseStringValue(string $test) : array
563
    {
564
        $number = null;
565
        $units = null;
566
        $empty = false;
567
        
568
        $found = $this->findUnits($test);
569
        if($found !== null) 
570
        {
571
            $number = $found['number'];
572
            $units = $found['units'];
573
        }
574
        
575
        // the filters have to restore the value
576
        if($this->postProcess)
577
        {
578
            $number = $this->postProcess($number, $test);
579
        }
580
        // empty number
581
        else if($number === '' || $number === null || is_bool($number))
582
        {
583
            $number = null;
584
            $empty = true;
585
        }
586
        // found a number
587
        else
588
        {
589
            $number = trim($number);
590
            
591
            // may be an arbitrary string in some cases
592
            if(!is_numeric($number))
593
            {
594
                $number = null;
595
                $empty = true;
596
            }
597
            else
598
            {
599
                $number = (float)$number * 1;
600
            }
601
        }
602
        
603
        $result = array(
604
            'units' => $units,
605
            'number' => $number,
606
            'empty' => $empty
607
        );
608
609
        return $this->filterInfo($result);
610
    }
611
    
612
   /**
613
    * Attempts to determine what kind of units are specified
614
    * in the string. Returns NULL if none could be matched.
615
    * 
616
    * @param string $value
617
    * @return array|NULL
618
    */
619
    private function findUnits(string $value) : ?array
620
    {
621
        $vlength = strlen($value);
622
        $names = array_keys($this->knownUnits);
623
        
624
        foreach($names as $unit)
625
        {
626
            $ulength = strlen($unit);
627
            $start = $vlength-$ulength;
628
            if($start < 0) {
629
                continue;
630
            }
631
            
632
            $search = substr($value, $start, $ulength);
633
            
634
            if($search==$unit) 
635
            {
636
                return array(
637
                    'units' => $unit,
638
                    'number' => substr($value, 0, $start)
639
                );
640
            }
641
        }
642
        
643
        return null;
644
    }
645
    
646
   /**
647
    * Creates the cache key for the specified value.
648
    * 
649
    * @param mixed $value
650
    * @return string
651
    */
652
    private function createValueKey($value) : string
653
    {
654
        if(!is_string($value) && !is_numeric($value))
655
        {
656
            return '_EMPTY_';
657
        }
658
659
        return (string)$value;
660
    }
661
    
662
    protected $postProcess = false;
663
    
664
   /**
665
    * Called if explicitly enabled: allows filtering the 
666
    * number after the detection process has completed.
667
    * 
668
    * @param string|NULL $number The adjusted number
669
    * @param string $originalString The original value before it was parsed
670
    * @return mixed
671
    */
672
    protected function postProcess(?string $number, /** @scrutinizer ignore-unused */ string $originalString)
673
    {
674
        return $number;
675
    }
676
    
677
   /**
678
    * Filters the value before it is parsed, but only if it is a string.
679
    * 
680
    * NOTE: This may be overwritten in a subclass, to allow custom filtering
681
    * the the values. An example of a use case would be a preprocessor for
682
    * variables in a templating system.
683
    * 
684
    * @param string $trimmedString The trimmed value.
685
    * @param array $cache The internal values cache array.
686
    * @param string $originalValue The original value that the NumberInfo was created for.
687
    * @return string
688
    * 
689
    * @see NumberInfo::enablePostProcess()
690
    */
691
    protected function preProcess(string $trimmedString, /** @scrutinizer ignore-unused */ array &$cache, /** @scrutinizer ignore-unused */ string $originalValue) : string
692
    {
693
        return str_replace(',', '.', $trimmedString);
694
    }
695
    
696
   /**
697
    * Enables the post processing so the postProcess method gets called.
698
    * This should be called in the {@link NumberInfo::preProcess()}
699
    * method as needed.
700
    * 
701
    * @return NumberInfo
702
    * @see NumberInfo::postProcess()
703
    */
704
    protected function enablePostProcess() : NumberInfo
705
    {
706
        $this->postProcess = true;
707
        return $this;
708
    }
709
    
710
   /**
711
    * Filters the number info array to adjust the units
712
    * and number according to the required rules.
713
    * 
714
    * @param array $info
715
    * @return array
716
    */
717
    protected function filterInfo(array $info) : array
718
    {
719
        $useUnits = 'px';
720
        if($info['units'] !== null) {
721
            $useUnits = $info['units'];
722
        }
723
        
724
        // the units are non-decimal: convert decimal values
725
        if($useUnits !== null && $this->knownUnits[$useUnits] === false && !$info['empty'] && is_numeric($info['number']))
726
        {
727
            $info['number'] = intval($info['number']);
728
        }
729
        
730
        return $info;
731
    }
732
}
733