Failed Conditions
Push — master ( ab1c6e...20f36c )
by Mark
26:02
created

NumberFormat::processComplexNumberFormatMask()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 3
nop 2
dl 0
loc 27
ccs 18
cts 18
cp 1
crap 4
rs 9.7
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
    const FORMAT_ACCOUNTING_USD = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
53
    const FORMAT_ACCOUNTING_EUR = '_("€"* #,##0.00_);_("€"* \(#,##0.00\);_("€"* "-"??_);_(@_)';
54
55
    /**
56
     * Excel built-in number formats.
57
     *
58
     * @var array
59
     */
60
    protected static $builtInFormats;
61
62
    /**
63
     * Excel built-in number formats (flipped, for faster lookups).
64
     *
65
     * @var array
66
     */
67
    protected static $flippedBuiltInFormats;
68
69
    /**
70
     * Format Code.
71
     *
72
     * @var string
73
     */
74
    protected $formatCode = self::FORMAT_GENERAL;
75
76
    /**
77
     * Built-in format Code.
78
     *
79
     * @var string
80
     */
81
    protected $builtInFormatCode = 0;
82
83
    /**
84
     * Create a new NumberFormat.
85
     *
86
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
87
     *                                    Leave this value at default unless you understand exactly what
88
     *                                        its ramifications are
89
     * @param bool $isConditional Flag indicating if this is a conditional style or not
90
     *                                    Leave this value at default unless you understand exactly what
91
     *                                        its ramifications are
92
     */
93 222
    public function __construct($isSupervisor = false, $isConditional = false)
94
    {
95
        // Supervisor?
96 222
        parent::__construct($isSupervisor);
97
98 222
        if ($isConditional) {
99 5
            $this->formatCode = null;
100 5
            $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...
101
        }
102 222
    }
103
104
    /**
105
     * Get the shared style component for the currently active cell in currently active sheet.
106
     * Only used for style supervisor.
107
     *
108
     * @return NumberFormat
109
     */
110 6
    public function getSharedComponent()
111
    {
112 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

112
        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...
113
    }
114
115
    /**
116
     * Build style array from subcomponents.
117
     *
118
     * @param array $array
119
     *
120
     * @return array
121
     */
122 38
    public function getStyleArray($array)
123
    {
124 38
        return ['numberFormat' => $array];
125
    }
126
127
    /**
128
     * Apply styles from array.
129
     *
130
     * <code>
131
     * $spreadsheet->getActiveSheet()->getStyle('B2')->getNumberFormat()->applyFromArray(
132
     *     [
133
     *         'formatCode' => NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE
134
     *     ]
135
     * );
136
     * </code>
137
     *
138
     * @param array $pStyles Array containing style information
139
     *
140
     * @throws PhpSpreadsheetException
141
     *
142
     * @return NumberFormat
143
     */
144 42
    public function applyFromArray(array $pStyles)
145
    {
146 42
        if ($this->isSupervisor) {
147
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($this->getStyleArray($pStyles));
148
        } else {
149 42
            if (isset($pStyles['formatCode'])) {
150 42
                $this->setFormatCode($pStyles['formatCode']);
151
            }
152
        }
153
154 42
        return $this;
155
    }
156
157
    /**
158
     * Get Format Code.
159
     *
160
     * @return string
161
     */
162 65
    public function getFormatCode()
163
    {
164 65
        if ($this->isSupervisor) {
165 6
            return $this->getSharedComponent()->getFormatCode();
166
        }
167 65
        if ($this->builtInFormatCode !== false) {
0 ignored issues
show
introduced by
The condition $this->builtInFormatCode !== false is always true.
Loading history...
168 55
            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

168
            return self::builtInFormatCode(/** @scrutinizer ignore-type */ $this->builtInFormatCode);
Loading history...
169
        }
170
171 33
        return $this->formatCode;
172
    }
173
174
    /**
175
     * Set Format Code.
176
     *
177
     * @param string $pValue see self::FORMAT_*
178
     *
179
     * @return NumberFormat
180
     */
181 112
    public function setFormatCode($pValue)
