Completed
Push — develop ( 8380fb...ce4b86 )
by Adrien
34:13
created

NumberFormat   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 735
Duplicated Lines 0 %

Test Coverage

Coverage 88.35%

Importance

Changes 0
Metric Value
wmc 75
dl 0
loc 735
ccs 235
cts 266
cp 0.8835
rs 1.8461
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getSharedComponent() 0 3 1
A getStyleArray() 0 3 1
A __construct() 0 8 2
A getBuiltInFormatCode() 0 7 2
A setBuiltInFormatCode() 0 11 2
A getHashCode() 0 10 2
A builtInFormatCodeIndex() 0 11 2
B fillBuiltInFormatCodes() 0 81 2
B formatAsDate() 0 33 4
F toFormattedString() 0 167 27
A builtInFormatCode() 0 14 2
A escapeQuotesCallback() 0 3 1
A formatAsPercentage() 0 15 4
A setLowercaseCallback() 0 3 1
A applyFromArray() 0 11 3
C complexNumberFormatMask() 0 38 7
A setFormatCode() 0 14 3
A getFormatCode() 0 10 3
B formatAsFraction() 0 22 6

How to fix   Complexity   

Complex Class

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

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

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

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

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

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

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
99
        }
100 172
    }
101
102
    /**
103
     * Get the shared style component for the currently active cell in currently active sheet.
104
     * Only used for style supervisor.
105
     *
106
     * @return NumberFormat
107
     */
108 5
    public function getSharedComponent()
109
    {
110 5
        return $this->parent->getSharedComponent()->getNumberFormat();
1 ignored issue
show
Bug introduced by
The method getSharedComponent() does not exist on PhpOffice\PhpSpreadsheet\Spreadsheet. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

110
        return $this->parent->/** @scrutinizer ignore-call */ getSharedComponent()->getNumberFormat();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
111
    }
112
113
    /**
114
     * Build style array from subcomponents.
115
     *
116
     * @param array $array
117
     *
118
     * @return array
119
     */
120 34
    public function getStyleArray($array)
121
    {
122 34
        return ['numberFormat' => $array];
123
    }
124
125
    /**
126
     * Apply styles from array.
127
     *
128
     * <code>
129
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray(
130
     *     [
131
     *         'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE
132
     *     ]
133
     * );
134
     * </code>
135
     *
136
     * @param array $pStyles Array containing style information
137
     *
138
     * @throws PhpSpreadsheetException
139
     *
140
     * @return NumberFormat
141
     */
142 38
    public function applyFromArray(array $pStyles)
143
    {
144 38
        if ($this->isSupervisor) {
145
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
146
        } else {
147 38
            if (isset($pStyles['formatCode'])) {
148 38
                $this->setFormatCode($pStyles['formatCode']);
149
            }
150
        }
151
152 38
        return $this;
153
    }
154
155
    /**
156
     * Get Format Code.
157
     *
158
     * @return string
159
     */
160 61
    public function getFormatCode()
161
    {
162 61
        if ($this->isSupervisor) {
163 5
            return $this->getSharedComponent()->getFormatCode();
164
        }
165 61
        if ($this->builtInFormatCode !== false) {
0 ignored issues
show
introduced by
The condition $this->builtInFormatCode !== false can never be false.
Loading history...
166 51
            return self::builtInFormatCode($this->builtInFormatCode);
0 ignored issues
show
Bug introduced by
$this->builtInFormatCode of type string is incompatible with the type integer expected by parameter $pIndex of PhpOffice\PhpSpreadsheet...at::builtInFormatCode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

166
            return self::builtInFormatCode(/** @scrutinizer ignore-type */ $this->builtInFormatCode);
Loading history...
167
        }
168
169 34
        return $this->formatCode;
170
    }
171
172
    /**
173
     * Set Format Code.
174
     *
175
     * @param string $pValue see self::FORMAT_*
176
     *
177
     * @return NumberFormat
178
     */
179 86
    public function setFormatCode($pValue)
180
    {
181 86
        if ($pValue == '') {
182 1
            $pValue = self::FORMAT_GENERAL;
183
        }
184 86
        if ($this->isSupervisor) {
185 34
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
186 34
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
187
        } else {
188 86
            $this->formatCode = $pValue;
189 86
            $this->builtInFormatCode = self::builtInFormatCodeIndex($pValue);
0 ignored issues
show
Documentation Bug introduced by
It seems like self::builtInFormatCodeIndex($pValue) can also be of type boolean. However, the property $builtInFormatCode is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

553
                    fmod($number, /** @scrutinizer ignore-type */ $divisor)
Loading history...
554
                );
555 6
                $number = floor($number / $divisor);
556 6
                $mask = substr_replace($mask, $blockValue, $offset, $size);
557
            }
558 6
            if ($number > 0) {
559 4
                $mask = substr_replace($mask, $number, $offset, 0);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $offset seems to be defined by a foreach iteration on line 546. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
560
            }
561 6
            $result = $mask;
562
        } else {
563 2
            $result = $number;
564
        }
565
566 6
        return (($sign) ? '-' : '') . $result;
567
    }
568
569
    /**
570
     * Convert a value in a pre-defined format to a PHP string.
571
     *
572
     * @param mixed $value Value to format
573
     * @param string $format Format code, see = self::FORMAT_*
574
     * @param array $callBack Callback function for additional formatting of string
575
     *
576
     * @return string Formatted string
577
     */
578 114
    public static function toFormattedString($value, $format, $callBack = null)
579
    {
580
        // For now we do not treat strings although section 4 of a format code affects strings
581 114
        if (!is_numeric($value)) {
582 50
            return $value;
583
        }
584
585
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
586
        // it seems to round numbers to a total of 10 digits.
587 94
        if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) {
588 27
            return $value;
589
        }
590
591
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
592 78
        $format = preg_replace('/(\\\(.))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
593
594
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
595 78
        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
596
597
        // Extract the relevant section depending on whether number is positive, negative, or zero?
598
        // Text not supported yet.
599
        // Here is how the sections apply to various values in Excel:
600
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
601
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
602
        //   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...
603
        //   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...
604 78
        switch (count($sections)) {
1 ignored issue
show
Bug introduced by
It seems like $sections can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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