Completed
Push — develop ( 870d86...2ad559 )
by Adrien
42:24 queued 15:50
created

NumberFormat::getFormatCode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 3
rs 9.4285
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Style;
4
5
/**
6
 * Copyright (c) 2006 - 2016 PhpSpreadsheet.
7
 *
8
 * This library is free software; you can redistribute it and/or
9
 * modify it under the terms of the GNU Lesser General Public
10
 * License as published by the Free Software Foundation; either
11
 * version 2.1 of the License, or (at your option) any later version.
12
 *
13
 * This library is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
 * Lesser General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Lesser General Public
19
 * License along with this library; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
 *
22
 * @category   PhpSpreadsheet
23
 *
24
 * @copyright  Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet)
25
 * @license    http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt    LGPL
26
 */
27
class NumberFormat extends Supervisor implements \PhpOffice\PhpSpreadsheet\IComparable
28
{
29
    /* Pre-defined formats */
30
    const FORMAT_GENERAL = 'General';
31
32
    const FORMAT_TEXT = '@';
33
34
    const FORMAT_NUMBER = '0';
35
    const FORMAT_NUMBER_00 = '0.00';
36
    const FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00';
37
    const FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-';
38
39
    const FORMAT_PERCENTAGE = '0%';
40
    const FORMAT_PERCENTAGE_00 = '0.00%';
41
42
    const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd';
43
    const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd';
44
    const FORMAT_DATE_DDMMYYYY = 'dd/mm/yy';
45
    const FORMAT_DATE_DMYSLASH = 'd/m/y';
46
    const FORMAT_DATE_DMYMINUS = 'd-m-y';
47
    const FORMAT_DATE_DMMINUS = 'd-m';
48
    const FORMAT_DATE_MYMINUS = 'm-y';
49
    const FORMAT_DATE_XLSX14 = 'mm-dd-yy';
50
    const FORMAT_DATE_XLSX15 = 'd-mmm-yy';
51
    const FORMAT_DATE_XLSX16 = 'd-mmm';
52
    const FORMAT_DATE_XLSX17 = 'mmm-yy';
53
    const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm';
54
    const FORMAT_DATE_DATETIME = 'd/m/y h:mm';
55
    const FORMAT_DATE_TIME1 = 'h:mm AM/PM';
56
    const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM';
57
    const FORMAT_DATE_TIME3 = 'h:mm';
58
    const FORMAT_DATE_TIME4 = 'h:mm:ss';
59
    const FORMAT_DATE_TIME5 = 'mm:ss';
60
    const FORMAT_DATE_TIME6 = 'h:mm:ss';
61
    const FORMAT_DATE_TIME7 = 'i:s.S';
62
    const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
63
    const FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd;@';
64
65
    const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-';
66
    const FORMAT_CURRENCY_USD = '$#,##0_-';
67
    const FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-';
68
69
    /**
70
     * Excel built-in number formats.
71
     *
72
     * @var array
73
     */
74
    protected static $builtInFormats;
75
76
    /**
77
     * Excel built-in number formats (flipped, for faster lookups).
78
     *
79
     * @var array
80
     */
81
    protected static $flippedBuiltInFormats;
82
83
    /**
84
     * Format Code.
85
     *
86
     * @var string
87
     */
88
    protected $formatCode = self::FORMAT_GENERAL;
89
90
    /**
91
     * Built-in format Code.
92
     *
93
     * @var string
94
     */
95
    protected $builtInFormatCode = 0;
96
97
    /**
98
     * Create a new NumberFormat.
99
     *
100
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
101
     *                                    Leave this value at default unless you understand exactly what
102
     *                                        its ramifications are
103
     * @param bool $isConditional Flag indicating if this is a conditional style or not
104
     *                                    Leave this value at default unless you understand exactly what
105
     *                                        its ramifications are
106
     */
107 76
    public function __construct($isSupervisor = false, $isConditional = false)
108
    {
109
        // Supervisor?
110 76
        parent::__construct($isSupervisor);
111
112 76
        if ($isConditional) {
113 2
            $this->formatCode = null;
114 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...
115
        }
116 76
    }
117
118
    /**
119
     * Get the shared style component for the currently active cell in currently active sheet.
120
     * Only used for style supervisor.
121
     *
122
     * @return NumberFormat
123
     */
124 1
    public function getSharedComponent()
125
    {
126 1
        return $this->parent->getSharedComponent()->getNumberFormat();
127
    }
128
129
    /**
130
     * Build style array from subcomponents.
131
     *
132
     * @param array $array
133
     *
134
     * @return array
135
     */
136 27
    public function getStyleArray($array)
137
    {
138 27
        return ['numberformat' => $array];
139
    }
140
141
    /**
142
     * Apply styles from array.
143
     *
144
     * <code>
145
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray(
146
     *        array(
147
     *            'code' => \PhpOffice\PhpSpreadsheet\Style\NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE
148
     *        )
149
     * );
150
     * </code>
151
     *
152
     * @param array $pStyles Array containing style information
153
     *
154
     * @throws \PhpOffice\PhpSpreadsheet\Exception
155
     *
156
     * @return NumberFormat
157
     */
158 30
    public function applyFromArray(array $pStyles)
159
    {
160 30
        if ($this->isSupervisor) {
161
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
162
        } else {
163 30
            if (isset($pStyles['code'])) {
164 30
                $this->setFormatCode($pStyles['code']);
165
            }
166
        }
167
168 30
        return $this;
169
    }
170
171
    /**
172
     * Get Format Code.
173
     *
174
     * @return string
175
     */
176 28
    public function getFormatCode()
177
    {
178 28
        if ($this->isSupervisor) {
179 1
            return $this->getSharedComponent()->getFormatCode();
180
        }
181 28
        if ($this->builtInFormatCode !== false) {
182 19
            return self::builtInFormatCode($this->builtInFormatCode);
183
        }
184
185 24
        return $this->formatCode;
186
    }
187
188
    /**
189
     * Set Format Code.
190
     *
191
     * @param string $pValue see self::FORMAT_*
192
     *
193
     * @return NumberFormat
194
     */
195 40
    public function setFormatCode($pValue)
196
    {
197 40
        if ($pValue == '') {
198
            $pValue = self::FORMAT_GENERAL;
199
        }
200 40
        if ($this->isSupervisor) {
201 27
            $styleArray = $this->getStyleArray(['code' => $pValue]);
202 27
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
203
        } else {
204 40
            $this->formatCode = $pValue;
205 40
            $this->builtInFormatCode = self::builtInFormatCodeIndex($pValue);
0 ignored issues
show
Documentation Bug introduced by
It seems like self::builtInFormatCodeIndex($pValue) of type integer or boolean 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...
206
        }
207
208 40
        return $this;
209
    }
210
211
    /**
212
     * Get Built-In Format Code.
213
     *
214
     * @return int
215
     */
216 57
    public function getBuiltInFormatCode()
217
    {
218 57
        if ($this->isSupervisor) {
219
            return $this->getSharedComponent()->getBuiltInFormatCode();
220
        }
221
222 57
        return $this->builtInFormatCode;
223
    }
224
225
    /**
226
     * Set Built-In Format Code.
227
     *
228
     * @param int $pValue
229
     *
230
     * @return NumberFormat
231
     */
232
    public function setBuiltInFormatCode($pValue)
233
    {
234
        if ($this->isSupervisor) {
235
            $styleArray = $this->getStyleArray(['code' => self::builtInFormatCode($pValue)]);
236
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
237
        } else {
238
            $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...
239
            $this->formatCode = self::builtInFormatCode($pValue);
240
        }
241
242
        return $this;
243
    }
244
245
    /**
246
     * Fill built-in format codes.
247
     */
248 43
    private static function fillBuiltInFormatCodes()
249
    {
250
        //  [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance]
251
        //  18.8.30. numFmt (Number Format)
252
        //
253
        //  The ECMA standard defines built-in format IDs
254
        //      14: "mm-dd-yy"
255
        //      22: "m/d/yy h:mm"
256
        //      37: "#,##0 ;(#,##0)"
257
        //      38: "#,##0 ;[Red](#,##0)"
258
        //      39: "#,##0.00;(#,##0.00)"
259
        //      40: "#,##0.00;[Red](#,##0.00)"
260
        //      47: "mmss.0"
261
        //      KOR fmt 55: "yyyy-mm-dd"
262
        //  Excel defines built-in format IDs
263
        //      14: "m/d/yyyy"
264
        //      22: "m/d/yyyy h:mm"
265
        //      37: "#,##0_);(#,##0)"
266
        //      38: "#,##0_);[Red](#,##0)"
267
        //      39: "#,##0.00_);(#,##0.00)"
268
        //      40: "#,##0.00_);[Red](#,##0.00)"
269
        //      47: "mm:ss.0"
270
        //      KOR fmt 55: "yyyy/mm/dd"
271
272
        // Built-in format codes
273 43
        if (is_null(self::$builtInFormats)) {
274 36
            self::$builtInFormats = [];
275
276
            // General
277 36
            self::$builtInFormats[0] = self::FORMAT_GENERAL;
278 36
            self::$builtInFormats[1] = '0';
279 36
            self::$builtInFormats[2] = '0.00';
280 36
            self::$builtInFormats[3] = '#,##0';
281 36
            self::$builtInFormats[4] = '#,##0.00';
282
283 36
            self::$builtInFormats[9] = '0%';
284 36
            self::$builtInFormats[10] = '0.00%';
285 36
            self::$builtInFormats[11] = '0.00E+00';
286 36
            self::$builtInFormats[12] = '# ?/?';
287 36
            self::$builtInFormats[13] = '# ??/??';
288 36
            self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
289 36
            self::$builtInFormats[15] = 'd-mmm-yy';
290 36
            self::$builtInFormats[16] = 'd-mmm';
291 36
            self::$builtInFormats[17] = 'mmm-yy';
292 36
            self::$builtInFormats[18] = 'h:mm AM/PM';
293 36
            self::$builtInFormats[19] = 'h:mm:ss AM/PM';
294 36
            self::$builtInFormats[20] = 'h:mm';
295 36
            self::$builtInFormats[21] = 'h:mm:ss';
296 36
            self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
297
298 36
            self::$builtInFormats[37] = '#,##0_);(#,##0)'; //  Despite ECMA '#,##0 ;(#,##0)';
299 36
            self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; //  Despite ECMA '#,##0 ;[Red](#,##0)';
300 36
            self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; //  Despite ECMA '#,##0.00;(#,##0.00)';
301 36
            self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; //  Despite ECMA '#,##0.00;[Red](#,##0.00)';
302
303 36
            self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
304 36
            self::$builtInFormats[45] = 'mm:ss';
305 36
            self::$builtInFormats[46] = '[h]:mm:ss';
306 36
            self::$builtInFormats[47] = 'mm:ss.0'; //  Despite ECMA 'mmss.0';
307 36
            self::$builtInFormats[48] = '##0.0E+0';
308 36
            self::$builtInFormats[49] = '@';
309
310
            // CHT
311 36
            self::$builtInFormats[27] = '[$-404]e/m/d';
312 36
            self::$builtInFormats[30] = 'm/d/yy';
313 36
            self::$builtInFormats[36] = '[$-404]e/m/d';
314 36
            self::$builtInFormats[50] = '[$-404]e/m/d';
315 36
            self::$builtInFormats[57] = '[$-404]e/m/d';
316
317
            // THA
318 36
            self::$builtInFormats[59] = 't0';
319 36
            self::$builtInFormats[60] = 't0.00';
320 36
            self::$builtInFormats[61] = 't#,##0';
321 36
            self::$builtInFormats[62] = 't#,##0.00';
322 36
            self::$builtInFormats[67] = 't0%';
323 36
            self::$builtInFormats[68] = 't0.00%';
324 36
            self::$builtInFormats[69] = 't# ?/?';
325 36
            self::$builtInFormats[70] = 't# ??/??';
326
327
            // Flip array (for faster lookups)
328 36
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
329
        }
330 43
    }
331
332
    /**
333
     * Get built-in format code.
334
     *
335
     * @param int $pIndex
336
     *
337
     * @return string
338
     */
339 27
    public static function builtInFormatCode($pIndex)
340
    {
341
        // Clean parameter
342 27
        $pIndex = (int) $pIndex;
343
344
        // Ensure built-in format codes are available
345 27
        self::fillBuiltInFormatCodes();
346
347
        // Lookup format code
348 27
        if (isset(self::$builtInFormats[$pIndex])) {
349 27
            return self::$builtInFormats[$pIndex];
350
        }
351
352
        return '';
353
    }
354
355
    /**
356
     * Get built-in format code index.
357
     *
358
     * @param string $formatCode
359
     *
360
     * @return int|bool
361
     */
362 40
    public static function builtInFormatCodeIndex($formatCode)
363
    {
364
        // Ensure built-in format codes are available
365 40
        self::fillBuiltInFormatCodes();
366
367
        // Lookup format code
368 40
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
369 35
            return self::$flippedBuiltInFormats[$formatCode];
370
        }
371
372 31
        return false;
373
    }
