Completed
Push — develop ( 782b4e...557e80 )
by Adrien
43:38
created

NumberFormat::getSharedComponent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Style;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
6
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
7
use PhpOffice\PhpSpreadsheet\IComparable;
8
use PhpOffice\PhpSpreadsheet\Shared\Date;
9
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
10
11
class NumberFormat extends Supervisor implements IComparable
12
{
13
    // Pre-defined formats
14
    const FORMAT_GENERAL = 'General';
15
16
    const FORMAT_TEXT = '@';
17
18
    const FORMAT_NUMBER = '0';
19
    const FORMAT_NUMBER_00 = '0.00';
20
    const FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00';
21
    const FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-';
22
23
    const FORMAT_PERCENTAGE = '0%';
24
    const FORMAT_PERCENTAGE_00 = '0.00%';
25
26
    const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd';
27
    const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd';
28
    const FORMAT_DATE_DDMMYYYY = 'dd/mm/yy';
29
    const FORMAT_DATE_DMYSLASH = 'd/m/y';
30
    const FORMAT_DATE_DMYMINUS = 'd-m-y';
31
    const FORMAT_DATE_DMMINUS = 'd-m';
32
    const FORMAT_DATE_MYMINUS = 'm-y';
33
    const FORMAT_DATE_XLSX14 = 'mm-dd-yy';
34
    const FORMAT_DATE_XLSX15 = 'd-mmm-yy';
35
    const FORMAT_DATE_XLSX16 = 'd-mmm';
36
    const FORMAT_DATE_XLSX17 = 'mmm-yy';
37
    const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm';
38
    const FORMAT_DATE_DATETIME = 'd/m/y h:mm';
39
    const FORMAT_DATE_TIME1 = 'h:mm AM/PM';
40
    const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM';
41
    const FORMAT_DATE_TIME3 = 'h:mm';
42
    const FORMAT_DATE_TIME4 = 'h:mm:ss';
43
    const FORMAT_DATE_TIME5 = 'mm:ss';
44
    const FORMAT_DATE_TIME6 = 'h:mm:ss';
45
    const FORMAT_DATE_TIME7 = 'i:s.S';
46
    const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
47
    const FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd;@';
48
49
    const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-';
50
    const FORMAT_CURRENCY_USD = '$#,##0_-';
51
    const FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-';
52
53
    /**
54
     * Excel built-in number formats.
55
     *
56
     * @var array
57
     */
58
    protected static $builtInFormats;
59
60
    /**
61
     * Excel built-in number formats (flipped, for faster lookups).
62
     *
63
     * @var array
64
     */
65
    protected static $flippedBuiltInFormats;
66
67
    /**
68
     * Format Code.
69
     *
70
     * @var string
71
     */
72
    protected $formatCode = self::FORMAT_GENERAL;
73
74
    /**
75
     * Built-in format Code.
76
     *
77
     * @var string
78
     */
79
    protected $builtInFormatCode = 0;
80
81
    /**
82
     * Create a new NumberFormat.
83
     *
84
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
85
     *                                    Leave this value at default unless you understand exactly what
86
     *                                        its ramifications are
87
     * @param bool $isConditional Flag indicating if this is a conditional style or not
88
     *                                    Leave this value at default unless you understand exactly what
89
     *                                        its ramifications are
90
     */
91 134
    public function __construct($isSupervisor = false, $isConditional = false)
92
    {
93
        // Supervisor?
94 134
        parent::__construct($isSupervisor);
95
96 134
        if ($isConditional) {
97 2
            $this->formatCode = null;
98 2
            $this->builtInFormatCode = false;
0 ignored issues
show
Documentation Bug introduced by
The property $builtInFormatCode was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
99
        }
100 134
    }
101
102
    /**
103
     * Get the shared style component for the currently active cell in currently active sheet.
104
     * Only used for style supervisor.
105
     *
106
     * @return NumberFormat
107
     */
108 5
    public function getSharedComponent()
109
    {
110 5
        return $this->parent->getSharedComponent()->getNumberFormat();
111
    }
112
113
    /**
114
     * Build style array from subcomponents.
115
     *
116
     * @param array $array
117
     *
118
     * @return array
119
     */
120 32
    public function getStyleArray($array)
121
    {
122 32
        return ['numberFormat' => $array];
123
    }
124
125
    /**
126
     * Apply styles from array.
127
     * <code>
128
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray(
129
     *        array(
130
     *            'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE
131
     *        )
132
     * );
133
     * </code>.
134
     *
135
     * @param array $pStyles Array containing style information
136
     *
137
     * @throws PhpSpreadsheetException
138
     *
139
     * @return NumberFormat
140
     */
141 36
    public function applyFromArray(array $pStyles)
142
    {
143 36
        if ($this->isSupervisor) {
144
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
145
        } else {
146 36
            if (isset($pStyles['formatCode'])) {
147 36
                $this->setFormatCode($pStyles['formatCode']);
148
            }
149
        }
150
151 36
        return $this;
152
    }
153
154
    /**
155
     * Get Format Code.
156
     *
157
     * @return string
158
     */
159 53
    public function getFormatCode()
160
    {
161 53
        if ($this->isSupervisor) {
162 5
            return $this->getSharedComponent()->getFormatCode();
163
        }
164 53
        if ($this->builtInFormatCode !== false) {
165 44
            return self::builtInFormatCode($this->builtInFormatCode);
166
        }
167
168 33
        return $this->formatCode;
169
    }
170
171
    /**
172
     * Set Format Code.
173
     *
174
     * @param string $pValue see self::FORMAT_*
175
     *
176
     * @return NumberFormat
177
     */
178 63 View Code Duplication
    public function setFormatCode($pValue)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
179
    {
180 63
        if ($pValue == '') {
181
            $pValue = self::FORMAT_GENERAL;
182
        }
183 63
        if ($this->isSupervisor) {
184 32
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
185 32
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
186
        } else {
187 63
            $this->formatCode = $pValue;
188 63
            $this->builtInFormatCode = self::builtInFormatCodeIndex($pValue);
0 ignored issues
show
Documentation Bug introduced by
It seems like self::builtInFormatCodeIndex($pValue) of type boolean or integer is incompatible with the declared type string of property $builtInFormatCode.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
189
        }
190
191 63
        return $this;
192
    }
193
194
    /**
195
     * Get Built-In Format Code.
196
     *
197
     * @return int
198
     */
199 58
    public function getBuiltInFormatCode()
200
    {
201 58
        if ($this->isSupervisor) {
202
            return $this->getSharedComponent()->getBuiltInFormatCode();
203
        }
204
205 58
        return $this->builtInFormatCode;
206
    }
207
208
    /**
209
     * Set Built-In Format Code.
210
     *
211
     * @param int $pValue
212
     *
213
     * @return NumberFormat
214
     */
215
    public function setBuiltInFormatCode($pValue)
216
    {
217
        if ($this->isSupervisor) {
218
            $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($pValue)]);
219
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
220
        } else {
221
            $this->builtInFormatCode = $pValue;
0 ignored issues
show
Documentation Bug introduced by
The property $builtInFormatCode was declared of type string, but $pValue is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
222
            $this->formatCode = self::builtInFormatCode($pValue);
223
        }
