Completed
Push — develop ( 44e246...6e4e0a )
by Adrien
19:07
created

NumberFormat::setFormatCode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 1
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
ccs 8
cts 9
cp 0.8889
crap 3.0123
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
/**
12
 * Copyright (c) 2006 - 2016 PhpSpreadsheet.
13
 *
14
 * This library is free software; you can redistribute it and/or
15
 * modify it under the terms of the GNU Lesser General Public
16
 * License as published by the Free Software Foundation; either
17
 * version 2.1 of the License, or (at your option) any later version.
18
 *
19
 * This library is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22
 * Lesser General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Lesser General Public
25
 * License along with this library; if not, write to the Free Software
26
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
27
 *
28
 * @category   PhpSpreadsheet
29
 *
30
 * @copyright  Copyright (c) 2006 - 2016 PhpSpreadsheet (https://github.com/PHPOffice/PhpSpreadsheet)
31
 * @license    http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt    LGPL
32
 */
33
class NumberFormat extends Supervisor implements IComparable
34
{
35
    /* Pre-defined formats */
36
    const FORMAT_GENERAL = 'General';
37
38
    const FORMAT_TEXT = '@';
39
40
    const FORMAT_NUMBER = '0';
41
    const FORMAT_NUMBER_00 = '0.00';
42
    const FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00';
43
    const FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-';
44
45
    const FORMAT_PERCENTAGE = '0%';
46
    const FORMAT_PERCENTAGE_00 = '0.00%';
47
48
    const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd';
49
    const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd';
50
    const FORMAT_DATE_DDMMYYYY = 'dd/mm/yy';
51
    const FORMAT_DATE_DMYSLASH = 'd/m/y';
52
    const FORMAT_DATE_DMYMINUS = 'd-m-y';
53
    const FORMAT_DATE_DMMINUS = 'd-m';
54
    const FORMAT_DATE_MYMINUS = 'm-y';
55
    const FORMAT_DATE_XLSX14 = 'mm-dd-yy';
56
    const FORMAT_DATE_XLSX15 = 'd-mmm-yy';
57
    const FORMAT_DATE_XLSX16 = 'd-mmm';
58
    const FORMAT_DATE_XLSX17 = 'mmm-yy';
59
    const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm';
60
    const FORMAT_DATE_DATETIME = 'd/m/y h:mm';
61
    const FORMAT_DATE_TIME1 = 'h:mm AM/PM';
62
    const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM';
63
    const FORMAT_DATE_TIME3 = 'h:mm';
64
    const FORMAT_DATE_TIME4 = 'h:mm:ss';
65
    const FORMAT_DATE_TIME5 = 'mm:ss';
66
    const FORMAT_DATE_TIME6 = 'h:mm:ss';
67
    const FORMAT_DATE_TIME7 = 'i:s.S';
68
    const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
69
    const FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd;@';
70
71
    const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-';
72
    const FORMAT_CURRENCY_USD = '$#,##0_-';
73
    const FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-';
74
75
    /**
76
     * Excel built-in number formats.
77
     *
78
     * @var array
79
     */
80
    protected static $builtInFormats;
81
82
    /**
83
     * Excel built-in number formats (flipped, for faster lookups).
84
     *
85
     * @var array
86
     */
87
    protected static $flippedBuiltInFormats;
88
89
    /**
90
     * Format Code.
91
     *
92
     * @var string
93
     */
94
    protected $formatCode = self::FORMAT_GENERAL;
95
96
    /**
97
     * Built-in format Code.
98
     *
99
     * @var string
100
     */
101
    protected $builtInFormatCode = 0;
102
103
    /**
104
     * Create a new NumberFormat.
105
     *
106
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
107
     *                                    Leave this value at default unless you understand exactly what
108
     *                                        its ramifications are
109
     * @param bool $isConditional Flag indicating if this is a conditional style or not
110
     *                                    Leave this value at default unless you understand exactly what
111
     *                                        its ramifications are
112
     */
113 76
    public function __construct($isSupervisor = false, $isConditional = false)
114
    {
115
        // Supervisor?
116 76
        parent::__construct($isSupervisor);
117
118 76
        if ($isConditional) {
119 2
            $this->formatCode = null;
120 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...
121
        }
122 76
    }
123
124
    /**
125
     * Get the shared style component for the currently active cell in currently active sheet.
126
     * Only used for style supervisor.
127
     *
128
     * @return NumberFormat
129
     */
130 1
    public function getSharedComponent()
131
    {
132 1
        return $this->parent->getSharedComponent()->getNumberFormat();
133
    }
134
135
    /**
136
     * Build style array from subcomponents.
137
     *
138
     * @param array $array
139
     *
140
     * @return array
141
     */
142 27
    public function getStyleArray($array)
143
    {
144 27
        return ['numberformat' => $array];
145
    }
146
147
    /**
148
     * Apply styles from array.
149
     * <code>
150
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray(
151
     *        array(
152
     *            'code' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE
153
     *        )
154
     * );
155
     * </code>.
156
     *
157
     * @param array $pStyles Array containing style information
158
     *
159
     * @throws PhpSpreadsheetException
160
     *
161
     * @return NumberFormat
162
     */
163 30
    public function applyFromArray(array $pStyles)
164
    {
165 30
        if ($this->isSupervisor) {
166
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
167
        } else {
168 30
            if (isset($pStyles['code'])) {
169 30
                $this->setFormatCode($pStyles['code']);
170
            }
171
        }
172
173 30
        return $this;
174
    }
175
176
    /**
177
     * Get Format Code.
178
     *
179
     * @return string
180
     */
181 28
    public function getFormatCode()
182
    {
183 28
        if ($this->isSupervisor) {
184 1
            return $this->getSharedComponent()->getFormatCode();
185
        }
186 28
        if ($this->builtInFormatCode !== false) {
187 19
            return self::builtInFormatCode($this->builtInFormatCode);
188
        }
189
190 24
        return $this->formatCode;
191
    }
192
193
    /**
194
     * Set Format Code.
195
     *
196
     * @param string $pValue see self::FORMAT_*
197
     *
198
     * @return NumberFormat
199
     */
200 40
    public function setFormatCode($pValue)
201
    {
202 40
        if ($pValue == '') {
203
            $pValue = self::FORMAT_GENERAL;
204
        }
205 40
        if ($this->isSupervisor) {
206 27
            $styleArray = $this->getStyleArray(['code' => $pValue]);
207 27
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
208
        } else {
209 40
            $this->formatCode = $pValue;
210 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...
211
        }
212
213 40
        return $this;
214
    }
215
216
    /**
217
     * Get Built-In Format Code.
218
     *
219
     * @return int
220
     */
221 57
    public function getBuiltInFormatCode()
222
    {
223 57
        if ($this->isSupervisor) {
224
            return $this->getSharedComponent()->getBuiltInFormatCode();
225
        }
226
227 57
        return $this->builtInFormatCode;
228
    }
229
230
    /**
231
     * Set Built-In Format Code.
232
     *
233
     * @param int $pValue
234
     *
235
     * @return NumberFormat
236
     */
237
    public function setBuiltInFormatCode($pValue)
238
    {
239
        if ($this->isSupervisor) {
240
            $styleArray = $this->getStyleArray(['code' => self::builtInFormatCode($pValue)]);
241
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
242
        } else {
243
            $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...
244
            $this->formatCode = self::builtInFormatCode($pValue);
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Fill built-in format codes.
252
     */
253 43
    private static function fillBuiltInFormatCodes()
254
    {
255
        //  [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance]
256
        //  18.8.30. numFmt (Number Format)
257
        //
258
        //  The ECMA standard defines built-in format IDs
259
        //      14: "mm-dd-yy"
260
        //      22: "m/d/yy h:mm"
261
        //      37: "#,##0 ;(#,##0)"
262
        //      38: "#,##0 ;[Red](#,##0)"
263
        //      39: "#,##0.00;(#,##0.00)"
264
        //      40: "#,##0.00;[Red](#,##0.00)"
265
        //      47: "mmss.0"
266
        //      KOR fmt 55: "yyyy-mm-dd"
267
        //  Excel defines built-in format IDs
268
        //      14: "m/d/yyyy"
269
        //      22: "m/d/yyyy h:mm"
270
        //      37: "#,##0_);(#,##0)"
271
        //      38: "#,##0_);[Red](#,##0)"
272
        //      39: "#,##0.00_);(#,##0.00)"
273
        //      40: "#,##0.00_);[Red](#,##0.00)"
274
        //      47: "mm:ss.0"
275
        //      KOR fmt 55: "yyyy/mm/dd"
276
277
        // Built-in format codes
278 43
        if (is_null(self::$builtInFormats)) {
279 36
            self::$builtInFormats = [];
280
281
            // General
282 36
            self::$builtInFormats[0] = self::FORMAT_GENERAL;
283 36
            self::$builtInFormats[1] = '0';
284 36
            self::$builtInFormats[2] = '0.00';
285 36
            self::$builtInFormats[3] = '#,##0';
286 36
            self::$builtInFormats[4] = '#,##0.00';
287
288 36
            self::$builtInFormats[9] = '0%';
289 36
            self::$builtInFormats[10] = '0.00%';
290 36
            self::$builtInFormats[11] = '0.00E+00';
291 36
            self::$builtInFormats[12] = '# ?/?';
292 36
            self::$builtInFormats[13] = '# ??/??';
293 36
            self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
294 36
            self::$builtInFormats[15] = 'd-mmm-yy';
295 36
            self::$builtInFormats[16] = 'd-mmm';
296 36
            self::$builtInFormats[17] = 'mmm-yy';
297 36
            self::$builtInFormats[18] = 'h:mm AM/PM';
298 36
            self::$builtInFormats[19] = 'h:mm:ss AM/PM';
299 36
            self::$builtInFormats[20] = 'h:mm';
300 36
            self::$builtInFormats[21] = 'h:mm:ss';
301 36
            self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
302
303 36
            self::$builtInFormats[37] = '#,##0_);(#,##0)'; //  Despite ECMA '#,##0 ;(#,##0)';
304 36
            self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; //  Despite ECMA '#,##0 ;[Red](#,##0)';
305 36
            self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; //  Despite ECMA '#,##0.00;(#,##0.00)';
306 36
            self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; //  Despite ECMA '#,##0.00;[Red](#,##0.00)';
307
308 36
            self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
309 36
            self::$builtInFormats[45] = 'mm:ss';
310 36
            self::$builtInFormats[46] = '[h]:mm:ss';
311 36
            self::$builtInFormats[47] = 'mm:ss.0'; //  Despite ECMA 'mmss.0';
312 36
            self::$builtInFormats[48] = '##0.0E+0';
313 36
            self::$builtInFormats[49] = '@';
314
315
            // CHT
316 36
            self::$builtInFormats[27] = '[$-404]e/m/d';
317 36
            self::$builtInFormats[30] = 'm/d/yy';
318 36
            self::$builtInFormats[36] = '[$-404]e/m/d';
319 36
            self::$builtInFormats[50] = '[$-404]e/m/d';
320 36
            self::$builtInFormats[57] = '[$-404]e/m/d';
321
322
            // THA
323 36
            self::$builtInFormats[59] = 't0';
324 36
            self::$builtInFormats[60] = 't0.00';
325 36
            self::$builtInFormats[61] = 't#,##0';
326 36
            self::$builtInFormats[62] = 't#,##0.00';
327 36
            self::$builtInFormats[67] = 't0%';
328 36
            self::$builtInFormats[68] = 't0.00%';
329 36
            self::$builtInFormats[69] = 't# ?/?';
330 36
            self::$builtInFormats[70] = 't# ??/??';
331
332
            // Flip array (for faster lookups)
333 36
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
334
        }
335 43
    }
336
337
    /**
338
     * Get built-in format code.
339
     *
340
     * @param int $pIndex
341
     *
342
     * @return string
343
     */
344 27
    public static function builtInFormatCode($pIndex)
345
    {
346
        // Clean parameter
347 27
        $pIndex = (int) $pIndex;
348
349
        // Ensure built-in format codes are available
350 27
        self::fillBuiltInFormatCodes();
351
352
        // Lookup format code
353 27
        if (isset(self::$builtInFormats[$pIndex])) {
354 27
            return self::$builtInFormats[$pIndex];
355
        }
356
357 1
        return '';
358
    }
359
360
    /**
361
     * Get built-in format code index.
362
     *
363
     * @param string $formatCode
364
     *
365
     * @return int|bool
366
     */
367 40
    public static function builtInFormatCodeIndex($formatCode)
368
    {
369
        // Ensure built-in format codes are available
370 40
        self::fillBuiltInFormatCodes();
371
372
        // Lookup format code
373 40
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
374 35
            return self::$flippedBuiltInFormats[$formatCode];
375
        }
376
377 30
        return false;
378
    }
379
380
    /**
381
     * Get hash code.
382
     *
383
     * @return string Hash code
384
     */
385 68
    public function getHashCode()
386
    {
387 68
        if ($this->isSupervisor) {
388
            return $this->getSharedComponent()->getHashCode();
389
        }
390
391 68
        return md5(
392 68
            $this->formatCode .
393 68
            $this->builtInFormatCode .
394 68
            __CLASS__
395
        );
396
    }
397
398
    /**
399
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
400
     *
401
     * @var array
402
     */
403
    private static $dateFormatReplacements = [
404
            // first remove escapes related to non-format characters
405
            '\\' => '',
406
            //    12-hour suffix
407
            'am/pm' => 'A',
408
            //    4-digit year
409
            'e' => 'Y',
410
            'yyyy' => 'Y',
411
            //    2-digit year
412
            'yy' => 'y',
413
            //    first letter of month - no php equivalent
414
            'mmmmm' => 'M',
415
            //    full month name
416
            'mmmm' => 'F',
417
            //    short month name
418
            'mmm' => 'M',
419
            //    mm is minutes if time, but can also be month w/leading zero
420
            //    so we try to identify times be the inclusion of a : separator in the mask
421
            //    It isn't perfect, but the best way I know how
422
            ':mm' => ':i',
423
            'mm:' => 'i:',
424
            //    month leading zero
425
            'mm' => 'm',
426
            //    month no leading zero
427
            'm' => 'n',
428
            //    full day of week name
429
            'dddd' => 'l',
430
            //    short day of week name
431
            'ddd' => 'D',
432
            //    days leading zero
433
            'dd' => 'd',
434
            //    days no leading zero
435
            'd' => 'j',
436
            //    seconds
437
            'ss' => 's',
438
            //    fractional seconds - no php equivalent
439
            '.s' => '',
440
        ];
441
    /**
442
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
443
     *
444
     * @var array
445
     */
446
    private static $dateFormatReplacements24 = [
447
            'hh' => 'H',
448
            'h' => 'G',
449
        ];
450
    /**
451
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
452
     *
453
     * @var array
454
     */
455
    private static $dateFormatReplacements12 = [
456
            'hh' => 'h',
457
            'h' => 'g',
458
        ];
459
460 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...
461
    {
462 18
        return mb_strtolower($matches[0]);
463
    }
464
465 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...
466
    {
467 7
        return '\\' . implode('\\', str_split($matches[1]));
468
    }
469
470 18
    private static function formatAsDate(&$value, &$format)
471
    {
472
        // strip off first part containing e.g. [$-F800] or [$USD-409]
473
        // general syntax: [$<Currency string>-<language info>]
474
        // language info is in hexadecimal
475
        // strip off chinese part like [DBNum1][$-804]
476 18
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
477
478
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
479
        //    but we don't want to change any quoted strings
480 18
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
481
482
        // Only process the non-quoted blocks for date format characters
483 18
        $blocks = explode('"', $format);
484 18
        foreach ($blocks as $key => &$block) {
485 18
            if ($key % 2 == 0) {
486 18
                $block = strtr($block, self::$dateFormatReplacements);
487 18
                if (!strpos($block, 'A')) {
488
                    // 24-hour time format
489 18
                    $block = strtr($block, self::$dateFormatReplacements24);
490
                } else {
491
                    // 12-hour time format
492 18
                    $block = strtr($block, self::$dateFormatReplacements12);
493
                }
494
            }
495
        }
496 18
        $format = implode('"', $blocks);
497
498
        // escape any quoted characters so that DateTime format() will render them correctly
499 18
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
500
501 18
        $dateObj = Date::excelToDateTimeObject($value);
502 18
        $value = $dateObj->format($format);
503 18
    }
504
505 3
    private static function formatAsPercentage(&$value, &$format)
506
    {
507 3
        if ($format === self::FORMAT_PERCENTAGE) {
508 3
            $value = round((100 * $value), 0) . '%';
509
        } else {
510
            if (preg_match('/\.[#0]+/i', $format, $m)) {
511
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
512
                $format = str_replace($m[0], $s, $format);
513
            }
514
            if (preg_match('/^[#0]+/', $format, $m)) {
515
                $format = str_replace($m[0], strlen($m[0]), $format);
516
            }
517
            $format = '%' . str_replace('%', 'f%%', $format);
518
519
            $value = sprintf($format, 100 * $value);
520
        }
521 3
    }
522
523 4
    private static function formatAsFraction(&$value, &$format)
524
    {
525 4
        $sign = ($value < 0) ? '-' : '';
526
527 4
        $integerPart = floor(abs($value));
528 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
529 4
        $decimalLength = strlen($decimalPart);
530 4
        $decimalDivisor = pow(10, $decimalLength);
531
532 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
533
534 4
        $adjustedDecimalPart = $decimalPart / $GCD;
535 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
536
537 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
538 3
            if ($integerPart == 0) {
539
                $integerPart = '';
540
            }
541 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
542
        } else {
543 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
544 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
545
        }
546 4
    }
547
548 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...
549
    {
550 6
        $sign = ($number < 0.0);
551 6
        $number = abs($number);
552 6
        if (strpos($mask, '.') !== false) {
553 2
            $numbers = explode('.', $number . '.0');
554 2
            $masks = explode('.', $mask . '.0');
555 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0], 1);
556 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), 1));
557
558 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
559
        }
