Failed Conditions
Push — master ( 27d83b...a2771e )
by Adrien
35:04
created

NumberFormat::setFormatCode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 4
nop 1
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 3
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/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 = '[$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 158
    public function __construct($isSupervisor = false, $isConditional = false)
91
    {
92
        // Supervisor?
93 158
        parent::__construct($isSupervisor);
94
95 158
        if ($isConditional) {
96 3
            $this->formatCode = null;
97 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...
98
        }
99 158
    }
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();
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

109
        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...
110
    }
111
112
    /**
113
     * Build style array from subcomponents.
114
     *
115
     * @param array $array
116
     *
117
     * @return array
118
     */
119 34
    public function getStyleArray($array)
120
    {
121 34
        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 38
    public function applyFromArray(array $pStyles)
141
    {
142 38
        if ($this->isSupervisor) {
143
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
144
        } else {
145 38
            if (isset($pStyles['formatCode'])) {
146 38
                $this->setFormatCode($pStyles['formatCode']);
147
            }
148
        }
149
150 38
        return $this;
151
    }
152
153
    /**
154
     * Get Format Code.
155
     *
156
     * @return string
157
     */
158 61
    public function getFormatCode()
159
    {
160 61
        if ($this->isSupervisor) {
161 5
            return $this->getSharedComponent()->getFormatCode();
162
        }
163 61
        if ($this->builtInFormatCode !== false) {
0 ignored issues
show
introduced by
The condition $this->builtInFormatCode !== false can never be false.
Loading history...
164 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

164
            return self::builtInFormatCode(/** @scrutinizer ignore-type */ $this->builtInFormatCode);
Loading history...
165
        }
166
167 34
        return $this->formatCode;
168
    }
169
170
    /**
171
     * Set Format Code.
172
     *
173
     * @param string $pValue see self::FORMAT_*
174
     *
175
     * @return NumberFormat
176
     */
177 72
    public function setFormatCode($pValue)
178
    {
179 72
        if ($pValue == '') {
180 1
            $pValue = self::FORMAT_GENERAL;
181
        }
182 72
        if ($this->isSupervisor) {
183 34
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
184 34
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
185
        } else {
186 72
            $this->formatCode = $pValue;
187 72
            $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 72
        return $this;
191
    }
192
193
    /**
194
     * Get Built-In Format Code.
195
     *
196
     * @return int
197
     */
198 65
    public function getBuiltInFormatCode()
199
    {
200 65
        if ($this->isSupervisor) {
201
            return $this->getSharedComponent()->getBuiltInFormatCode();
202
        }
203
204 65
        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 94
    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 94
        if (self::$builtInFormats === null) {
0 ignored issues
show
introduced by
The condition self::builtInFormats === null can never be true.
Loading history...
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 94
    }
313
314
    /**
315
     * Get built-in format code.
316
     *
317
     * @param int $pIndex
318
     *
319
     * @return string
320
     */
321 75
    public static function builtInFormatCode($pIndex)
322
    {
323
        // Clean parameter
324 75
        $pIndex = (int) $pIndex;
325
326
        // Ensure built-in format codes are available
327 75
        self::fillBuiltInFormatCodes();
328
329
        // Lookup format code
330 75
        if (isset(self::$builtInFormats[$pIndex])) {
331 75
            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 72
    public static function builtInFormatCodeIndex($formatCode)
345
    {
346
        // Ensure built-in format codes are available
347 72
        self::fillBuiltInFormatCodes();
348
349
        // Lookup format code
350 72
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
351 63
            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 82
    public function getHashCode()
363
    {
364 82
        if ($this->isSupervisor) {
365
            return $this->getSharedComponent()->getHashCode();
366
        }
367
368 82
        return md5(
369 82
            $this->formatCode .
370 82
            $this->builtInFormatCode .
371 82
            __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
    /**
420
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
421
     *
422
     * @var array
423
     */
424
    private static $dateFormatReplacements24 = [
425
            'hh' => 'H',
426
            'h' => 'G',
427
        ];
428
429
    /**
430
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
431
     *
432
     * @var array
433
     */
434
    private static $dateFormatReplacements12 = [
435
            'hh' => 'h',
436
            'h' => 'g',
437
        ];
438
439 27
    private static function setLowercaseCallback($matches)
440
    {
441 27
        return mb_strtolower($matches[0]);
442
    }
443
444 11
    private static function escapeQuotesCallback($matches)
445
    {
446 11
        return '\\' . implode('\\', str_split($matches[1]));
447
    }
448
449 27
    private static function formatAsDate(&$value, &$format)
450
    {
451
        // strip off first part containing e.g. [$-F800] or [$USD-409]
452
        // general syntax: [$<Currency string>-<language info>]
453
        // language info is in hexadecimal
454
        // strip off chinese part like [DBNum1][$-804]
455 27
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
456
457
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
458
        //    but we don't want to change any quoted strings
459 27
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
460
461
        // Only process the non-quoted blocks for date format characters
462 27
        $blocks = explode('"', $format);
463 27
        foreach ($blocks as $key => &$block) {
464 27
            if ($key % 2 == 0) {
465 27
                $block = strtr($block, self::$dateFormatReplacements);
466 27
                if (!strpos($block, 'A')) {
467
                    // 24-hour time format
468 27
                    $block = strtr($block, self::$dateFormatReplacements24);
469
                } else {
470
                    // 12-hour time format
471 27
                    $block = strtr($block, self::$dateFormatReplacements12);
472
                }
473
            }
474
        }
475 27
        $format = implode('"', $blocks);
476
477
        // escape any quoted characters so that DateTime format() will render them correctly
478 27
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
479
480 27
        $dateObj = Date::excelToDateTimeObject($value);
481 27
        $value = $dateObj->format($format);
482 27
    }
483
484 4
    private static function formatAsPercentage(&$value, &$format)
485
    {
486 4
        if ($format === self::FORMAT_PERCENTAGE) {
487 3
            $value = round((100 * $value), 0) . '%';
488
        } else {
489 1
            if (preg_match('/\.[#0]+/', $format, $m)) {
490 1
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
491 1
                $format = str_replace($m[0], $s, $format);
492
            }
493 1
            if (preg_match('/^[#0]+/', $format, $m)) {
494 1
                $format = str_replace($m[0], strlen($m[0]), $format);
495
            }
496 1
            $format = '%' . str_replace('%', 'f%%', $format);
497
498 1
            $value = sprintf($format, 100 * $value);
499
        }
500 4
    }
501
502 4
    private static function formatAsFraction(&$value, &$format)
503
    {
504 4
        $sign = ($value < 0) ? '-' : '';
505
506 4
        $integerPart = floor(abs($value));
507 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
508 4
        $decimalLength = strlen($decimalPart);
509 4
        $decimalDivisor = pow(10, $decimalLength);
510
511 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
512
513 4
        $adjustedDecimalPart = $decimalPart / $GCD;
514 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
515
516 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
517 3
            if ($integerPart == 0) {
518
                $integerPart = '';
519
            }
520 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
521
        } else {
522 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
523 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
524
        }
525 4
    }
526
527 6
    private static function complexNumberFormatMask($number, $mask)
528
    {
529 6
        $sign = ($number < 0.0);
530 6
        $number = abs($number);
531 6
        if (strpos($mask, '.') !== false) {
532 2
            $numbers = explode('.', $number . '.0');
533 2
            $masks = explode('.', $mask . '.0');
534 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
535 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
536
537 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
538
        }
539
540 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
541 6
        if ($r > 1) {
542 6
            $result = array_reverse($result[0]);
543
544 6
            foreach ($result as $block) {
545 6
                $divisor = 1 . $block[0];
546 6
                $size = strlen($block[0]);
547 6
                $offset = $block[1];
548
549 6
                $blockValue = sprintf(
550 6
                    '%0' . $size . 'd',
551 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

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

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