224
225
        return $this;
226
    }
227
228
    /**
229
     * Fill built-in format codes.
230
     */
231 78
    private static function fillBuiltInFormatCodes()
232
    {
233
        //  [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance]
234
        //  18.8.30. numFmt (Number Format)
235
        //
236
        //  The ECMA standard defines built-in format IDs
237
        //      14: "mm-dd-yy"
238
        //      22: "m/d/yy h:mm"
239
        //      37: "#,##0 ;(#,##0)"
240
        //      38: "#,##0 ;[Red](#,##0)"
241
        //      39: "#,##0.00;(#,##0.00)"
242
        //      40: "#,##0.00;[Red](#,##0.00)"
243
        //      47: "mmss.0"
244
        //      KOR fmt 55: "yyyy-mm-dd"
245
        //  Excel defines built-in format IDs
246
        //      14: "m/d/yyyy"
247
        //      22: "m/d/yyyy h:mm"
248
        //      37: "#,##0_);(#,##0)"
249
        //      38: "#,##0_);[Red](#,##0)"
250
        //      39: "#,##0.00_);(#,##0.00)"
251
        //      40: "#,##0.00_);[Red](#,##0.00)"
252
        //      47: "mm:ss.0"
253
        //      KOR fmt 55: "yyyy/mm/dd"
254
255
        // Built-in format codes
256 78
        if (self::$builtInFormats === null) {
257 69
            self::$builtInFormats = [];
258
259
            // General
260 69
            self::$builtInFormats[0] = self::FORMAT_GENERAL;
261 69
            self::$builtInFormats[1] = '0';
262 69
            self::$builtInFormats[2] = '0.00';
263 69
            self::$builtInFormats[3] = '#,##0';
264 69
            self::$builtInFormats[4] = '#,##0.00';
265
266 69
            self::$builtInFormats[9] = '0%';
267 69
            self::$builtInFormats[10] = '0.00%';
268 69
            self::$builtInFormats[11] = '0.00E+00';
269 69
            self::$builtInFormats[12] = '# ?/?';
270 69
            self::$builtInFormats[13] = '# ??/??';
271 69
            self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
272 69
            self::$builtInFormats[15] = 'd-mmm-yy';
273 69
            self::$builtInFormats[16] = 'd-mmm';
274 69
            self::$builtInFormats[17] = 'mmm-yy';
275 69
            self::$builtInFormats[18] = 'h:mm AM/PM';
276 69
            self::$builtInFormats[19] = 'h:mm:ss AM/PM';
277 69
            self::$builtInFormats[20] = 'h:mm';
278 69
            self::$builtInFormats[21] = 'h:mm:ss';
279 69
            self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
280
281 69
            self::$builtInFormats[37] = '#,##0_);(#,##0)'; //  Despite ECMA '#,##0 ;(#,##0)';
282 69
            self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; //  Despite ECMA '#,##0 ;[Red](#,##0)';
283 69
            self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; //  Despite ECMA '#,##0.00;(#,##0.00)';
284 69
            self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; //  Despite ECMA '#,##0.00;[Red](#,##0.00)';
285
286 69
            self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
287 69
            self::$builtInFormats[45] = 'mm:ss';
288 69
            self::$builtInFormats[46] = '[h]:mm:ss';
289 69
            self::$builtInFormats[47] = 'mm:ss.0'; //  Despite ECMA 'mmss.0';
290 69
            self::$builtInFormats[48] = '##0.0E+0';
291 69
            self::$builtInFormats[49] = '@';
292
293
            // CHT
294 69
            self::$builtInFormats[27] = '[$-404]e/m/d';
295 69
            self::$builtInFormats[30] = 'm/d/yy';
296 69
            self::$builtInFormats[36] = '[$-404]e/m/d';
297 69
            self::$builtInFormats[50] = '[$-404]e/m/d';
298 69
            self::$builtInFormats[57] = '[$-404]e/m/d';
299
300
            // THA
301 69
            self::$builtInFormats[59] = 't0';
302 69
            self::$builtInFormats[60] = 't0.00';
303 69
            self::$builtInFormats[61] = 't#,##0';
304 69
            self::$builtInFormats[62] = 't#,##0.00';
305 69
            self::$builtInFormats[67] = 't0%';
306 69
            self::$builtInFormats[68] = 't0.00%';
307 69
            self::$builtInFormats[69] = 't# ?/?';
308 69
            self::$builtInFormats[70] = 't# ??/??';
309
310
            // Flip array (for faster lookups)
311 69
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
312
        }
313 78
    }