374
375
    /**
376
     * Get hash code.
377
     *
378
     * @return string Hash code
379
     */
380 68
    public function getHashCode()
381
    {
382 68
        if ($this->isSupervisor) {
383
            return $this->getSharedComponent()->getHashCode();
384
        }
385
386 68
        return md5(
387 68
            $this->formatCode .
388 68
            $this->builtInFormatCode .
389 68
            __CLASS__
390
        );
391
    }
392
393
    /**
394
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
395
     *
396
     * @var array
397
     */
398
    private static $dateFormatReplacements = [
399
            // first remove escapes related to non-format characters
400
            '\\' => '',
401
            //    12-hour suffix
402
            'am/pm' => 'A',
403
            //    4-digit year
404
            'e' => 'Y',
405
            'yyyy' => 'Y',
406
            //    2-digit year
407
            'yy' => 'y',
408
            //    first letter of month - no php equivalent
409
            'mmmmm' => 'M',
410
            //    full month name
411
            'mmmm' => 'F',
412
            //    short month name
413
            'mmm' => 'M',
414
            //    mm is minutes if time, but can also be month w/leading zero
415
            //    so we try to identify times be the inclusion of a : separator in the mask
416
            //    It isn't perfect, but the best way I know how
417
            ':mm' => ':i',
418
            'mm:' => 'i:',
419
            //    month leading zero
420
            'mm' => 'm',
421
            //    month no leading zero
422
            'm' => 'n',
423
            //    full day of week name
424
            'dddd' => 'l',
425
            //    short day of week name
426
            'ddd' => 'D',
427
            //    days leading zero
428
            'dd' => 'd',
429
            //    days no leading zero
430
            'd' => 'j',
431
            //    seconds
432
            'ss' => 's',
433
            //    fractional seconds - no php equivalent
434
            '.s' => '',
435
        ];
