Test Failed
Push — develop ( 90366f...812a46 )
by Adrien
28:16
created

NumberFormat::applyFromArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
crap 3.0416
rs 9.4285
c 0
b 0
f 0
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/y';
29
    const FORMAT_DATE_DMYMINUS = 'd-m-y';
30
    const FORMAT_DATE_DMMINUS = 'd-m';
31
    const FORMAT_DATE_MYMINUS = 'm-y';
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/y 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 = '[$EUR ]#,##0.00_-';
51
52
    /**
53
     * Excel built-in number formats.
54
     *
55
     * @var array
56
     */
57
    protected static $builtInFormats;
58
59
    /**
60
     * Excel built-in number formats (flipped, for faster lookups).
61
     *
62
     * @var array
63
     */
64
    protected static $flippedBuiltInFormats;
65
66
    /**
67
     * Format Code.
68
     *
69
     * @var string
70
     */
71
    protected $formatCode = self::FORMAT_GENERAL;
72
73
    /**
74
     * Built-in format Code.
75
     *
76
     * @var string
77
     */
78
    protected $builtInFormatCode = 0;
79
80
    /**
81
     * Create a new NumberFormat.
82
     *
83
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
84
     *                                    Leave this value at default unless you understand exactly what
85
     *                                        its ramifications are
86
     * @param bool $isConditional Flag indicating if this is a conditional style or not
87
     *                                    Leave this value at default unless you understand exactly what
88
     *                                        its ramifications are
89
     */
90 137
    public function __construct($isSupervisor = false, $isConditional = false)
91
    {
92
        // Supervisor?
93 137
        parent::__construct($isSupervisor);
94
95 137
        if ($isConditional) {
96 2
            $this->formatCode = null;
97 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...
98
        }
99 137
    }
100
101
    /**
102
     * Get the shared style component for the currently active cell in currently active sheet.
103
     * Only used for style supervisor.
104
     *
105
     * @return NumberFormat
106
     */
107 5
    public function getSharedComponent()
108
    {
109 5
        return $this->parent->getSharedComponent()->getNumberFormat();
110
    }
111
112
    /**
113
     * Build style array from subcomponents.
114
     *
115
     * @param array $array
116
     *
117
     * @return array
118
     */
119 32
    public function getStyleArray($array)
120
    {
121 32
        return ['numberFormat' => $array];
122
    }
123
124
    /**
125
     * Apply styles from array.
126
     * <code>
127
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray(
128
     *        array(
129
     *            'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE
130
     *        )
131
     * );
132
     * </code>.
133
     *
134
     * @param array $pStyles Array containing style information
135
     *
136
     * @throws PhpSpreadsheetException
137
     *
138
     * @return NumberFormat
139
     */
140 36
    public function applyFromArray(array $pStyles)
141
    {
142 36
        if ($this->isSupervisor) {
143
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
144
        } else {
145 36
            if (isset($pStyles['formatCode'])) {
146 36
                $this->setFormatCode($pStyles['formatCode']);
147
            }
148
        }
149
150 36
        return $this;
151
    }
152
153
    /**
154
     * Get Format Code.
155
     *
156
     * @return string
157
     */
158 53
    public function getFormatCode()
159
    {
160 53
        if ($this->isSupervisor) {
161 5
            return $this->getSharedComponent()->getFormatCode();
162
        }
163 53
        if ($this->builtInFormatCode !== false) {
164 44
            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

164
            return self::builtInFormatCode(/** @scrutinizer ignore-type */ $this->builtInFormatCode);
Loading history...
165
        }
166
167 33
        return $this->formatCode;
168
    }
169
170
    /**
171
     * Set Format Code.
172
     *
173
     * @param string $pValue see self::FORMAT_*
174
     *
175
     * @return NumberFormat
176
     */
177 63 View Code Duplication
    public function setFormatCode($pValue)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
178
    {
179 63
        if ($pValue == '') {
180
            $pValue = self::FORMAT_GENERAL;
181
        }
182 63
        if ($this->isSupervisor) {
183 32
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
184 32
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
185
        } else {
186 63
            $this->formatCode = $pValue;
187 63
            $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...
188
        }
189
190 63
        return $this;
191
    }
192
193
    /**
194
     * Get Built-In Format Code.
195
     *
196
     * @return int
197
     */
198 58
    public function getBuiltInFormatCode()
199
    {
200 58
        if ($this->isSupervisor) {
201
            return $this->getSharedComponent()->getBuiltInFormatCode();
202
        }
203
204 58
        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...
205
    }