314
315
    /**
316
     * Get built-in format code.
317
     *
318
     * @param int $pIndex
319
     *
320
     * @return string
321
     */
322 61
    public static function builtInFormatCode($pIndex)
323
    {
324
        // Clean parameter
325 61
        $pIndex = (int) $pIndex;
326
327
        // Ensure built-in format codes are available
328 61
        self::fillBuiltInFormatCodes();
329
330
        // Lookup format code
331 61
        if (isset(self::$builtInFormats[$pIndex])) {
332 61
            return self::$builtInFormats[$pIndex];
333
        }
334
335 2
        return '';
336
    }
337
338
    /**
339
     * Get built-in format code index.
340
     *
341
     * @param string $formatCode
342
     *
343
     * @return bool|int
344
     */
345 63
    public static function builtInFormatCodeIndex($formatCode)
346
    {
347
        // Ensure built-in format codes are available
348 63
        self::fillBuiltInFormatCodes();
349
350
        // Lookup format code
351 63
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
352 54
            return self::$flippedBuiltInFormats[$formatCode];
353
        }
354
355 49
        return false;
356
    }
357
358
    /**
359
     * Get hash code.
360
     *
361
     * @return string Hash code
362
     */
363 76
    public function getHashCode()
364
    {
365 76
        if ($this->isSupervisor) {
366
            return $this->getSharedComponent()->getHashCode();
367
        }
368
369 76
        return md5(
370 76
            $this->formatCode .
371 76
            $this->builtInFormatCode .
372 76
            __CLASS__
373
        );
374
    }