182
    {
183 112
        if ($pValue == '') {
184 3
            $pValue = self::FORMAT_GENERAL;
185
        }
186 112
        if ($this->isSupervisor) {
187 38
            $styleArray = $this->getStyleArray(['formatCode' => $pValue]);
188 38
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
189
        } else {
190 112
            $this->formatCode = $pValue;
191 112
            $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...
192
        }
193
194 112
        return $this;
195
    }
196
197
    /**
198
     * Get Built-In Format Code.
199
     *
200
     * @return int
201
     */
202 92
    public function getBuiltInFormatCode()
203
    {
204 92
        if ($this->isSupervisor) {
205
            return $this->getSharedComponent()->getBuiltInFormatCode();
206
        }
207
208 92
        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...
209
    }
210
211
    /**
212
     * Set Built-In Format Code.
213
     *
214
     * @param int $pValue
215
     *
216
     * @return NumberFormat
217
     */
218
    public function setBuiltInFormatCode($pValue)
219
    {
220
        if ($this->isSupervisor) {
221
            $styleArray = $this->getStyleArray(['formatCode' => self::builtInFormatCode($pValue)]);
222
            $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray);
223
        } else {
224
            $this->builtInFormatCode = $pValue;
225
            $this->formatCode = self::builtInFormatCode($pValue);
226
        }
227
228
        return $this;
229
    }
230
231
    /**
232
     * Fill built-in format codes.
233
     */
234 137
    private static function fillBuiltInFormatCodes()
