Completed
Push — develop ( f99eb8...c5339b )
by Adrien
31:45
created

NumberFormat::formatAsDate()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0047

Importance

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