560
561 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
562 6
        if ($r > 1) {
563 6
            $result = array_reverse($result[0]);
564
565 6
            foreach ($result as $block) {
566 6
                $divisor = 1 . $block[0];
567 6
                $size = strlen($block[0]);
568 6
                $offset = $block[1];
569
570 6
                $blockValue = sprintf(
571 6
                    '%0' . $size . 'd',
572
                    fmod($number, $divisor)
573
                );
574 6
                $number = floor($number / $divisor);
575 6
                $mask = substr_replace($mask, $blockValue, $offset, $size);
576
            }
577 6
            if ($number > 0) {
578 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...
579
            }
580 6
            $result = $mask;
581
        } else {
582 2
            $result = $number;
583
        }
584
585 6
        return (($sign) ? '-' : '') . $result;
586
    }
587
588
    /**
589
     * Convert a value in a pre-defined format to a PHP string.
590
     *
591
     * @param mixed $value Value to format
592
     * @param string $format Format code, see = self::FORMAT_*
593
     * @param array $callBack Callback function for additional formatting of string
594
     *
595
     * @return string Formatted string
596
     */
597 81
    public static function toFormattedString($value, $format, $callBack = null)
598
    {
599
        // For now we do not treat strings although section 4 of a format code affects strings
600 81
        if (!is_numeric($value)) {
601 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...
602
        }
603
604
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
605
        // it seems to round numbers to a total of 10 digits.
606 70
        if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) {
607 7
            return $value;
608
        }