436
    /**
437
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
438
     *
439
     * @var array
440
     */
441
    private static $dateFormatReplacements24 = [
442
            'hh' => 'H',
443
            'h' => 'G',
444
        ];
445
    /**
446
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
447
     *
448
     * @var array
449
     */
450
    private static $dateFormatReplacements12 = [
451
            'hh' => 'h',
452
            'h' => 'g',
453
        ];
454
455 18
    private static function setLowercaseCallback($matches)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
456
    {
457 18
        return mb_strtolower($matches[0]);
458
    }
459
460 7
    private static function escapeQuotesCallback($matches)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
461
    {
462 7
        return '\\' . implode('\\', str_split($matches[1]));
463
    }
464
465 18
    private static function formatAsDate(&$value, &$format)
466
    {
467
        // strip off first part containing e.g. [$-F800] or [$USD-409]
468
        // general syntax: [$<Currency string>-<language info>]
469
        // language info is in hexadecimal
470
        // strip off chinese part like [DBNum1][$-804]
471 18
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
472
473
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
474
        //    but we don't want to change any quoted strings
475 18
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
476
477
        // Only process the non-quoted blocks for date format characters
478 18
        $blocks = explode('"', $format);
479 18
        foreach ($blocks as $key => &$block) {
480 18
            if ($key % 2 == 0) {
481 18
                $block = strtr($block, self::$dateFormatReplacements);
482 18
                if (!strpos($block, 'A')) {
483
                    // 24-hour time format
484 18
                    $block = strtr($block, self::$dateFormatReplacements24);
485
                } else {
486
                    // 12-hour time format
487 18
                    $block = strtr($block, self::$dateFormatReplacements12);
488
                }
489
            }
490
        }
491 18
        $format = implode('"', $blocks);
492
493
        // escape any quoted characters so that DateTime format() will render them correctly
494 18
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
495
496 18
        $dateObj = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value);