235
    {
236
        //  [MS-OI29500: Microsoft Office Implementation Information for ISO/IEC-29500 Standard Compliance]
237
        //  18.8.30. numFmt (Number Format)
238
        //
239
        //  The ECMA standard defines built-in format IDs
240
        //      14: "mm-dd-yy"
241
        //      22: "m/d/yy h:mm"
242
        //      37: "#,##0 ;(#,##0)"
243
        //      38: "#,##0 ;[Red](#,##0)"
244
        //      39: "#,##0.00;(#,##0.00)"
245
        //      40: "#,##0.00;[Red](#,##0.00)"
246
        //      47: "mmss.0"
247
        //      KOR fmt 55: "yyyy-mm-dd"
248
        //  Excel defines built-in format IDs
249
        //      14: "m/d/yyyy"
250
        //      22: "m/d/yyyy h:mm"
251
        //      37: "#,##0_);(#,##0)"
252
        //      38: "#,##0_);[Red](#,##0)"
253
        //      39: "#,##0.00_);(#,##0.00)"
254
        //      40: "#,##0.00_);[Red](#,##0.00)"
255
        //      47: "mm:ss.0"
256
        //      KOR fmt 55: "yyyy/mm/dd"
257
258
        // Built-in format codes
259 137
        if (self::$builtInFormats === null) {
0 ignored issues
show
introduced by
The condition self::builtInFormats === null is always false.
Loading history...
260 70
            self::$builtInFormats = [];
261
262
            // General
263 70
            self::$builtInFormats[0] = self::FORMAT_GENERAL;
264 70
            self::$builtInFormats[1] = '0';
265 70
            self::$builtInFormats[2] = '0.00';
266 70
            self::$builtInFormats[3] = '#,##0';
267 70
            self::$builtInFormats[4] = '#,##0.00';
268
269 70
            self::$builtInFormats[9] = '0%';
270 70
            self::$builtInFormats[10] = '0.00%';
271 70
            self::$builtInFormats[11] = '0.00E+00';
272 70
            self::$builtInFormats[12] = '# ?/?';
273 70
            self::$builtInFormats[13] = '# ??/??';
274 70
            self::$builtInFormats[14] = 'm/d/yyyy'; // Despite ECMA 'mm-dd-yy';
275 70
            self::$builtInFormats[15] = 'd-mmm-yy';
276 70
            self::$builtInFormats[16] = 'd-mmm';
277 70
            self::$builtInFormats[17] = 'mmm-yy';
278 70
            self::$builtInFormats[18] = 'h:mm AM/PM';
279 70
            self::$builtInFormats[19] = 'h:mm:ss AM/PM';
280 70
            self::$builtInFormats[20] = 'h:mm';
281 70
            self::$builtInFormats[21] = 'h:mm:ss';
282 70
            self::$builtInFormats[22] = 'm/d/yyyy h:mm'; // Despite ECMA 'm/d/yy h:mm';
283
284 70
            self::$builtInFormats[37] = '#,##0_);(#,##0)'; //  Despite ECMA '#,##0 ;(#,##0)';
285 70
            self::$builtInFormats[38] = '#,##0_);[Red](#,##0)'; //  Despite ECMA '#,##0 ;[Red](#,##0)';
286 70
            self::$builtInFormats[39] = '#,##0.00_);(#,##0.00)'; //  Despite ECMA '#,##0.00;(#,##0.00)';
287 70
            self::$builtInFormats[40] = '#,##0.00_);[Red](#,##0.00)'; //  Despite ECMA '#,##0.00;[Red](#,##0.00)';
288
289 70
            self::$builtInFormats[44] = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
290 70
            self::$builtInFormats[45] = 'mm:ss';
291 70
            self::$builtInFormats[46] = '[h]:mm:ss';
292 70
            self::$builtInFormats[47] = 'mm:ss.0'; //  Despite ECMA 'mmss.0';
293 70
            self::$builtInFormats[48] = '##0.0E+0';
294 70
            self::$builtInFormats[49] = '@';
295
296
            // CHT
297 70
            self::$builtInFormats[27] = '[$-404]e/m/d';
298 70
            self::$builtInFormats[30] = 'm/d/yy';
299 70
            self::$builtInFormats[36] = '[$-404]e/m/d';
300 70
            self::$builtInFormats[50] = '[$-404]e/m/d';
301 70
            self::$builtInFormats[57] = '[$-404]e/m/d';
302
303
            // THA
304 70
            self::$builtInFormats[59] = 't0';
305 70
            self::$builtInFormats[60] = 't0.00';
306 70
            self::$builtInFormats[61] = 't#,##0';
307 70
            self::$builtInFormats[62] = 't#,##0.00';
308 70
            self::$builtInFormats[67] = 't0%';
309 70
            self::$builtInFormats[68] = 't0.00%';
310 70
            self::$builtInFormats[69] = 't# ?/?';
311 70
            self::$builtInFormats[70] = 't# ??/??';
312
313
            // JPN
314 70
            self::$builtInFormats[28] = '[$-411]ggge"年"m"月"d"日"';
315 70
            self::$builtInFormats[29] = '[$-411]ggge"年"m"月"d"日"';
316 70
            self::$builtInFormats[31] = 'yyyy"年"m"月"d"日"';
317 70
            self::$builtInFormats[32] = 'h"時"mm"分"';
318 70
            self::$builtInFormats[33] = 'h"時"mm"分"ss"秒"';
319 70
            self::$builtInFormats[34] = 'yyyy"年"m"月"';
320 70
            self::$builtInFormats[35] = 'm"月"d"日"';
321 70
            self::$builtInFormats[51] = '[$-411]ggge"年"m"月"d"日"';
322 70
            self::$builtInFormats[52] = 'yyyy"年"m"月"';
323 70
            self::$builtInFormats[53] = 'm"月"d"日"';
324 70
            self::$builtInFormats[54] = '[$-411]ggge"年"m"月"d"日"';
325 70
            self::$builtInFormats[55] = 'yyyy"年"m"月"';
326 70
            self::$builtInFormats[56] = 'm"月"d"日"';
327 70
            self::$builtInFormats[58] = '[$-411]ggge"年"m"月"d"日"';
328
329
            // Flip array (for faster lookups)
330 70
            self::$flippedBuiltInFormats = array_flip(self::$builtInFormats);
331
        }
332 137
    }
333
334
    /**
335
     * Get built-in format code.
336
     *
337
     * @param int $pIndex
338
     *
339
     * @return string
340
     */
341 114
    public static function builtInFormatCode($pIndex)