609
610
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
611 69
        $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
612
613
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
614 69
        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
615
616
        // Extract the relevant section depending on whether number is positive, negative, or zero?
617
        // Text not supported yet.
618
        // Here is how the sections apply to various values in Excel:
619
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
620
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
621
        //   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...
622
        //   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...
623 69
        switch (count($sections)) {
624 69
            case 1:
625 63
                $format = $sections[0];
626 63
                break;
627 6
            case 2:
628 6
                $format = ($value >= 0) ? $sections[0] : $sections[1];
629 6
                $value = abs($value); // Use the absolute value
630 6
                break;
631 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...
632
                $format = ($value > 0) ?
633
                    $sections[0] : (($value < 0) ?
634
                        $sections[1] : $sections[2]);
635
                $value = abs($value); // Use the absolute value
636
                break;
637 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...
638
                $format = ($value > 0) ?
639
                    $sections[0] : (($value < 0) ?
640
                        $sections[1] : $sections[2]);
641
                $value = abs($value); // Use the absolute value
642
                break;
643
            default:
644
                // something is wrong, just use first section
645
                $format = $sections[0];
646
                break;
647
        }
648
649
        // In Excel formats, "_" is used to add spacing,
650
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
651 69
        $format = preg_replace('/_./', ' ', $format);