375
376
    /**
377
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
378
     *
379
     * @var array
380
     */
381
    private static $dateFormatReplacements = [
382
            // first remove escapes related to non-format characters
383
            '\\' => '',
384
            //    12-hour suffix
385
            'am/pm' => 'A',
386
            //    4-digit year
387
            'e' => 'Y',
388
            'yyyy' => 'Y',
389
            //    2-digit year
390
            'yy' => 'y',
391
            //    first letter of month - no php equivalent
392
            'mmmmm' => 'M',
393
            //    full month name
394
            'mmmm' => 'F',
395
            //    short month name
396
            'mmm' => 'M',
397
            //    mm is minutes if time, but can also be month w/leading zero
398
            //    so we try to identify times be the inclusion of a : separator in the mask
399
            //    It isn't perfect, but the best way I know how
400
            ':mm' => ':i',
401
            'mm:' => 'i:',
402
            //    month leading zero
403
            'mm' => 'm',
404
            //    month no leading zero
405
            'm' => 'n',
406
            //    full day of week name
407
            'dddd' => 'l',
408
            //    short day of week name
409
            'ddd' => 'D',
410
            //    days leading zero
411
            'dd' => 'd',
412
            //    days no leading zero
413
            'd' => 'j',
414
            //    seconds
415
            'ss' => 's',
416
            //    fractional seconds - no php equivalent
417
            '.s' => '',
418
        ];
419
    /**
420
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
421
     *
422
     * @var array
423
     */
424
    private static $dateFormatReplacements24 = [
425
            'hh' => 'H',
426
            'h' => 'G',
427
        ];
428
    /**
429
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
430
     *
431
     * @var array
432
     */
433
    private static $dateFormatReplacements12 = [
434
            'hh' => 'h',
435
            'h' => 'g',
436
        ];
437
438 27
    private static function setLowercaseCallback($matches)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
439
    {
440 27
        return mb_strtolower($matches[0]);
441
    }
442
443 11
    private static function escapeQuotesCallback($matches)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
444
    {
445 11
        return '\\' . implode('\\', str_split($matches[1]));
446
    }
447
448 27
    private static function formatAsDate(&$value, &$format)
449
    {
450
        // strip off first part containing e.g. [$-F800] or [$USD-409]
451
        // general syntax: [$<Currency string>-<language info>]
452
        // language info is in hexadecimal
453
        // strip off chinese part like [DBNum1][$-804]
454 27
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
455
456
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
457
        //    but we don't want to change any quoted strings
458 27
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
459
460
        // Only process the non-quoted blocks for date format characters
461 27
        $blocks = explode('"', $format);
462 27
        foreach ($blocks as $key => &$block) {
463 27
            if ($key % 2 == 0) {
464 27
                $block = strtr($block, self::$dateFormatReplacements);
465 27
                if (!strpos($block, 'A')) {
466
                    // 24-hour time format
467 27
                    $block = strtr($block, self::$dateFormatReplacements24);
468
                } else {
469
                    // 12-hour time format
470 27
                    $block = strtr($block, self::$dateFormatReplacements12);
471
                }
472
            }
473
        }
474 27
        $format = implode('"', $blocks);
475
476
        // escape any quoted characters so that DateTime format() will render them correctly
477 27
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
478
479 27
        $dateObj = Date::excelToDateTimeObject($value);
480 27
        $value = $dateObj->format($format);
481 27
    }
482
483 4
    private static function formatAsPercentage(&$value, &$format)
484
    {
485 4
        if ($format === self::FORMAT_PERCENTAGE) {
486 3
            $value = round((100 * $value), 0) . '%';
487
        } else {
488 1
            if (preg_match('/\.[#0]+/', $format, $m)) {
489 1
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
490 1
                $format = str_replace($m[0], $s, $format);
491
            }
492 1
            if (preg_match('/^[#0]+/', $format, $m)) {
493 1
                $format = str_replace($m[0], strlen($m[0]), $format);
494
            }
495 1
            $format = '%' . str_replace('%', 'f%%', $format);
496
497 1
            $value = sprintf($format, 100 * $value);
498
        }
499 4
    }