342
    {
343
        // Clean parameter
344 114
        $pIndex = (int) $pIndex;
345
346
        // Ensure built-in format codes are available
347 114
        self::fillBuiltInFormatCodes();
348
349
        // Lookup format code
350 114
        if (isset(self::$builtInFormats[$pIndex])) {
351 114
            return self::$builtInFormats[$pIndex];
352
        }
353
354 4
        return '';
355
    }
356
357
    /**
358
     * Get built-in format code index.
359
     *
360
     * @param string $formatCode
361
     *
362
     * @return bool|int
363
     */
364 112
    public static function builtInFormatCodeIndex($formatCode)
365
    {
366
        // Ensure built-in format codes are available
367 112
        self::fillBuiltInFormatCodes();
368
369
        // Lookup format code
370 112
        if (isset(self::$flippedBuiltInFormats[$formatCode])) {
371 103
            return self::$flippedBuiltInFormats[$formatCode];
372
        }
373
374 51
        return false;
375
    }
376
377
    /**
378
     * Get hash code.
379
     *
380
     * @return string Hash code
381
     */
382 119
    public function getHashCode()
383
    {
384 119
        if ($this->isSupervisor) {
385
            return $this->getSharedComponent()->getHashCode();
386
        }
387
388 119
        return md5(
389 119
            $this->formatCode .
390 119
            $this->builtInFormatCode .
391 119
            __CLASS__
392
        );
393
    }
394
395
    /**
396
     * Search/replace values to convert Excel date/time format masks to PHP format masks.
397
     *
398
     * @var array
399
     */
400
    private static $dateFormatReplacements = [
401
            // first remove escapes related to non-format characters
402
            '\\' => '',
403
            //    12-hour suffix
404
            'am/pm' => 'A',
405
            //    4-digit year
406
            'e' => 'Y',
407
            'yyyy' => 'Y',
408
            //    2-digit year
409
            'yy' => 'y',
410
            //    first letter of month - no php equivalent
411
            'mmmmm' => 'M',
412
            //    full month name
413
            'mmmm' => 'F',
414
            //    short month name
415
            'mmm' => 'M',
416
            //    mm is minutes if time, but can also be month w/leading zero
417
            //    so we try to identify times be the inclusion of a : separator in the mask
418
            //    It isn't perfect, but the best way I know how
419
            ':mm' => ':i',
420
            'mm:' => 'i:',
421
            //    month leading zero
422
            'mm' => 'm',
423
            //    month no leading zero
424
            'm' => 'n',
425
            //    full day of week name
426
            'dddd' => 'l',
427
            //    short day of week name
428
            'ddd' => 'D',
429
            //    days leading zero
430
            'dd' => 'd',
431
            //    days no leading zero
432
            'd' => 'j',
433
            //    seconds
434
            'ss' => 's',
435
            //    fractional seconds - no php equivalent
436
            '.s' => '',
437
        ];
438
439
    /**
440
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock).
441
     *
442
     * @var array
443
     */
444
    private static $dateFormatReplacements24 = [
445
        'hh' => 'H',
446
        'h' => 'G',
447
    ];
448
449
    /**
450
     * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock).
451
     *
452
     * @var array
453
     */
454
    private static $dateFormatReplacements12 = [
455
        'hh' => 'h',
456
        'h' => 'g',
457
    ];
458
459 31
    private static function setLowercaseCallback($matches)
460
    {
461 31
        return mb_strtolower($matches[0]);
462
    }
463
464 11
    private static function escapeQuotesCallback($matches)
465
    {
466 11
        return '\\' . implode('\\', str_split($matches[1]));
467
    }
468
469 31
    private static function formatAsDate(&$value, &$format)
470
    {
471
        // strip off first part containing e.g. [$-F800] or [$USD-409]
472
        // general syntax: [$<Currency string>-<language info>]
473
        // language info is in hexadecimal
474
        // strip off chinese part like [DBNum1][$-804]
475 31
        $format = preg_replace('/^(\[[0-9A-Za-z]*\])*(\[\$[A-Z]*-[0-9A-F]*\])/i', '', $format);
476
477
        // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case;
478
        //    but we don't want to change any quoted strings
479 31
        $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format);
