Completed
Push — master ( 125f46...57404f )
by Adrien
38:00 queued 31:11
created

NumberFormat::getFormatCode()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 3
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 190
    public function __construct($isSupervisor = false, $isConditional = false)
92
    {
93
        // Supervisor?
94 190
        parent::__construct($isSupervisor);
95
96 190
        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 190
    }
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 37
    public function getStyleArray($array)
121
    {
122 37
        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 41
    public function applyFromArray(array $pStyles)
143
    {
144 41
        if ($this->isSupervisor) {
145
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
146
        } else {
147 41
            if (isset($pStyles['formatCode'])) {
148 41
                $this->setFormatCode($pStyles['formatCode']);
149
            }
150
        }
151
152 41
        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 is always true. If $this->builtInFormatCode can have other possible types, add them to src/PhpSpreadsheet/Style/NumberFormat.php:77
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 34
        return $this->formatCode;
170
    }
171
172
    /**
173
     * Set Format Code.
174
     *
175
     * @param string $pValue see self::FORMAT_*
176
     *
177
     * @return NumberFormat
178
     */
179 98
    public function setFormatCode($pValue)
180
    {
181 98
        if ($pValue == '') {
182 1
            $pValue = self::FORMAT_GENERAL;
183
        }
184 98
        if ($this->isSupervisor) {
185 37
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
186 37
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
187
        } else {
188 98
            $this->formatCode = $pValue;
189 98
            $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 98
        return $this;
193
    }
194
195
    /**
196
     * Get Built-In Format Code.
197
     *
198
     * @return int
199
     */
200 88
    public function getBuiltInFormatCode()
201
    {
202 88
        if ($this->isSupervisor) {
203
            return $this->getSharedComponent()->getBuiltInFormatCode();
204
        }
205
206 88
        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 122
    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 122
        if (self::$builtInFormats === null) {
0 ignored issues
show
introduced by
The condition self::builtInFormats is always false. If self::builtInFormats can have other possible types, add them to src/PhpSpreadsheet/Style/NumberFormat.php:56
Loading history...
258 70
            self::$builtInFormats = [];
259
260
            // General
261 70
            self::$builtInFormats[0] = self::FORMAT_GENERAL;
262 70
            self::$builtInFormats[1] = '0';
263 70
            self::$builtInFormats[2] = '0.00';
264 70
            self::$builtInFormats[3] = '#,##0';
265 70
            self::$builtInFormats[4] = '#,##0.00';
266
267 70
            self::$builtInFormats[9] = '0%';
268 70
            self::$builtInFormats[10] = '0.00%';
269 70
            self::$builtInFormats[11] = '0.00E+00';
270 70
            self::$builtInFormats[12] = '# ?/?';
271 70
            self::$builtInFormats[13] = '# ??/??';
272 70
            self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
273 70
            self::$builtInFormats[15] = 'd-mmm-yy';
274 70
            self::$builtInFormats[16] = 'd-mmm';
275 70
            self::$builtInFormats[17] = 'mmm-yy';
276 70
            self::$builtInFormats[18] = 'h:mm AM/PM';
277 70
            self::$builtInFormats[19] = 'h:mm:ss AM/PM';
278 70
            self::$builtInFormats[20] = 'h:mm';
279 70
            self::$builtInFormats[21] = 'h:mm:ss';
280 70
            self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
281
282 70
            self::$builtInFormats[37] = '#,##0_);(#,##0)'; //  Despite ECMA '#,##0 ;(#,##0)';
283 70
            self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; //  Despite ECMA '#,##0 ;[Red](#,##0)';
284 70
            self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; //  Despite ECMA '#,##0.00;(#,##0.00)';
285 70
            self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; //  Despite ECMA '#,##0.00;[Red](#,##0.00)';
286
287 70
            self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
288 70
            self::$builtInFormats[45] = 'mm:ss';
289 70
            self::$builtInFormats[46] = '[h]:mm:ss';
290 70
            self::$builtInFormats[47] = 'mm:ss.0'; //  Despite ECMA 'mmss.0';
291 70
            self::$builtInFormats[48] = '##0.0E+0';
292 70
            self::$builtInFormats[49] = '@';
293
294
            // CHT
295 70
            self::$builtInFormats[27] = '[$-404]e/m/d';
296 70
            self::$builtInFormats[30] = 'm/d/yy';
297 70
            self::$builtInFormats[36] = '[$-404]e/m/d';
298 70
            self::$builtInFormats[50] = '[$-404]e/m/d';
299 70
            self::$builtInFormats[57] = '[$-404]e/m/d';
300
301
            // THA
302 70
            self::$builtInFormats[59] = 't0';
303 70
            self::$builtInFormats[60] = 't0.00';
304 70
            self::$builtInFormats[61] = 't#,##0';
305 70
            self::$builtInFormats[62] = 't#,##0.00';
306 70
            self::$builtInFormats[67] = 't0%';
307 70
            self::$builtInFormats[68] = 't0.00%';
308 70
            self::$builtInFormats[69] = 't# ?/?';
309 70
            self::$builtInFormats[70] = 't# ??/??';
310
311
            // Flip array (for faster lookups)
312 70
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
313
        }
314 122
    }
315
316
    /**
317
     * Get built-in format code.
318
     *
319
     * @param int $pIndex
320
     *
321
     * @return string
322
     */
323 100
    public static function builtInFormatCode($pIndex)
324
    {
325
        // Clean parameter
326 100
        $pIndex = (int) $pIndex;
327
328
        // Ensure built-in format codes are available
329 100
        self::fillBuiltInFormatCodes();
330
331
        // Lookup format code
332 100
        if (isset(self::$builtInFormats[$pIndex])) {
333 100
            return self::$builtInFormats[$pIndex];
334
        }
335
336 2
        return '';
337
    }
338
339
    /**
340
     * Get built-in format code index.
341
     *
342
     * @param string $formatCode
343
     *
344
     * @return bool|int
345
     */
346 98
    public static function builtInFormatCodeIndex($formatCode)
347
    {
348
        // Ensure built-in format codes are available
349 98
        self::fillBuiltInFormatCodes();
350
351
        // Lookup format code
352 98
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
353 89
            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 109
    public function getHashCode()
365
    {
366 109
        if ($this->isSupervisor) {
367
            return $this->getSharedComponent()->getHashCode();
368
        }
369
370 109
        return md5(
371 109
            $this->formatCode .
372 109
            $this->builtInFormatCode .
373 109
            __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 29
                    $block = strtr($block, self::$dateFormatReplacements24);
471
                } else {
472
                    // 12-hour time format
473 31
                    $block = strtr($block, self::$dateFormatReplacements12);
474
                }
475
            }
476
        }
477 31
        $format = implode('"', $blocks);
478
479
        // escape any quoted characters so that DateTime format() will render them correctly
480 31
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
481
482 31
        $dateObj = Date::excelToDateTimeObject($value);
483 31
        $value = $dateObj->format($format);
484 31
    }
485
486 4
    private static function formatAsPercentage(&$value, &$format)
487
    {
488 4
        if ($format === self::FORMAT_PERCENTAGE) {
489 3
            $value = round((100 * $value), 0) . '%';
490
        } else {
491 1
            if (preg_match('/\.[#0]+/', $format, $m)) {
492 1
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
493 1
                $format = str_replace($m[0], $s, $format);
494
            }
495 1
            if (preg_match('/^[#0]+/', $format, $m)) {
496 1
                $format = str_replace($m[0], strlen($m[0]), $format);
497
            }
498 1
            $format = '%' . str_replace('%', 'f%%', $format);
499
500 1
            $value = sprintf($format, 100 * $value);
501
        }
502 4
    }
503
504 4
    private static function formatAsFraction(&$value, &$format)
505
    {
506 4
        $sign = ($value < 0) ? '-' : '';
507
508 4
        $integerPart = floor(abs($value));
509 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
510 4
        $decimalLength = strlen($decimalPart);
511 4
        $decimalDivisor = pow(10, $decimalLength);
512
513 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
514
515 4
        $adjustedDecimalPart = $decimalPart / $GCD;
516 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
517
518 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
519 3
            if ($integerPart == 0) {
520
                $integerPart = '';
521
            }
522 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
523
        } else {
524 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
525 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
526
        }
527 4
    }
528
529 6
    private static function complexNumberFormatMask($number, $mask)
530
    {
531 6
        $sign = ($number < 0.0);
532 6
        $number = abs($number);
533 6
        if (strpos($mask, '.') !== false) {
534 2
            $numbers = explode('.', $number . '.0');
535 2
            $masks = explode('.', $mask . '.0');
536 2
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
537 2
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
538
539 2
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
540
        }
541
542 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
543 6
        if ($r > 1) {
544 6
            $result = array_reverse($result[0]);
545
546 6
            foreach ($result as $block) {
547 6
                $divisor = 1 . $block[0];
548 6
                $size = strlen($block[0]);
549 6
                $offset = $block[1];
550
551 6
                $blockValue = sprintf(
552 6
                    '%0' . $size . 'd',
553 6
                    fmod($number, $divisor)
0 ignored issues
show
Bug introduced by
$divisor of type string is incompatible with the type double expected by parameter $y of fmod(). ( Ignorable by Annotation )

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

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

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

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