Completed
Push — master ( 4f6d4a...97a80f )
by Adrien
11:15 queued 03:45
created

NumberFormat::setBuiltInFormatCode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 1
dl 0
loc 11
ccs 0
cts 7
cp 0
crap 6
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\Shared\Date;
7
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
8
9
class NumberFormat extends Supervisor
10
{
11
    // Pre-defined formats
12
    const FORMAT_GENERAL = 'General';
13
14
    const FORMAT_TEXT = '@';
15
16
    const FORMAT_NUMBER = '0';
17
    const FORMAT_NUMBER_00 = '0.00';
18
    const FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00';
19
    const FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-';
20
21
    const FORMAT_PERCENTAGE = '0%';
22
    const FORMAT_PERCENTAGE_00 = '0.00%';
23
24
    const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd';
25
    const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd';
26
    const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy';
27
    const FORMAT_DATE_DMYSLASH = 'd/m/yy';
28
    const FORMAT_DATE_DMYMINUS = 'd-m-yy';
29
    const FORMAT_DATE_DMMINUS = 'd-m';
30
    const FORMAT_DATE_MYMINUS = 'm-yy';
31
    const FORMAT_DATE_XLSX14 = 'mm-dd-yy';
32
    const FORMAT_DATE_XLSX15 = 'd-mmm-yy';
33
    const FORMAT_DATE_XLSX16 = 'd-mmm';
34
    const FORMAT_DATE_XLSX17 = 'mmm-yy';
35
    const FORMAT_DATE_XLSX22 = 'm/d/yy h:mm';
36
    const FORMAT_DATE_DATETIME = 'd/m/yy h:mm';
37
    const FORMAT_DATE_TIME1 = 'h:mm AM/PM';
38
    const FORMAT_DATE_TIME2 = 'h:mm:ss AM/PM';
39
    const FORMAT_DATE_TIME3 = 'h:mm';
40
    const FORMAT_DATE_TIME4 = 'h:mm:ss';
41
    const FORMAT_DATE_TIME5 = 'mm:ss';
42
    const FORMAT_DATE_TIME6 = 'h:mm:ss';
43
    const FORMAT_DATE_TIME7 = 'i:s.S';
44
    const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
45
    const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@';
46
47
    const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-';
48
    const FORMAT_CURRENCY_USD = '$#,##0_-';
49
    const FORMAT_CURRENCY_EUR_SIMPLE = '#,##0.00_-"€"';
50
    const FORMAT_CURRENCY_EUR = '#,##0_-"€"';
51
    const FORMAT_ACCOUNTING_USD = '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';
52
    const FORMAT_ACCOUNTING_EUR = '_("€"* #,##0.00_);_("€"* \(#,##0.00\);_("€"* "-"??_);_(@_)';
53
54
    /**
55
     * Excel built-in number formats.
56
     *
57
     * @var array
58
     */
59
    protected static $builtInFormats;
60
61
    /**
62
     * Excel built-in number formats (flipped, for faster lookups).
63
     *
64
     * @var array
65
     */
66
    protected static $flippedBuiltInFormats;
67
68
    /**
69
     * Format Code.
70
     *
71
     * @var string
72
     */
73
    protected $formatCode = self::FORMAT_GENERAL;
74
75
    /**
76
     * Built-in format Code.
77
     *
78
     * @var string
79
     */
80
    protected $builtInFormatCode = 0;
81
82
    /**
83
     * Create a new NumberFormat.
84
     *
85
     * @param bool $isSupervisor Flag indicating if this is a supervisor or not
86
     *                                    Leave this value at default unless you understand exactly what
87
     *                                        its ramifications are
88
     * @param bool $isConditional Flag indicating if this is a conditional style or not
89
     *                                    Leave this value at default unless you understand exactly what
90
     *                                        its ramifications are
91
     */
92 342
    public function __construct($isSupervisor = false, $isConditional = false)
93
    {
94
        // Supervisor?
95 342
        parent::__construct($isSupervisor);
96
97 342
        if ($isConditional) {
98 8
            $this->formatCode = null;
99 8
            $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...
100
        }
101 342
    }
102
103
    /**
104
     * Get the shared style component for the currently active cell in currently active sheet.
105
     * Only used for style supervisor.
106
     *
107
     * @return NumberFormat
108
     */
109 6
    public function getSharedComponent()
110
    {
111 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

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

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

591
                    fmod($number, /** @scrutinizer ignore-type */ $divisor)
Loading history...
592
                );
593
                $number = floor($number / $divisor);
594 13
                $mask = substr_replace($mask, $blockValue, $offset, $size);
595
            }
596
            if ($number > 0) {
597 13
                $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 584. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
598
            }