206
207
    /**
208
     * Set Built-In Format Code.
209
     *
210
     * @param int $pValue
211
     *
212
     * @return NumberFormat
213
     */
214
    public function setBuiltInFormatCode($pValue)
215
    {
216
        if ($this->isSupervisor) {
217
            $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($pValue)]);
218
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
219
        } else {
220
            $this->builtInFormatCode = $pValue;
221
            $this->formatCode = self::builtInFormatCode($pValue);
222
        }
223
224
        return $this;
225
    }
226
227
    /**
228
     * Fill built-in format codes.
229
     */
230 78
    private static function fillBuiltInFormatCodes()
231
    {
232
        //  [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance]
233
        //  18.8.30. numFmt (Number Format)
234
        //
235
        //  The ECMA standard defines built-in format IDs
236
        //      14: "mm-dd-yy"
237
        //      22: "m/d/yy h:mm"
238
        //      37: "#,##0 ;(#,##0)"
239
        //      38: "#,##0 ;[Red](#,##0)"
240
        //      39: "#,##0.00;(#,##0.00)"
241
        //      40: "#,##0.00;[Red](#,##0.00)"
242
        //      47: "mmss.0"
243
        //      KOR fmt 55: "yyyy-mm-dd"
244
        //  Excel defines built-in format IDs
245
        //      14: "m/d/yyyy"
246
        //      22: "m/d/yyyy h:mm"
247
        //      37: "#,##0_);(#,##0)"
248
        //      38: "#,##0_);[Red](#,##0)"
249
        //      39: "#,##0.00_);(#,##0.00)"
250
        //      40: "#,##0.00_);[Red](#,##0.00)"
251
        //      47: "mm:ss.0"
252
        //      KOR fmt 55: "yyyy/mm/dd"
253
254
        // Built-in format codes
255 78
        if (self::$builtInFormats === null) {
256 69
            self::$builtInFormats = [];
257
258
            // General
259 69
            self::$builtInFormats[0] = self::FORMAT_GENERAL;
260 69
            self::$builtInFormats[1] = '0';
261 69
            self::$builtInFormats[2] = '0.00';
262 69
            self::$builtInFormats[3] = '#,##0';
263 69
            self::$builtInFormats[4] = '#,##0.00';
264
265 69
            self::$builtInFormats[9] = '0%';
266 69
            self::$builtInFormats[10] = '0.00%';
267 69
            self::$builtInFormats[11] = '0.00E+00';
268 69
            self::$builtInFormats[12] = '# ?/?';
269 69
            self::$builtInFormats[13] = '# ??/??';
270 69
            self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
271 69
            self::$builtInFormats[15] = 'd-mmm-yy';
272 69
            self::$builtInFormats[16] = 'd-mmm';
273 69
            self::$builtInFormats[17] = 'mmm-yy';
274 69
            self::$builtInFormats[18] = 'h:mm AM/PM';
275 69
            self::$builtInFormats[19] = 'h:mm:ss AM/PM';
276 69
            self::$builtInFormats[20] = 'h:mm';
277 69
            self::$builtInFormats[21] = 'h:mm:ss';
278 69
            self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
279
280 69
            self::$builtInFormats[37] = '#,##0_);(#,##0)'; //  Despite ECMA '#,##0 ;(#,##0)';
281 69
            self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; //  Despite ECMA '#,##0 ;[Red](#,##0)';
282 69
            self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; //  Despite ECMA '#,##0.00;(#,##0.00)';
283 69
            self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; //  Despite ECMA '#,##0.00;[Red](#,##0.00)';
284
285 69
            self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
286 69
            self::$builtInFormats[45] = 'mm:ss';
287 69
            self::$builtInFormats[46] = '[h]:mm:ss';
288 69
            self::$builtInFormats[47] = 'mm:ss.0'; //  Despite ECMA 'mmss.0';
289 69
            self::$builtInFormats[48] = '##0.0E+0';
290 69
            self::$builtInFormats[49] = '@';
291
292
            // CHT
293 69
            self::$builtInFormats[27] = '[$-404]e/m/d';
294 69
            self::$builtInFormats[30] = 'm/d/yy';
295 69
            self::$builtInFormats[36] = '[$-404]e/m/d';
296 69
            self::$builtInFormats[50] = '[$-404]e/m/d';
297 69
            self::$builtInFormats[57] = '[$-404]e/m/d';
298
299
            // THA
300 69
            self::$builtInFormats[59] = 't0';
301 69
            self::$builtInFormats[60] = 't0.00';
302 69
            self::$builtInFormats[61] = 't#,##0';
303 69
            self::$builtInFormats[62] = 't#,##0.00';
304 69
            self::$builtInFormats[67] = 't0%';
305 69
            self::$builtInFormats[68] = 't0.00%';
306 69
            self::$builtInFormats[69] = 't# ?/?';
307 69
            self::$builtInFormats[70] = 't# ??/??';
308
309
            // Flip array (for faster lookups)
310 69
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
311
        }
312 78
    }