480
481
        // Only process the non-quoted blocks for date format characters
482 31
        $blocks = explode('"', $format);
483 31
        foreach ($blocks as $key => &$block) {
484 31
            if ($key % 2 == 0) {
485 31
                $block = strtr($block, self::$dateFormatReplacements);
486 31
                if (!strpos($block, 'A')) {
487
                    // 24-hour time format
488
                    // when [h]:mm format, the [h] should replace to the hours of the value * 24
489 29
                    if (false !== strpos($block, '[h]')) {
490 1
                        $hours = (int) ($value * 24);
491 1
                        $block = str_replace('[h]', $hours, $block);
492
493 1
                        continue;
494
                    }
495 28
                    $block = strtr($block, self::$dateFormatReplacements24);
496
                } else {
497
                    // 12-hour time format
498 2
                    $block = strtr($block, self::$dateFormatReplacements12);
499
                }
500
            }
501
        }
502 31
        $format = implode('"', $blocks);
503
504
        // escape any quoted characters so that DateTime format() will render them correctly
505 31
        $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format);
506
507 31
        $dateObj = Date::excelToDateTimeObject($value);
508 31
        $value = $dateObj->format($format);
509 31
    }
510
511 5
    private static function formatAsPercentage(&$value, &$format)
512
    {
513 5
        if ($format === self::FORMAT_PERCENTAGE) {
514 4
            $value = round((100 * $value), 0) . '%';
515
        } else {
516 1
            if (preg_match('/\.[#0]+/', $format, $m)) {
517 1
                $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1);
518 1
                $format = str_replace($m[0], $s, $format);
519
            }
520 1
            if (preg_match('/^[#0]+/', $format, $m)) {
521 1
                $format = str_replace($m[0], strlen($m[0]), $format);
522
            }
523 1
            $format = '%' . str_replace('%', 'f%%', $format);
524
525 1
            $value = sprintf($format, 100 * $value);
526
        }
527 5
    }
528
529 4
    private static function formatAsFraction(&$value, &$format)
530
    {
531 4
        $sign = ($value < 0) ? '-' : '';
532
533 4
        $integerPart = floor(abs($value));
534 4
        $decimalPart = trim(fmod(abs($value), 1), '0.');
535 4
        $decimalLength = strlen($decimalPart);
536 4
        $decimalDivisor = pow(10, $decimalLength);
537
538 4
        $GCD = MathTrig::GCD($decimalPart, $decimalDivisor);
539
540 4
        $adjustedDecimalPart = $decimalPart / $GCD;
541 4
        $adjustedDecimalDivisor = $decimalDivisor / $GCD;
542
543 4
        if ((strpos($format, '0') !== false) || (strpos($format, '#') !== false) || (substr($format, 0, 3) == '? ?')) {
544 3
            if ($integerPart == 0) {
545
                $integerPart = '';
546
            }
547 3
            $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor";
548
        } else {
549 1
            $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor;
550 1
            $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor";
551
        }
552 4
    }
553
554 2
    private static function mergeComplexNumberFormatMasks($numbers, $masks)
555
    {
556 2
        $decimalCount = strlen($numbers[1]);
557 2
        $postDecimalMasks = [];
558
559
        do {
560 2
            $tempMask = array_pop($masks);
561 2
            $postDecimalMasks[] = $tempMask;
562 2
            $decimalCount -= strlen($tempMask);
563 2
        } while ($decimalCount > 0);
564
565
        return [
566 2
            implode('.', $masks),
567 2
            implode('.', array_reverse($postDecimalMasks)),
568
        ];
569
    }
570
571 11
    private static function processComplexNumberFormatMask($number, $mask)