599 13
            $result = $mask;
600 13
        }
601
602 13
        return $result;
603 6
    }
604 6
605 6
    private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true)
606 2
    {
607
        $sign = ($number < 0.0);
608 6
        $number = abs($number);
609 6
610
        if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
611 6
            $numbers = explode('.', $number);
612
            $masks = explode('.', $mask);
613
            if (count($masks) > 2) {
614 13
                $masks = self::mergeComplexNumberFormatMasks($numbers, $masks);
615
            }
616 13
            $result1 = self::complexNumberFormatMask($numbers[0], $masks[0], false);
617
            $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false));
618
619 68
            return (($sign) ? '-' : '') . $result1 . '.' . $result2;
620
        }
621 68
622 68
        $result = self::processComplexNumberFormatMask($number, $mask);
623 68
624
        return (($sign) ? '-' : '') . $result;
625
    }
626 68
627 68
    private static function formatStraightNumericValue($value, $format, array $matches, $useThousands, $number_regex)
628 32
    {
629
        $left = $matches[1];
630 32
        $dec = $matches[2];
631 32
        $right = $matches[3];
632 32
633
        // minimun width of formatted number (including dot)
634 32
        $minWidth = strlen($left) + strlen($dec) + strlen($right);
635
        if ($useThousands) {
636 40
            $value = number_format(
637
                $value,
638 7
                strlen($right),
639 37
                StringHelper::getDecimalSeparator(),
640 13
                StringHelper::getThousandsSeparator()
641 1
            );
642
            $value = preg_replace($number_regex, $value, $format);
643 13
        } else {
644
            if (preg_match('/[0#]E[+-]0/i', $format)) {
645 24
                //    Scientific format
646 24
                $value = sprintf('%5.2E', $value);
647 24
            } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) {
648
                if ($value == (int) $value && substr_count($format, '.') === 1) {
649
                    $value *= 10 ** strlen(explode('.', $format)[1]);
650
                }
651 68
                $value = self::complexNumberFormatMask($value, $format);
652
            } else {
653
                $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
654 75
                $value = sprintf($sprintf_pattern, $value);
655
                $value = preg_replace($number_regex, $value, $format);
656 75
            }
657
        }
658
659
        return $value;
660
    }
661 75
662
    private static function formatAsNumber($value, $format)
663
    {
664
        // The "_" in this string has already been stripped out,
665
        // so this test is never true. Furthermore, testing
666 75
        // on Excel shows this format uses Euro symbol, not "EUR".
667 75
        //if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
668 32
        //    return 'EUR ' . sprintf('%1.2f', $value);
669 32
        //}
670
671
        // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
672
        $format = str_replace(['"', '*'], '', $format);
673
674
        // Find out if we need thousands separator
675 75
        // This is indicated by a comma enclosed by a digit placeholder:
676 75
        //        #,#   or   0,0
677 75
        $useThousands = preg_match('/(#,#|0,0)/', $format);
678 2
        if ($useThousands) {
679
            $format = preg_replace('/0,0/', '00', $format);
680
            $format = preg_replace('/#,#/', '##', $format);
681 2
        }
682 2
683
        // Scale thousands, millions,...
684
        // This is indicated by a number of commas after a digit placeholder:
685 75
        //        #,   or    0.0,,
686 4
        $scale = 1; // same as no scale
687 4
        $matches = [];
688
        if (preg_match('/(#|0)(,+)/', $format, $matches)) {
689
            $scale = pow(1000, strlen($matches[2]));
690
691
            // strip the commas
692
            $format = preg_replace('/0,+/', '0', $format);
693 71
            $format = preg_replace('/#,+/', '#', $format);
694
        }
695 71
696
        if (preg_match('/#?.*\?\/\?/', $format, $m)) {
697 71
            if ($value != (int) $value) {
698
                self::formatAsFraction($value, $format);
699 71
            }
700 71
        } else {
701 71
            // Handle the number itself
702 71
703 68
            // scale number
704
            $value = $value / $scale;
705
            // Strip #
706
            $format = preg_replace('/\\#/', '0', $format);
707 75
            // Remove locale code [$-###]
708
            $format = preg_replace('/\[\$\-.*\]/', '', $format);
709 1
710 1
            $n = '/\\[[^\\]]+\\]/';
711 1
            $m = preg_replace($n, '', $format);
712
            $number_regex = '/(0+)(\\.?)(0*)/';
713
            if (preg_match($number_regex, $m, $matches)) {
714 1
                $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands, $number_regex);
715
            }
716
        }
717 75
718
        if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
719
            //  Currency or Accounting
720
            $currencyCode = $m[1];
721
            [$currencyCode] = explode('-', $currencyCode);