313
314
    /**
315
     * Get built-in format code.
316
     *
317
     * @param int $pIndex
318
     *
319
     * @return string
320
     */
321 61
    public static function builtInFormatCode($pIndex)
322
    {
323
        // Clean parameter
324 61
        $pIndex = (int) $pIndex;
325
326
        // Ensure built-in format codes are available
327 61
        self::fillBuiltInFormatCodes();
328
329
        // Lookup format code
330 61
        if (isset(self::$builtInFormats[$pIndex])) {
331 61
            return self::$builtInFormats[$pIndex];
332
        }
333
334 2
        return '';
335
    }
336
337
    /**
338
     * Get built-in format code index.
339
     *
340
     * @param string $formatCode
341
     *
342
     * @return bool|int
343
     */
344 63
    public static function builtInFormatCodeIndex($formatCode)
345
    {
346
        // Ensure built-in format codes are available
347 63
        self::fillBuiltInFormatCodes();
348
349
        // Lookup format code
350 63
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
351 54
            return self::$flippedBuiltInFormats[$formatCode];
352
        }
353
354 49
        return false;
355
    }
356
357
    /**
358
     * Get hash code.
359
     *
360
     * @return string Hash code
361
     */
362 76
    public function getHashCode()
363
    {
364 76
        if ($this->isSupervisor) {
365
            return $this->getSharedComponent()->getHashCode();
366
        }
367
368 76
        return md5(
369 76
            $this->formatCode .
370 76
            $this->builtInFormatCode .
371 76
            __CLASS__
372
        );
373
    }
374
375
    /**
376
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
377
     *
378
     * @var array
379
     */
380
    private static $dateFormatReplacements = [
381
            // first remove escapes related to non-format characters
382
            '\\' => '',
383
            //    12-hour suffix
384
            'am/pm' => 'A',
385
            //    4-digit year
386
            'e' => 'Y',
387
            'yyyy' => 'Y',
388
            //    2-digit year
389
            'yy' => 'y',
390
            //    first letter of month - no php equivalent
391
            'mmmmm' => 'M',
392
            //    full month name
393
            'mmmm' => 'F',
394
            //    short month name
395
            'mmm' => 'M',
396
            //    mm is minutes if time, but can also be month w/leading zero
397
            //    so we try to identify times be the inclusion of a : separator in the mask
398
            //    It isn't perfect, but the best way I know how
399
            ':mm' => ':i',
400
            'mm:' => 'i:',
401
            //    month leading zero
402
            'mm' => 'm',
403
            //    month no leading zero
404
            'm' => 'n',
405
            //    full day of week name
406
            'dddd' => 'l',
407
            //    short day of week name
408
            'ddd' => 'D',
409
            //    days leading zero
410
            'dd' => 'd',
411
            //    days no leading zero
412
            'd' => 'j',
413
            //    seconds
414
            'ss' => 's',
415
            //    fractional seconds - no php equivalent
416
            '.s' => '',
417
        ];
418
    /**
419
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
420
     *
421
     * @var array
422
     */
423
    private static $dateFormatReplacements24 = [
424
            'hh' => 'H',
425
            'h' => 'G',
426
        ];
427
    /**
428
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
429
     *
430
     * @var array
431
     */
432
    private static $dateFormatReplacements12 = [
433
            'hh' => 'h',
434
            'h' => 'g',
435
        ];
436
437 27
    private static function setLowercaseCallback($matches)
438
    {
439 27
        return mb_strtolower($matches[0]);
440
    }
441
442 11
    private static function escapeQuotesCallback($matches)
443
    {
444 11
        return '\\' . implode('\\', str_split($matches[1]));
445
    }
446
447 27
    private static function formatAsDate(&$value, &$format)
