Passed
Push — develop ( 39b573...699da0 )
by Adrien
27:28
created

NumberFormat::escapeQuotesCallback()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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 = '#,##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 197
    public function __construct($isSupervisor = false, $isConditional = false)
92
    {
93
        // Supervisor?
94 197
        parent::__construct($isSupervisor);
95
96 197
        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 197
    }
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 36
    public function getStyleArray($array)
121
    {
122 36
        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 40
    public function applyFromArray(array $pStyles)
143
    {
144 40
        if ($this->isSupervisor) {
145
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
146
        } else {
147 40
            if (isset($pStyles['formatCode'])) {
148 40
                $this->setFormatCode($pStyles['formatCode']);
149
            }
150
        }
151
152 40
        return $this;
153
    }
154
155
    /**
156
     * Get Format Code.
157
     *
158
     * @return string
159
     */
160 63
    public function getFormatCode()
161
    {
162 63
        if ($this->isSupervisor) {
163 5
            return $this->getSharedComponent()->getFormatCode();
164
        }
165 63
        if ($this->builtInFormatCode !== false) {
0 ignored issues
show
introduced by
The condition $this->builtInFormatCode !== false is always true.
Loading history...
166 53
            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 33
        return $this->formatCode;
170
    }
171
172
    /**
173
     * Set Format Code.
174
     *
175
     * @param string $pValue see self::FORMAT_*
176
     *
177
     * @return NumberFormat
178
     */
179 99
    public function setFormatCode($pValue)
180
    {
181 99
        if ($pValue == '') {
182 1
            $pValue = self::FORMAT_GENERAL;
183
        }
184 99
        if ($this->isSupervisor) {
185 36
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
186 36
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
187
        } else {
188 99
            $this->formatCode = $pValue;
189 99
            $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 99
        return $this;
193
    }
194
195
    /**
196
     * Get Built-In Format Code.
197
     *
198
     * @return int
199
     */
200 89
    public function getBuiltInFormatCode()
201
    {
202 89
        if ($this->isSupervisor) {
203
            return $this->getSharedComponent()->getBuiltInFormatCode();
204
        }
205
206 89
        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 123
    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 123
        if (self::$builtInFormats === null) {
0 ignored issues
show
introduced by
The condition self::builtInFormats === null is always false.
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 123
    }
315
316
    /**
317
     * Get built-in format code.
318
     *
319
     * @param int $pIndex
320
     *
321
     * @return string
322
     */
323 101
    public static function builtInFormatCode($pIndex)
324
    {
325
        // Clean parameter
326 101
        $pIndex = (int) $pIndex;
327
328
        // Ensure built-in format codes are available
329 101
        self::fillBuiltInFormatCodes();
330
331
        // Lookup format code
332 101
        if (isset(self::$builtInFormats[$pIndex])) {
333 101
            return self::$builtInFormats[$pIndex];
334
        }
335
336 4
        return '';
337
    }
338
339
    /**
340
     * Get built-in format code index.
341
     *
342
     * @param string $formatCode
343
     *
344
     * @return bool|int
345
     */
346 99
    public static function builtInFormatCodeIndex($formatCode)
347
    {
348
        // Ensure built-in format codes are available
349 99
        self::fillBuiltInFormatCodes();
350
351
        // Lookup format code
352 99
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
353 90
            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 110
    public function getHashCode()
365
    {
366 110
        if ($this->isSupervisor) {
367
            return $this->getSharedComponent()->getHashCode();
368
        }
369
370 110
        return md5(
371 110
            $this->formatCode .
372 110
            $this->builtInFormatCode .
373 110
            __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 31
    private static function setLowercaseCallback($matches)
442
    {
443 31
        return mb_strtolower($matches[0]);
444
    }
445
446 7
    private static function escapeQuotesCallback($matches)
447
    {
448 7
        return '\\' . implode('\\', str_split($matches[1]));
449
    }
450
451 31
    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 31
        $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 31
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
462
463
        // Only process the non-quoted blocks for date format characters
464 31
        $blocks = explode('"', $format);
465 31
        foreach ($blocks as $key => &$block) {
466 31
            if ($key % 2 == 0) {
467 31
                $block = strtr($block, self::$dateFormatReplacements);
468 31
                if (!strpos($block, 'A')) {
469
                    // 24-hour time format
470
                    // when [h]:mm format, the [h] should replace to the hours of the value * 24
471 29
                    if (false !== strpos($block, '[h]')) {
472 1
                        $hours = (int) ($value * 24);
473 1
                        $block = str_replace('[h]', $hours, $block);
474
475 1
                        continue;
476
                    }
477 28
                    $block = strtr($block, self::$dateFormatReplacements24);
478
                } else {
479
                    // 12-hour time format
480 30
                    $block = strtr($block, self::$dateFormatReplacements12);
481
                }
482
            }
483
        }
484 31
        $format = implode('"', $blocks);
485
486
        // escape any quoted characters so that DateTime format() will render them correctly
487 31
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
488
489 31
        $dateObj = Date::excelToDateTimeObject($value);
490 31
        $value = $dateObj->format($format);
491 31
    }
492
493 5
    private static function formatAsPercentage(&$value, &$format)
494
    {
495 5
        if ($format === self::FORMAT_PERCENTAGE) {
496 4
            $value = round((100 * $value), 0) . '%';
497
        } else {
498 1
            if (preg_match('/\.[#0]+/', $format, $m)) {
499 1
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
500 1
                $format = str_replace($m[0], $s, $format);
501
            }
502 1
            if (preg_match('/^[#0]+/', $format, $m)) {
503 1
                $format = str_replace($m[0], strlen($m[0]), $format);
504
            }
505 1
            $format = '%' . str_replace('%', 'f%%', $format);
506
507 1
            $value = sprintf($format, 100 * $value);
508
        }
509 5
    }
510
511 4
    private static function formatAsFraction(&$value, &$format)
512
    {
513 4
        $sign = ($value < 0) ? '-' : '';
514
515 4
        $integerPart = floor(abs($value));
516 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
517 4
        $decimalLength = strlen($decimalPart);
518 4
        $decimalDivisor = pow(10, $decimalLength);
519
520 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
521
522 4
        $adjustedDecimalPart = $decimalPart / $GCD;
523 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
524
525 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
526 3
            if ($integerPart == 0) {
527
                $integerPart = '';
528
            }
529 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
530
        } else {
531 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
532 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
533
        }
534 4
    }
535
536 6
    private static function complexNumberFormatMask($number, $mask)
537
    {
538 6
        $sign = ($number < 0.0);
539 6
        $number = abs($number);
540 6
        if (strpos($mask, '.') !== false) {
541 2
            $numbers = explode('.', $number . '.0');
542 2
            $masks = explode('.', $mask . '.0');
543 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
544 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
545
546 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
547
        }
548
549 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
550 6
        if ($r > 1) {
551 6
            $result = array_reverse($result[0]);
552
553 6
            foreach ($result as $block) {
554 6
                $divisor = 1 . $block[0];
555 6
                $size = strlen($block[0]);
556 6
                $offset = $block[1];
557
558 6
                $blockValue = sprintf(
559 6
                    '%0' . $size . 'd',
560 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

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

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