497 18
        $value = $dateObj->format($format);
498 18
    }
499
500 3
    private static function formatAsPercentage(&$value, &$format)
501
    {
502 3
        if ($format === self::FORMAT_PERCENTAGE) {
503 3
            $value = round((100 * $value), 0) . '%';
504
        } else {
505
            if (preg_match('/\.[#0]+/i', $format, $m)) {
506
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
507
                $format = str_replace($m[0], $s, $format);
508
            }
509
            if (preg_match('/^[#0]+/', $format, $m)) {
510
                $format = str_replace($m[0], strlen($m[0]), $format);
511
            }
512
            $format = '%' . str_replace('%', 'f%%', $format);
513
514
            $value = sprintf($format, 100 * $value);
515
        }
516 3
    }
517
518 4
    private static function formatAsFraction(&$value, &$format)
519
    {
520 4
        $sign = ($value < 0) ? '-' : '';
521
522 4
        $integerPart = floor(abs($value));
523 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
524 4
        $decimalLength = strlen($decimalPart);
525 4
        $decimalDivisor = pow(10, $decimalLength);
526
527 4
        $GCD = \PhpOffice\PhpSpreadsheet\Calculation\MathTrig::GCD($decimalPart, $decimalDivisor);
528
529 4
        $adjustedDecimalPart = $decimalPart / $GCD;
530 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
531
532 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
533 3
            if ($integerPart == 0) {
534
                $integerPart = '';
535
            }
536 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
537
        } else {
538 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
539 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
540
        }