448
    {
449
        // strip off first part containing e.g. [$-F800] or [$USD-409]
450
        // general syntax: [$<Currency string>-<language info>]
451
        // language info is in hexadecimal
452
        // strip off chinese part like [DBNum1][$-804]
453 27
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
454
455
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
456
        //    but we don't want to change any quoted strings
457 27
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
458
459
        // Only process the non-quoted blocks for date format characters
460 27
        $blocks = explode('"', $format);
461 27
        foreach ($blocks as $key => &$block) {
462 27
            if ($key % 2 == 0) {
463 27
                $block = strtr($block, self::$dateFormatReplacements);
0 ignored issues
show
Bug introduced by
The call to strtr() has too few arguments starting with to. ( Ignorable by Annotation )

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

463
                $block = /** @scrutinizer ignore-call */ strtr($block, self::$dateFormatReplacements);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Bug introduced by
self::dateFormatReplacements of type array is incompatible with the type string expected by parameter $from of strtr(). ( Ignorable by Annotation )

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

463
                $block = strtr($block, /** @scrutinizer ignore-type */ self::$dateFormatReplacements);
Loading history...
464 27
                if (!strpos($block, 'A')) {
465
                    // 24-hour time format
466 27
                    $block = strtr($block, self::$dateFormatReplacements24);
467
                } else {
468
                    // 12-hour time format
469 27
                    $block = strtr($block, self::$dateFormatReplacements12);
470
                }
471
            }
472
        }
473 27
        $format = implode('"', $blocks);
474
475
        // escape any quoted characters so that DateTime format() will render them correctly
476 27
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
477
478 27
        $dateObj = Date::excelToDateTimeObject($value);
479 27
        $value = $dateObj->format($format);
480 27
    }
481
482 4
    private static function formatAsPercentage(&$value, &$format)
483
    {
484 4
        if ($format === self::FORMAT_PERCENTAGE) {
485 3
            $value = round((100 * $value), 0) . '%';
486
        } else {
487 1
            if (preg_match('/\.[#0]+/', $format, $m)) {
488 1
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
1 ignored issue
show
Bug introduced by
Are you sure substr($m[0], 0, 1) of type false|string can be used in concatenation? ( Ignorable by Annotation )

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

488
                $s = /** @scrutinizer ignore-type */ substr($m[0], 0, 1) . (strlen($m[0]) - 1);
Loading history...
489 1
                $format = str_replace($m[0], $s, $format);
490
            }
491 1
            if (preg_match('/^[#0]+/', $format, $m)) {
492 1
                $format = str_replace($m[0], strlen($m[0]), $format);
493
            }
494 1
            $format = '%' . str_replace('%', 'f%%', $format);
495
496 1
            $value = sprintf($format, 100 * $value);
497
        }
498 4
    }
499
500 4
    private static function formatAsFraction(&$value, &$format)
501
    {
502 4
        $sign = ($value < 0) ? '-' : '';
503
504 4
        $integerPart = floor(abs($value));
505 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
506 4
        $decimalLength = strlen($decimalPart);
507 4
        $decimalDivisor = pow(10, $decimalLength);
508
509 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
510
511 4
        $adjustedDecimalPart = $decimalPart / $GCD;
512 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
513
514 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
515 3
            if ($integerPart == 0) {
516
                $integerPart = '';
517
            }
518 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
519
        } else {
520 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
521 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
522
        }
523 4
    }
524
525 6
    private static function complexNumberFormatMask($number, $mask)