722
            if ($currencyCode == '') {
723
                $currencyCode = StringHelper::getCurrencyCode();
724
            }
725
            $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
726
        }
727
728
        return $value;
729 163
    }
730
731
    private static function splitFormatCompare($value, $cond, $val, $dfcond, $dfval)
732 163
    {
733 71
        if (!$cond) {
734
            $cond = $dfcond;
735
            $val = $dfval;
736
        }
737
        switch ($cond) {
738 123
            case '>':
739 29
                return $value > $val;
740
741
            case '<':
742
                return $value < $val;
743 105
744
            case '<=':
745
                return $value <= $val;
746 105
747
            case '<>':
748
                return $value != $val;
749
750
            case '=':
751
                return $value == $val;
752
        }
753
754
        return $value >= $val;
755 105
    }
756 105
757 89
    private static function splitFormat($sections, $value)
758
    {
759 89
        // Extract the relevant section depending on whether number is positive, negative, or zero?
760 20
        // Text not supported yet.
761 15
        // Here is how the sections apply to various values in Excel:
762 15
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
763 15
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
764 5
        //   3 sections:  [POSITIVE/TEXT] [NEGATIVE] [ZERO]
765 3
        //   4 sections:  [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
766 3
        $cnt = count($sections);
767 3
        $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/';
768 3
        $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/';
769 3
        $colors = ['', '', '', '', ''];
770 2
        $condops = ['', '', '', '', ''];
771 2
        $condvals = [0, 0, 0, 0, 0];
772 2
        for ($idx = 0; $idx < $cnt; ++$idx) {
773 2
            if (preg_match($color_regex, $sections[$idx], $matches)) {
774 2
                $colors[$idx] = $matches[0];
775 2
                $sections[$idx] = preg_replace($color_regex, '', $sections[$idx]);
776
            }
777
            if (preg_match($cond_regex, $sections[$idx], $matches)) {
778
                $condops[$idx] = $matches[1];
779
                $condvals[$idx] = $matches[2];
780
                $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]);
781
            }
782
        }
783
        $color = $colors[0];
784
        $format = $sections[0];
785 105
        $absval = $value;
786
        switch ($cnt) {
787
            case 2:
788 105
                $absval = abs($value);
789
                if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>=', 0)) {
790 105
                    $color = $colors[1];
791 105
                    $format = $sections[1];
792
                }
793
794
                break;
795 105
            case 3:
796
            case 4:
797 32
                $absval = abs($value);
798
                if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>', 0)) {
799 83
                    if (self::splitFormatCompare($value, $condops[1], $condvals[1], '<', 0)) {
800 3
                        $color = $colors[1];
801 80
                        $format = $sections[1];
802
                    } else {
803 5
                        $color = $colors[2];
804
                        $format = $sections[2];
805 75
                    }
806
                }
807
808
                break;
809
        }
810 105
811 4
        return [$color, $format, $absval];
812 4
    }
813
814
    /**
815 105
     * Convert a value in a pre-defined format to a PHP string.
816
     *
817
     * @param mixed $value Value to format
818
     * @param string $format Format code, see = self::FORMAT_*
819
     * @param array $callBack Callback function for additional formatting of string
820
     *
821
     * @return string Formatted string
822
     */
823
    public static function toFormattedString($value, $format, $callBack = null)
824
    {
825
        // For now we do not treat strings although section 4 of a format code affects strings
826
        if (!is_numeric($value)) {
827
            return $value;
828
        }
829
830
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
831
        // it seems to round numbers to a total of 10 digits.
832
        if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) {
833
            return $value;
834
        }
835
836
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
837
        $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format);
838
839
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
840
        $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format);
841
842
        [$colors, $format, $value] = self::splitFormat($sections, $value);
843
844
        // In Excel formats, "_" is used to add spacing,
845
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
846
        $format = preg_replace('/_./', ' ', $format);
847
848
        // Let's begin inspecting the format and converting the value to a formatted string
849
850
        //  Check for date/time characters (not inside quotes)
851
        if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) {
852
            // datetime format
853
            self::formatAsDate($value, $format);
854
        } else {
855
            if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"') {
856
                $value = substr($format, 1, -1);
857
            } elseif (preg_match('/%$/', $format)) {
858
                // % number format
859
                self::formatAsPercentage($value, $format);
860
            } else {
861
                $value = self::formatAsNumber($value, $format);
862
            }
863
        }
864
865
        // Additional formatting provided by callback function
866
        if ($callBack !== null) {
867
            [$writerInstance, $function] = $callBack;
868
            $value = $writerInstance->$function($value, $colors);
869
        }
870
871
        return $value;
872
    }
873
}
874