541 4
    }
542
543 6
    private static function complexNumberFormatMask($number, $mask, $level = 0)
0 ignored issues
show
Unused Code introduced by
The parameter $level is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
544
    {
545 6
        $sign = ($number < 0.0);
546 6
        $number = abs($number);
547 6
        if (strpos($mask, '.') !== false) {
548 2
            $numbers = explode('.', $number . '.0');
549 2
            $masks = explode('.', $mask . '.0');
550 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0], 1);
551 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), 1));
552
553 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
554
        }
555
556 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
557 6
        if ($r > 1) {
558 6
            $result = array_reverse($result[0]);
559
560 6
            foreach ($result as $block) {
561 6
                $divisor = 1 . $block[0];
562 6
                $size = strlen($block[0]);
563 6
                $offset = $block[1];
564
565 6
                $blockValue = sprintf(
566 6
                    '%0' . $size . 'd',
567
                    fmod($number, $divisor)
568
                );
569 6
                $number = floor($number / $divisor);
570 6
                $mask = substr_replace($mask, $blockValue, $offset, $size);
571
            }
572 6
            if ($number > 0) {
573 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...
574
            }
575 6
            $result = $mask;
576
        } else {
577 2
            $result = $number;
578
        }
579
580 6
        return (($sign) ? '-' : '') . $result;
581
    }
582
583
    /**
584
     * Convert a value in a pre-defined format to a PHP string.
585
     *
586
     * @param mixed $value Value to format
587
     * @param string $format Format code, see = self::FORMAT_*
588
     * @param array $callBack Callback function for additional formatting of string
589
     *
590
     * @return string Formatted string
591
     */
592 81
    public static function toFormattedString($value, $format, $callBack = null)
593
    {
594
        // For now we do not treat strings although section 4 of a format code affects strings
595 81
        if (!is_numeric($value)) {
596 19
            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...
597
        }
598
599
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
600
        // it seems to round numbers to a total of 10 digits.
601 70
        if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) {
602 7
            return $value;
603
        }
604
605
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
606 69
        $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
607
608
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
609 69
        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
610
611
        // Extract the relevant section depending on whether number is positive, negative, or zero?
612
        // Text not supported yet.
613
        // Here is how the sections apply to various values in Excel:
614
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
615
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
616
        //   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...
617
        //   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...
618 69
        switch (count($sections)) {
619 69
            case 1:
620 63
                $format = $sections[0];
621 63
                break;
622 6
            case 2:
623 6
                $format = ($value >= 0) ? $sections[0] : $sections[1];
624 6
                $value = abs($value); // Use the absolute value
625 6
                break;
626 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...
627
                $format = ($value > 0) ?
628
                    $sections[0] : (($value < 0) ?
629
                        $sections[1] : $sections[2]);
630
                $value = abs($value); // Use the absolute value
631
                break;
632 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...
633
                $format = ($value > 0) ?
634
                    $sections[0] : (($value < 0) ?
635
                        $sections[1] : $sections[2]);
636
                $value = abs($value); // Use the absolute value
637
                break;
638
            default:
639
                // something is wrong, just use first section
640
                $format = $sections[0];
641
                break;
642
        }
643
644
        // In Excel formats, "_" is used to add spacing,
645
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
646 69
        $format = preg_replace('/_./', ' ', $format);
647
648
        // Save format with color information for later use below
649 69
        $formatColor = $format;
650
651
        // Strip color information
652 69
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
653 69
        $format = preg_replace($color_regex, '', $format);
654
655
        // Let's begin inspecting the format and converting the value to a formatted string