526
    {
527 6
        $sign = ($number < 0.0);
528 6
        $number = abs($number);
529 6
        if (strpos($mask, '.') !== false) {
530 2
            $numbers = explode('.', $number . '.0');
531 2
            $masks = explode('.', $mask . '.0');
532 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
533 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
534
535 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
536
        }
537
538 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
539 6
        if ($r > 1) {
540 6
            $result = array_reverse($result[0]);
541
542 6
            foreach ($result as $block) {
543 6
                $divisor = 1 . $block[0];
544 6
                $size = strlen($block[0]);
545 6
                $offset = $block[1];
546
547 6
                $blockValue = sprintf(
548 6
                    '%0' . $size . 'd',
549 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

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

600
        switch (count(/** @scrutinizer ignore-type */ $sections)) {
Loading history...
601 78
            case 1:
602 72
                $format = $sections[0];
603
604 72
                break;
605 10
            case 2:
606 10
                $format = ($value >= 0) ? $sections[0] : $sections[1];
607 10
                $value = abs($value); // Use the absolute value
608 10
                break;
609 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...
610
                $format = ($value > 0) ?
611
                    $sections[0] : (($value < 0) ?
612
                        $sections[1] : $sections[2]);
613
                $value = abs($value); // Use the absolute value
614
                break;
615 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...
616
                $format = ($value > 0) ?
617
                    $sections[0] : (($value < 0) ?
618
                        $sections[1] : $sections[2]);
619
                $value = abs($value); // Use the absolute value
620
                break;
621
            default:
622
                // something is wrong, just use first section
623
                $format = $sections[0];
624
625
                break;
626
        }
627
628
        // In Excel formats, "_" is used to add spacing,
629
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
630 78
        $format = preg_replace('/_./', ' ', $format);
631
632
        // Save format with color information for later use below
633 78
        $formatColor = $format;
634
635
        // Strip color information
636 78
        $color_regex = '/^\\[[a-zA-Z]+\\]/';
637 78
        $format = preg_replace($color_regex, '', $format);
638
639
        // Let's begin inspecting the format and converting the value to a formatted string
640
641
        //  Check for date/time characters (not inside quotes)
642 78
        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
643
            // datetime format
644 27
            self::formatAsDate($value, $format);
645 61
        } elseif (preg_match('/%$/', $format)) {
646
            // % number format
647 4
            self::formatAsPercentage($value, $format);
648
        } else {
649 57
            if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
650
                $value = 'EUR ' . sprintf('%1.2f', $value);
651
            } else {
652
                // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
653 57
                $format = str_replace(['"', '*'], '', $format);
654
655
                // Find out if we need thousands separator
656
                // This is indicated by a comma enclosed by a digit placeholder:
657
                //        #,#   or   0,0
658 57
                $useThousands = preg_match('/(#,#|0,0)/', $format);
659 57
                if ($useThousands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $useThousands of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
660 25
                    $format = preg_replace('/0,0/', '00', $format);
661 25
                    $format = preg_replace('/#,#/', '##', $format);
662
                }
663
664
                // Scale thousands, millions,...
665
                // This is indicated by a number of commas after a digit placeholder:
666
                //        #,   or    0.0,,
667 57
                $scale = 1; // same as no scale
668 57
                $matches = [];
669 57
                if (preg_match('/(#|0)(,+)/', $format, $matches)) {
670 2
                    $scale = pow(1000, strlen($matches[2]));
671
672
                    // strip the commas
673 2
                    $format = preg_replace('/0,+/', '0', $format);
674 2
                    $format = preg_replace('/#,+/', '#', $format);
675
                }
676
677 57
                if (preg_match('/#?.*\?\/\?/', $format, $m)) {
678 4
                    if ($value != (int) $value) {
679 4
                        self::formatAsFraction($value, $format);
680
                    }
681
                } else {
682
                    // Handle the number itself
683
684
                    // scale number
685 53
                    $value = $value / $scale;
686
687
                    // Strip #
688 53
                    $format = preg_replace('/\\#/', '0', $format);
689
690 53
                    $n = "/\[[^\]]+\]/";
691 53
                    $m = preg_replace($n, '', $format);
692 53
                    $number_regex = "/(0+)(\.?)(0*)/";
693 53
                    if (preg_match($number_regex, $m, $matches)) {
694 53
                        $left = $matches[1];
695 53
                        $dec = $matches[2];
696 53
                        $right = $matches[3];
697
698
                        // minimun width of formatted number (including dot)
699 53
                        $minWidth = strlen($left) + strlen($dec) + strlen($right);
700 53
                        if ($useThousands) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $useThousands of type integer|false is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
701 25
                            $value = number_format(
702 25
                                $value,
703 25
                                strlen($right),
704 25
                                StringHelper::getDecimalSeparator(),
705 25
                                StringHelper::getThousandsSeparator()
706
                            );
707 25
                            $value = preg_replace($number_regex, $value, $format);
708
                        } else {
709 32
                            if (preg_match('/[0#]E[+-]0/i', $format)) {
710
                                //    Scientific format
711 7
                                $value = sprintf('%5.2E', $value);
712 29
                            } elseif (preg_match('/0([^\d\.]+)0/', $format)) {
713 6
                                $value = self::complexNumberFormatMask($value, $format);
714
                            } else {
715 23
                                $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
716 23
                                $value = sprintf($sprintf_pattern, $value);
717 23
                                $value = preg_replace($number_regex, $value, $format);
718
                            }
719
                        }
720
                    }
721
                }
722 57
                if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
723
                    //  Currency or Accounting
724 5
                    $currencyCode = $m[1];
725 5
                    list($currencyCode) = explode('-', $currencyCode);
726 5
                    if ($currencyCode == '') {
727
                        $currencyCode = StringHelper::getCurrencyCode();
728
                    }
729 5
                    $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
730
                }
731
            }
732
        }
733
734
        // Additional formatting provided by callback function
735 78
        if ($callBack !== null) {
736 4
            list($writerInstance, $function) = $callBack;
737 4
            $value = $writerInstance->$function($value, $formatColor);
738
        }
739
740 78
        return $value;
741
    }
742
}
743