Completed
Push — master ( 1c99f4...a54ed6 )
by Mark
61:36
created

NumberFormat::fillBuiltInFormatCodes()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 97
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 59
nc 2
nop 0
dl 0
loc 97
ccs 49
cts 49
cp 1
crap 2
rs 8.8945
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 204
    public function __construct($isSupervisor = false, $isConditional = false)
92
    {
93
        // Supervisor?
94 204
        parent::__construct($isSupervisor);
95
96 204
        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 204
    }
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 6
    public function getSharedComponent()
109
    {
110 6
        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 64
    public function getFormatCode()
161
    {
162 64
        if ($this->isSupervisor) {
163 6
            return $this->getSharedComponent()->getFormatCode();
164
        }
165 64
        if ($this->builtInFormatCode !== false) {
0 ignored issues
show
introduced by
The condition $this->builtInFormatCode !== false is always true.
Loading history...
166 54
            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 100
    public function setFormatCode($pValue)
180
    {
181 100
        if ($pValue == '') {
182 1
            $pValue = self::FORMAT_GENERAL;
183
        }
184 100
        if ($this->isSupervisor) {
185 37
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
186 37
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
187
        } else {
188 100
            $this->formatCode = $pValue;
189 100
            $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 100
        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 124
    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 124
        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
            // JPN
312 69
            self::$builtInFormats[28] = '[$-411]ggge"年"m"月"d"日"';
313
            self::$builtInFormats[29] = '[$-411]ggge"年"m"月"d"日"';
314 124
            self::$builtInFormats[31] = 'yyyy"年"m"月"d"日"';
315
            self::$builtInFormats[32] = 'h"時"mm"分"';
316
            self::$builtInFormats[33] = 'h"時"mm"分"ss"秒"';
317
            self::$builtInFormats[34] = 'yyyy"年"m"月"';
318
            self::$builtInFormats[35] = 'm"月"d"日"';
319
            self::$builtInFormats[51] = '[$-411]ggge"年"m"月"d"日"';
320
            self::$builtInFormats[52] = 'yyyy"年"m"月"';
321
            self::$builtInFormats[53] = 'm"月"d"日"';
322
            self::$builtInFormats[54] = '[$-411]ggge"年"m"月"d"日"';
323 102
            self::$builtInFormats[55] = 'yyyy"年"m"月"';
324
            self::$builtInFormats[56] = 'm"月"d"日"';
325
            self::$builtInFormats[58] = '[$-411]ggge"年"m"月"d"日"';
326 102
327
            // Flip array (for faster lookups)
328
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
329 102
        }
330
    }
331
332 102
    /**
333 102
     * Get built-in format code.
334
     *
335
     * @param int $pIndex
336 4
     *
337
     * @return string
338
     */
339
    public static function builtInFormatCode($pIndex)
340
    {
341
        // Clean parameter
342
        $pIndex = (int) $pIndex;
343
344
        // Ensure built-in format codes are available
345
        self::fillBuiltInFormatCodes();
346 100
347
        // Lookup format code
348
        if (isset(self::$builtInFormats[$pIndex])) {
349 100
            return self::$builtInFormats[$pIndex];
350
        }
351
352 100
        return '';
353 91
    }
354
355
    /**
356 49
     * Get built-in format code index.
357
     *
358
     * @param string $formatCode
359
     *
360
     * @return bool|int
361
     */
362
    public static function builtInFormatCodeIndex($formatCode)
363
    {
364 114
        // Ensure built-in format codes are available
365
        self::fillBuiltInFormatCodes();
366 114
367
        // Lookup format code
368
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
369
            return self::$flippedBuiltInFormats[$formatCode];
370 114
        }
371 114
372 114
        return false;
373 114
    }
374
375
    /**
376
     * Get hash code.
377
     *
378
     * @return string Hash code
379
     */
380
    public function getHashCode()
381
    {
382
        if ($this->isSupervisor) {
383
            return $this->getSharedComponent()->getHashCode();
384
        }
385
386
        return md5(
387
            $this->formatCode .
388
            $this->builtInFormatCode .
389
            __CLASS__
390
        );
391
    }
392
393
    /**
394
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
395
     *
396
     * @var array
397
     */
398
    private static $dateFormatReplacements = [
399
            // first remove escapes related to non-format characters
400
            '\\' => '',
401
            //    12-hour suffix
402
            'am/pm' => 'A',
403
            //    4-digit year
404
            'e' => 'Y',
405
            'yyyy' => 'Y',
406
            //    2-digit year
407
            'yy' => 'y',
408
            //    first letter of month - no php equivalent
409
            'mmmmm' => 'M',
410
            //    full month name
411
            'mmmm' => 'F',
412
            //    short month name
413
            'mmm' => 'M',
414
            //    mm is minutes if time, but can also be month w/leading zero
415
            //    so we try to identify times be the inclusion of a : separator in the mask
416
            //    It isn't perfect, but the best way I know how
417
            ':mm' => ':i',
418
            'mm:' => 'i:',
419
            //    month leading zero
420
            'mm' => 'm',
421
            //    month no leading zero
422
            'm' => 'n',
423
            //    full day of week name
424
            'dddd' => 'l',
425
            //    short day of week name
426
            'ddd' => 'D',
427
            //    days leading zero
428
            'dd' => 'd',
429
            //    days no leading zero
430
            'd' => 'j',
431
            //    seconds
432
            'ss' => 's',
433
            //    fractional seconds - no php equivalent
434
            '.s' => '',
435
        ];
436
437
    /**
438
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
439
     *
440
     * @var array
441 31
     */
442
    private static $dateFormatReplacements24 = [
443 31
        'hh' => 'H',
444
        'h' => 'G',
445
    ];
446 7
447
    /**
448 7
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
449
     *
450
     * @var array
451 31
     */
452
    private static $dateFormatReplacements12 = [
453
        'hh' => 'h',
454
        'h' => 'g',
455
    ];
456
457 31
    private static function setLowercaseCallback($matches)
458
    {
459
        return mb_strtolower($matches[0]);
460
    }
461 31
462
    private static function escapeQuotesCallback($matches)
463
    {
464 31
        return '\\' . implode('\\', str_split($matches[1]));
465 31
    }
466 31
467 31
    private static function formatAsDate(&$value, &$format)
468 31
    {
469
        // strip off first part containing e.g. [$-F800] or [$USD-409]
470
        // general syntax: [$<Currency string>-<language info>]
471 29
        // language info is in hexadecimal
472 1
        // strip off chinese part like [DBNum1][$-804]
473 1
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
474
475 1
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
476
        //    but we don't want to change any quoted strings
477 28
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
478
479
        // Only process the non-quoted blocks for date format characters
480 2
        $blocks = explode('"', $format);
481
        foreach ($blocks as $key => &$block) {
482
            if ($key % 2 == 0) {
483
                $block = strtr($block, self::$dateFormatReplacements);
484 31
                if (!strpos($block, 'A')) {
485
                    // 24-hour time format
486
                    // when [h]:mm format, the [h] should replace to the hours of the value * 24
487 31
                    if (false !== strpos($block, '[h]')) {
488
                        $hours = (int) ($value * 24);
489 31
                        $block = str_replace('[h]', $hours, $block);
490 31
491 31
                        continue;
492
                    }
493 5
                    $block = strtr($block, self::$dateFormatReplacements24);
494
                } else {
495 5
                    // 12-hour time format
496 4
                    $block = strtr($block, self::$dateFormatReplacements12);
497
                }
498 1
            }
499 1
        }
500 1
        $format = implode('"', $blocks);
501
502 1
        // escape any quoted characters so that DateTime format() will render them correctly
503 1
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
504
505 1
        $dateObj = Date::excelToDateTimeObject($value);
506
        $value = $dateObj->format($format);
507 1
    }
508
509 5
    private static function formatAsPercentage(&$value, &$format)
510
    {
511 4
        if ($format === self::FORMAT_PERCENTAGE) {
512
            $value = round((100 * $value), 0) . '%';
513 4
        } else {
514
            if (preg_match('/\.[#0]+/', $format, $m)) {
515 4
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
516 4
                $format = str_replace($m[0], $s, $format);
517 4
            }
518 4
            if (preg_match('/^[#0]+/', $format, $m)) {
519
                $format = str_replace($m[0], strlen($m[0]), $format);
520 4
            }
521
            $format = '%' . str_replace('%', 'f%%', $format);
522 4
523 4
            $value = sprintf($format, 100 * $value);
524
        }
525 4
    }
526 3
527
    private static function formatAsFraction(&$value, &$format)
528
    {
529 3
        $sign = ($value < 0) ? '-' : '';
530
531 1
        $integerPart = floor(abs($value));
532 1
        $decimalPart = trim(fmod(abs($value), 1), '0.');
533
        $decimalLength = strlen($decimalPart);
534 4
        $decimalDivisor = pow(10, $decimalLength);
535
536 6
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
537
538 6
        $adjustedDecimalPart = $decimalPart / $GCD;
539 6
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
540 6
541 2
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
542 2
            if ($integerPart == 0) {
543 2
                $integerPart = '';
544 2
            }
545
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
546 2
        } else {
547
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
548
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
549 6
        }
550 6
    }
551 6
552
    private static function complexNumberFormatMask($number, $mask)
553 6
    {
554 6
        $sign = ($number < 0.0);
555 6
        $number = abs($number);
556 6
        if (strpos($mask, '.') !== false) {
557
            $numbers = explode('.', $number . '.0');
558 6
            $masks = explode('.', $mask . '.0');
559 6
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
560 6
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
561
562 6
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
563 6
        }
564
565 6
        $r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
566 4
        if ($r > 1) {
567
            $result = array_reverse($result[0]);
568 6
569
            foreach ($result as $block) {
570 2
                $divisor = 1 . $block[0];
571
                $size = strlen($block[0]);
572
                $offset = $block[1];
573 6
574
                $blockValue = sprintf(
575
                    '%0' . $size . 'd',
576
                    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

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

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