652
653
        // Save format with color information for later use below
654 69
        $formatColor = $format;
655
656
        // Strip color information
657 69
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
658 69
        $format = preg_replace($color_regex, '', $format);
659
660
        // Let's begin inspecting the format and converting the value to a formatted string
661
662
        //  Check for date/time characters (not inside quotes)
663 69
        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
664
            // datetime format
665 18
            self::formatAsDate($value, $format);
666 56
        } elseif (preg_match('/%$/', $format)) {
667
            // % number format
668 3
            self::formatAsPercentage($value, $format);
669
        } else {
670 53
            if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
671
                $value = 'EUR ' . sprintf('%1.2f', $value);
672
            } else {
673
                // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
674 53
                $format = str_replace(['"', '*'], '', $format);
675
676
                // Find out if we need thousands separator
677
                // This is indicated by a comma enclosed by a digit placeholder:
678
                //        #,#   or   0,0
679 53
                $useThousands = preg_match('/(#,#|0,0)/', $format);
680 53
                if ($useThousands) {
681 21
                    $format = preg_replace('/0,0/', '00', $format);
682 21
                    $format = preg_replace('/#,#/', '##', $format);
683
                }
684
685
                // Scale thousands, millions,...
686
                // This is indicated by a number of commas after a digit placeholder:
687
                //        #,   or    0.0,,
688 53
                $scale = 1; // same as no scale
689 53
                $matches = [];
690 53
                if (preg_match('/(#|0)(,+)/', $format, $matches)) {
691 2
                    $scale = pow(1000, strlen($matches[2]));
692
693
                    // strip the commas
694 2
                    $format = preg_replace('/0,+/', '0', $format);
695 2
                    $format = preg_replace('/#,+/', '#', $format);
696
                }
697
698 53
                if (preg_match('/#?.*\?\/\?/', $format, $m)) {
699 4
                    if ($value != (int) $value) {
700 4
                        self::formatAsFraction($value, $format);
701
                    }
702
                } else {
703
                    // Handle the number itself
704
705
                    // scale number
706 49
                    $value = $value / $scale;
707
708
                    // Strip #
709 49
                    $format = preg_replace('/\\#/', '0', $format);
710
711 49
                    $n = "/\[[^\]]+\]/";
712 49
                    $m = preg_replace($n, '', $format);
713 49
                    $number_regex = "/(0+)(\.?)(0*)/";
714 49
                    if (preg_match($number_regex, $m, $matches)) {
715 49
                        $left = $matches[1];
716 49
                        $dec = $matches[2];
717 49
                        $right = $matches[3];
718
719
                        // minimun width of formatted number (including dot)
720 49
                        $minWidth = strlen($left) + strlen($dec) + strlen($right);
721 49
                        if ($useThousands) {
722 21
                            $value = number_format(
723
                                $value,
724
                                strlen($right),
725 21
                                StringHelper::getDecimalSeparator(),
726 21
                                StringHelper::getThousandsSeparator()
727
                            );
728 21
                            $value = preg_replace($number_regex, $value, $format);
729
                        } else {
730 28
                            if (preg_match('/[0#]E[+-]0/i', $format)) {
731
                                //    Scientific format
732 3
                                $value = sprintf('%5.2E', $value);
733 25
                            } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
734 6
                                $value = self::complexNumberFormatMask($value, $format);
735
                            } else {
736 19
                                $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
737 19
                                $value = sprintf($sprintf_pattern, $value);
738 19
                                $value = preg_replace($number_regex, $value, $format);
739
                            }
740
                        }
741
                    }
742
                }
743 53
                if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
744
                    //  Currency or Accounting
745 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...
746 5
                    $currencyCode = $m[1];
747 5
                    list($currencyCode) = explode('-', $currencyCode);
748 5
                    if ($currencyCode == '') {
749
                        $currencyCode = StringHelper::getCurrencyCode();
750
                    }
751 5
                    $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
752
                }
753
            }
754
        }
755
756
        // Escape any escaped slashes to a single slash
757 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...
758
759
        // Additional formatting provided by callback function
760 69
        if ($callBack !== null) {
761 4
            list($writerInstance, $function) = $callBack;
762 4
            $value = $writerInstance->$function($value, $formatColor);
763
        }
764
765 69
        return $value;
766
    }
767
}
768