500
501 4
    private static function formatAsFraction(&$value, &$format)
502
    {
503 4
        $sign = ($value < 0) ? '-' : '';
504
505 4
        $integerPart = floor(abs($value));
506 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
507 4
        $decimalLength = strlen($decimalPart);
508 4
        $decimalDivisor = pow(10, $decimalLength);
509
510 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
511
512 4
        $adjustedDecimalPart = $decimalPart / $GCD;
513 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
514
515 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
516 3
            if ($integerPart == 0) {
517
                $integerPart = '';
518
            }
519 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
520
        } else {
521 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
522 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
523
        }
524 4
    }
525
526 6
    private static function complexNumberFormatMask($number, $mask)
527
    {
528 6
        $sign = ($number < 0.0);
529 6
        $number = abs($number);
530 6
        if (strpos($mask, '.') !== false) {
531 2
            $numbers = explode('.', $number . '.0');
532 2
            $masks = explode('.', $mask . '.0');
533 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
534 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
535
536 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
537
        }
538
539 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
540 6
        if ($r > 1) {
541 6
            $result = array_reverse($result[0]);
542
543 6
            foreach ($result as $block) {
544 6
                $divisor = 1 . $block[0];
545 6
                $size = strlen($block[0]);
546 6
                $offset = $block[1];
547
548 6
                $blockValue = sprintf(
549 6
                    '%0' . $size . 'd',
550 6
                    fmod($number, $divisor)
551
                );
552 6
                $number = floor($number / $divisor);
553 6
                $mask = substr_replace($mask, $blockValue, $offset, $size);
554
            }
555 6
            if ($number > 0) {
556 4
                $mask = substr_replace($mask, $number, $offset, 0);
0 ignored issues
show
Bug introduced by
The variable $offset does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
557
            }
558 6
            $result = $mask;
559
        } else {
560 2
            $result = $number;
561
        }
562
563 6
        return (($sign) ? '-' : '') . $result;
564
    }
565
566
    /**
567
     * Convert a value in a pre-defined format to a PHP string.
568
     *
569
     * @param mixed $value Value to format
570
     * @param string $format Format code, see = self::FORMAT_*
571
     * @param array $callBack Callback function for additional formatting of string
572
     *
573
     * @return string Formatted string
574
     */
575 107
    public static function toFormattedString($value, $format, $callBack = null)
576
    {
577
        // For now we do not treat strings although section 4 of a format code affects strings
578 107
        if (!is_numeric($value)) {
579 44
            return $value;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $value; (object|string|null|array|boolean) is incompatible with the return type documented by PhpOffice\PhpSpreadsheet...rmat::toFormattedString of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
580
        }
581
582
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
583
        // it seems to round numbers to a total of 10 digits.
584 93
        if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) {
585 26
            return $value;
586
        }
587
588
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
589 78
        $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
590
591
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
592 78
        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
593
594
        // Extract the relevant section depending on whether number is positive, negative, or zero?
595
        // Text not supported yet.
596
        // Here is how the sections apply to various values in Excel:
597
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
598
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
599
        //   3 sections:  [POSITIVE/TEXT] [NEGATIVE] [ZERO]
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
600
        //   4 sections:  [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
601 78
        switch (count($sections)) {
602 78
            case 1:
603 72
                $format = $sections[0];
604
605 72
                break;
606 10
            case 2:
607 10
                $format = ($value >= 0) ? $sections[0] : $sections[1];
608 10
                $value = abs($value); // Use the absolute value
609 10
                break;
610 View Code Duplication
            case 3:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
611
                $format = ($value > 0) ?
612
                    $sections[0] : (($value < 0) ?
613
                        $sections[1] : $sections[2]);
614
                $value = abs($value); // Use the absolute value
615
                break;
616 View Code Duplication
            case 4:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
617
                $format = ($value > 0) ?
618
                    $sections[0] : (($value < 0) ?
619
                        $sections[1] : $sections[2]);
620
                $value = abs($value); // Use the absolute value
621
                break;
622
            default:
623
                // something is wrong, just use first section
624
                $format = $sections[0];
625
626
                break;
627
        }
628
629
        // In Excel formats, "_" is used to add spacing,