572
    {
573 11
        $result = $number;
574 11
        $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
575
576 11
        if ($maskingBlockCount > 1) {
577 11
            $maskingBlocks = array_reverse($maskingBlocks[0]);
578
579 11
            foreach ($maskingBlocks as $block) {
580 11
                $divisor = 1 . $block[0];
581 11
                $size = strlen($block[0]);
582 11
                $offset = $block[1];
583
584 11
                $blockValue = sprintf(
585 11
                    '%0' . $size . 'd',
586 11
                    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

586
                    fmod($number, /** @scrutinizer ignore-type */ $divisor)
Loading history...
587
                );
588 11
                $number = floor($number / $divisor);
589 11
                $mask = substr_replace($mask, $blockValue, $offset, $size);
590
            }
591 11
            if ($number > 0) {
592 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 579. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
593
            }
594 11
            $result = $mask;
595
        }
596
597 11
        return $result;
598
    }
599
600 11
    private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true)
601
    {
602 11
        $sign = ($number < 0.0);
603 11
        $number = abs($number);
604 11
        if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
605 5
            $numbers = explode('.', $number);
606 5
            $masks = explode('.', $mask);
607 5
            if (count($masks) > 2) {
608 2
                $masks = self::mergeComplexNumberFormatMasks($numbers, $masks);
609
            }
610 5
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0], false);
611 5
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false));
612
613 5
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
614
        }
615
616 11
        $result = self::processComplexNumberFormatMask($number, $mask);
617
618 11
        return (($sign) ? '-' : '') . $result;
619
    }
620
621 72
    private static function formatAsNumber($value, $format)
622
    {
623 72
        if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
624
            return 'EUR ' . sprintf('%1.2f', $value);
625
        }
626
627
        // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
628 72
        $format = str_replace(['"', '*'], '', $format);
629
630
        // Find out if we need thousands separator
631
        // This is indicated by a comma enclosed by a digit placeholder:
632
        //        #,#   or   0,0
633 72
        $useThousands = preg_match('/(#,#|0,0)/', $format);
634 72
        if ($useThousands) {
635 31
            $format = preg_replace('/0,0/', '00', $format);
636 31
            $format = preg_replace('/#,#/', '##', $format);
637
        }
638
639
        // Scale thousands, millions,...
640
        // This is indicated by a number of commas after a digit placeholder:
641
        //        #,   or    0.0,,
642 72
        $scale = 1; // same as no scale
643 72
        $matches = [];
644 72
        if (preg_match('/(#|0)(,+)/', $format, $matches)) {
645 2
            $scale = pow(1000, strlen($matches[2]));
646
647
            // strip the commas
648 2
            $format = preg_replace('/0,+/', '0', $format);
649 2
            $format = preg_replace('/#,+/', '#', $format);
650
        }
651
652 72
        if (preg_match('/#?.*\?\/\?/', $format, $m)) {
653 4
            if ($value != (int) $value) {
654 4
                self::formatAsFraction($value, $format);
655
            }
656
        } else {
657
            // Handle the number itself
658
659
            // scale number
660 68
            $value = $value / $scale;
661
662
            // Strip #
663 68
            $format = preg_replace('/\\#/', '0', $format);
664
665
            // Remove locale code [$-###]
666 68
            $format = preg_replace('/\[\$\-.*\]/', '', $format);
667
668 68
            $n = '/\\[[^\\]]+\\]/';
669 68
            $m = preg_replace($n, '', $format);
670 68
            $number_regex = '/(0+)(\\.?)(0*)/';
671 68
            if (preg_match($number_regex, $m, $matches)) {
672 65
                $left = $matches[1];
673 65
                $dec = $matches[2];
674 65
                $right = $matches[3];
675
676
                // minimun width of formatted number (including dot)
677 65
                $minWidth = strlen($left) + strlen($dec) + strlen($right);
678 65
                if ($useThousands) {
679 31
                    $value = number_format(
680 31
                        $value,
681 31
                        strlen($right),
682 31
                        StringHelper::getDecimalSeparator(),
683 31
                        StringHelper::getThousandsSeparator()
684
                    );
685 31
                    $value = preg_replace($number_regex, $value, $format);
686
                } else {
687 38
                    if (preg_match('/[0#]E[+-]0/i', $format)) {
688
                        //    Scientific format
689 7
                        $value = sprintf('%5.2E', $value);
690 35
                    } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) {
691 11
                        $value = self::complexNumberFormatMask($value, $format);
692
                    } else {
693 24
                        $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
694 24
                        $value = sprintf($sprintf_pattern, $value);
695 24
                        $value = preg_replace($number_regex, $value, $format);
696
                    }
697
                }
698
            }