656
657
        //  Check for date/time characters (not inside quotes)
658 69
        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
659
            // datetime format
660 18
            self::formatAsDate($value, $format);
661 56
        } elseif (preg_match('/%$/', $format)) {
662
            // % number format
663 3
            self::formatAsPercentage($value, $format);
664
        } else {
665 53
            if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
666
                $value = 'EUR ' . sprintf('%1.2f', $value);
667
            } else {
668
                // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
669 53
                $format = str_replace(['"', '*'], '', $format);
670
671
                // Find out if we need thousands separator
672
                // This is indicated by a comma enclosed by a digit placeholder:
673
                //        #,#   or   0,0
674 53
                $useThousands = preg_match('/(#,#|0,0)/', $format);
675 53
                if ($useThousands) {
676 21
                    $format = preg_replace('/0,0/', '00', $format);
677 21
                    $format = preg_replace('/#,#/', '##', $format);
678
                }
679
680
                // Scale thousands, millions,...
681
                // This is indicated by a number of commas after a digit placeholder:
682
                //        #,   or    0.0,,
683 53
                $scale = 1; // same as no scale
684 53
                $matches = [];
685 53
                if (preg_match('/(#|0)(,+)/', $format, $matches)) {
686 2
                    $scale = pow(1000, strlen($matches[2]));
687
688
                    // strip the commas
689 2
                    $format = preg_replace('/0,+/', '0', $format);
690 2
                    $format = preg_replace('/#,+/', '#', $format);
691
                }
692
693 53
                if (preg_match('/#?.*\?\/\?/', $format, $m)) {
694 4
                    if ($value != (int) $value) {
695 4
                        self::formatAsFraction($value, $format);
696
                    }
697
                } else {
698
                    // Handle the number itself
699
700
                    // scale number
701 49
                    $value = $value / $scale;
702
703
                    // Strip #
704 49
                    $format = preg_replace('/\\#/', '0', $format);
705
706 49
                    $n = "/\[[^\]]+\]/";
707 49
                    $m = preg_replace($n, '', $format);
708 49
                    $number_regex = "/(0+)(\.?)(0*)/";
709 49
                    if (preg_match($number_regex, $m, $matches)) {
710 49
                        $left = $matches[1];
711 49
                        $dec = $matches[2];
712 49
                        $right = $matches[3];
713
714
                        // minimun width of formatted number (including dot)
715 49
                        $minWidth = strlen($left) + strlen($dec) + strlen($right);
716 49
                        if ($useThousands) {
717 21
                            $value = number_format(
718
                                $value,
719
                                strlen($right),
720 21
                                \PhpOffice\PhpSpreadsheet\Shared\StringHelper::getDecimalSeparator(),
721 21
                                \PhpOffice\PhpSpreadsheet\Shared\StringHelper::getThousandsSeparator()
722
                            );
723 21
                            $value = preg_replace($number_regex, $value, $format);
724
                        } else {
725 28
                            if (preg_match('/[0#]E[+-]0/i', $format)) {
726
                                //    Scientific format
727 3
                                $value = sprintf('%5.2E', $value);
728 25
                            } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
729 6
                                $value = self::complexNumberFormatMask($value, $format);
730
                            } else {
731 19
                                $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
732 19
                                $value = sprintf($sprintf_pattern, $value);
733 19
                                $value = preg_replace($number_regex, $value, $format);
734
                            }
735
                        }
736
                    }
737
                }
738 53
                if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
739
                    //  Currency or Accounting
740 5
                    $currencyFormat = $m[0];
0 ignored issues
show
Unused Code introduced by
$currencyFormat is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
741 5
                    $currencyCode = $m[1];
742 5
                    list($currencyCode) = explode('-', $currencyCode);
743 5
                    if ($currencyCode == '') {
744
                        $currencyCode = \PhpOffice\PhpSpreadsheet\Shared\StringHelper::getCurrencyCode();
745
                    }
746 5
                    $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
747
                }
748
            }
749
        }
750
751
        // Escape any escaped slashes to a single slash
752 69
        $format = preg_replace('/\\\\/u', '\\', $format);
0 ignored issues
show
Unused Code introduced by
$format is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
753
754
        // Additional formatting provided by callback function
755 69
        if ($callBack !== null) {
756 4
            list($writerInstance, $function) = $callBack;
757 4
            $value = $writerInstance->$function($value, $formatColor);
758
        }
759
760 69
        return $value;
761
    }
762
}
763