630
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
631 78
        $format = preg_replace('/_./', ' ', $format);
632
633
        // Save format with color information for later use below
634 78
        $formatColor = $format;
635
636
        // Strip color information
637 78
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
638 78
        $format = preg_replace($color_regex, '', $format);
639
640
        // Let's begin inspecting the format and converting the value to a formatted string
641
642
        //  Check for date/time characters (not inside quotes)
643 78
        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
644
            // datetime format
645 27
            self::formatAsDate($value, $format);
646 61
        } elseif (preg_match('/%$/', $format)) {
647
            // % number format
648 4
            self::formatAsPercentage($value, $format);
649
        } else {
650 57
            if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
651
                $value = 'EUR ' . sprintf('%1.2f', $value);
652
            } else {
653
                // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
654 57
                $format = str_replace(['"', '*'], '', $format);
655
656
                // Find out if we need thousands separator
657
                // This is indicated by a comma enclosed by a digit placeholder:
658
                //        #,#   or   0,0
659 57
                $useThousands = preg_match('/(#,#|0,0)/', $format);
660 57
                if ($useThousands) {
661 25
                    $format = preg_replace('/0,0/', '00', $format);
662 25
                    $format = preg_replace('/#,#/', '##', $format);
663
                }
664
665
                // Scale thousands, millions,...
666
                // This is indicated by a number of commas after a digit placeholder:
667
                //        #,   or    0.0,,
668 57
                $scale = 1; // same as no scale
669 57
                $matches = [];
670 57
                if (preg_match('/(#|0)(,+)/', $format, $matches)) {
671 2
                    $scale = pow(1000, strlen($matches[2]));
672
673
                    // strip the commas
674 2
                    $format = preg_replace('/0,+/', '0', $format);
675 2
                    $format = preg_replace('/#,+/', '#', $format);
676
                }
677
678 57
                if (preg_match('/#?.*\?\/\?/', $format, $m)) {
679 4
                    if ($value != (int) $value) {
680 4
                        self::formatAsFraction($value, $format);
681
                    }
682
                } else {
683
                    // Handle the number itself
684
685
                    // scale number
686 53
                    $value = $value / $scale;
687
688
                    // Strip #
689 53
                    $format = preg_replace('/\\#/', '0', $format);
690
691 53
                    $n = "/\[[^\]]+\]/";
692 53
                    $m = preg_replace($n, '', $format);
693 53
                    $number_regex = "/(0+)(\.?)(0*)/";
694 53
                    if (preg_match($number_regex, $m, $matches)) {
695 53
                        $left = $matches[1];
696 53
                        $dec = $matches[2];
697 53
                        $right = $matches[3];
698
699
                        // minimun width of formatted number (including dot)
700 53
                        $minWidth = strlen($left) + strlen($dec) + strlen($right);
701 53
                        if ($useThousands) {
702 25
                            $value = number_format(
703 25
                                $value,
704 25
                                strlen($right),
705 25
                                StringHelper::getDecimalSeparator(),
706 25
                                StringHelper::getThousandsSeparator()
707
                            );
708 25
                            $value = preg_replace($number_regex, $value, $format);
709
                        } else {
710 32
                            if (preg_match('/[0#]E[+-]0/i', $format)) {
711
                                //    Scientific format
712 7
                                $value = sprintf('%5.2E', $value);
713 29
                            } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
714 6
                                $value = self::complexNumberFormatMask($value, $format);
715
                            } else {
716 23
                                $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
717 23
                                $value = sprintf($sprintf_pattern, $value);
718 23
                                $value = preg_replace($number_regex, $value, $format);
719
                            }
720
                        }
721
                    }
722
                }
723 57
                if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
724
                    //  Currency or Accounting
725 5
                    $currencyCode = $m[1];
726 5
                    list($currencyCode) = explode('-', $currencyCode);
727 5
                    if ($currencyCode == '') {
728
                        $currencyCode = StringHelper::getCurrencyCode();
729
                    }
730 5
                    $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
731
                }
732
            }
733
        }
734
735
        // Additional formatting provided by callback function
736 78
        if ($callBack !== null) {
737 4
            list($writerInstance, $function) = $callBack;
738 4
            $value = $writerInstance->$function($value, $formatColor);
739
        }
740
741 78
        return $value;
742
    }
743
}
744