699
        }
700
701 72
        if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
702
            //  Currency or Accounting
703 1
            $currencyCode = $m[1];
704 1
            list($currencyCode) = explode('-', $currencyCode);
705 1
            if ($currencyCode == '') {
706
                $currencyCode = StringHelper::getCurrencyCode();
707
            }
708 1
            $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
709
        }
710
711 72
        return $value;
712
    }
713
714
    /**
715
     * Convert a value in a pre-defined format to a PHP string.
716
     *
717
     * @param mixed $value Value to format
718
     * @param string $format Format code, see = self::FORMAT_*
719
     * @param array $callBack Callback function for additional formatting of string
720
     *
721
     * @return string Formatted string
722
     */
723 142
    public static function toFormattedString($value, $format, $callBack = null)
724
    {
725
        // For now we do not treat strings although section 4 of a format code affects strings
726 142
        if (!is_numeric($value)) {
727 52
            return $value;
728
        }
729
730
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
731
        // it seems to round numbers to a total of 10 digits.
732 120
        if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) {
733 28
            return $value;
734
        }
735
736
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
737 102
        $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
738
739
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
740 102
        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
741
742
        // Extract the relevant section depending on whether number is positive, negative, or zero?
743
        // Text not supported yet.
744
        // Here is how the sections apply to various values in Excel:
745
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
746
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
747
        //   3 sections:  [POSITIVE/TEXT] [NEGATIVE] [ZERO]
748
        //   4 sections:  [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
749 102
        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

749
        switch (count(/** @scrutinizer ignore-type */ $sections)) {
Loading history...
750 102
            case 1:
751 86
                $format = $sections[0];
752
753 86
                break;
754 20
            case 2:
755 15
                $format = ($value >= 0) ? $sections[0] : $sections[1];
756 15
                $value = abs($value); // Use the absolute value
757 15
                break;
758 5
            case 3:
759 3
                $format = ($value > 0) ?
760 3
                    $sections[0] : (($value < 0) ?
761 3
                        $sections[1] : $sections[2]);
762 3
                $value = abs($value); // Use the absolute value
763 3
                break;
764 2
            case 4:
765 2
                $format = ($value > 0) ?
766 2
                    $sections[0] : (($value < 0) ?
767 2
                        $sections[1] : $sections[2]);
768 2
                $value = abs($value); // Use the absolute value
769 2
                break;
770
            default:
771
                // something is wrong, just use first section
772
                $format = $sections[0];
773
774
                break;
775
        }
776
777
        // In Excel formats, "_" is used to add spacing,
778
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
779 102
        $format = preg_replace('/_./', ' ', $format);
780
781
        // Save format with color information for later use below
782 102
        $formatColor = $format;
783
        // Strip colour information
784 102
        $color_regex = '/\[(' . implode('|', Color::NAMED_COLORS) . ')\]/';
785 102
        $format = preg_replace($color_regex, '', $format);
786
        // Let's begin inspecting the format and converting the value to a formatted string
787
788
        //  Check for date/time characters (not inside quotes)
789 102
        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
790
            // datetime format
791 31
            self::formatAsDate($value, $format);
792
        } else {
793 80
            if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"') {
794 3
                $value = substr($format, 1, -1);
795 77
            } elseif (preg_match('/%$/', $format)) {
796
                // % number format
797 5
                self::formatAsPercentage($value, $format);
798
            } else {
799 72
                $value = self::formatAsNumber($value, $format);
800
            }
801
        }
802
803
        // Additional formatting provided by callback function
804 102
        if ($callBack !== null) {
805 3
            list($writerInstance, $function) = $callBack;
806 3
            $value = $writerInstance->$function($value, $formatColor);
807
        }
808
809 102
        return $value;
810
    }
811
}
812