Failed Conditions
Push — master ( bf4629...7712d5 )
by Adrien
27:59 queued 18:08
created

Formatter::toFormattedString()   C

Complexity

Conditions 16
Paths 13

Size

Total Lines 72
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 16

Importance

Changes 0
Metric Value
eloc 33
c 0
b 0
f 0
dl 0
loc 72
ccs 33
cts 33
cp 1
rs 5.5666
cc 16
nc 13
nop 3
crap 16

How to fix   Long Method    Complexity   

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\NumberFormat;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Reader\Xls\Color\BIFF8;
7
use PhpOffice\PhpSpreadsheet\RichText\RichText;
8
use PhpOffice\PhpSpreadsheet\Style\Color;
9
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
10
11
class Formatter
12
{
13
    /**
14
     * Matches any @ symbol that isn't enclosed in quotes.
15
     */
16
    private const SYMBOL_AT = '/@(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu';
17
18
    /**
19
     * Matches any ; symbol that isn't enclosed in quotes, for a "section" split.
20
     */
21
    private const SECTION_SPLIT = '/;(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu';
22
23 178
    private static function splitFormatComparison(
24
        mixed $value,
25
        ?string $condition,
26
        mixed $comparisonValue,
27
        string $defaultCondition,
28
        mixed $defaultComparisonValue
29
    ): bool {
30 178
        if (!$condition) {
31 169
            $condition = $defaultCondition;
32 169
            $comparisonValue = $defaultComparisonValue;
33
        }
34
35 178
        return match ($condition) {
36 178
            '>' => $value > $comparisonValue,
37 178
            '<' => $value < $comparisonValue,
38 178
            '<=' => $value <= $comparisonValue,
39 178
            '<>' => $value != $comparisonValue,
40 178
            '=' => $value == $comparisonValue,
41 178
            default => $value >= $comparisonValue,
42 178
        };
43
    }
44
45 920
    private static function splitFormatForSectionSelection(array $sections, mixed $value): array
46
    {
47
        // Extract the relevant section depending on whether number is positive, negative, or zero?
48
        // Text not supported yet.
49
        // Here is how the sections apply to various values in Excel:
50
        //   1 section:   [POSITIVE/NEGATIVE/ZERO/TEXT]
51
        //   2 sections:  [POSITIVE/ZERO/TEXT] [NEGATIVE]
52
        //   3 sections:  [POSITIVE/TEXT] [NEGATIVE] [ZERO]
53
        //   4 sections:  [POSITIVE] [NEGATIVE] [ZERO] [TEXT]
54 920
        $sectionCount = count($sections);
55
        // Colour could be a named colour, or a numeric index entry in the colour-palette
56 920
        $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . '|color\\s*(\\d+))\\]/mui';
57 920
        $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/';
58 920
        $colors = ['', '', '', '', ''];
59 920
        $conditionOperations = ['', '', '', '', ''];
60 920
        $conditionComparisonValues = [0, 0, 0, 0, 0];
61 920
        for ($idx = 0; $idx < $sectionCount; ++$idx) {
62 920
            if (preg_match($color_regex, $sections[$idx], $matches)) {
63 49
                if (isset($matches[2])) {
64 14
                    $colors[$idx] = '#' . BIFF8::lookup((int) $matches[2] + 7)['rgb'];
65
                } else {
66 35
                    $colors[$idx] = $matches[0];
67
                }
68 49
                $sections[$idx] = (string) preg_replace($color_regex, '', $sections[$idx]);
69
            }
70 920
            if (preg_match($cond_regex, $sections[$idx], $matches)) {
71 9
                $conditionOperations[$idx] = $matches[1];
72 9
                $conditionComparisonValues[$idx] = $matches[2];
73 9
                $sections[$idx] = (string) preg_replace($cond_regex, '', $sections[$idx]);
74
            }
75
        }
76 920
        $color = $colors[0];
77 920
        $format = $sections[0];
78 920
        $absval = $value;
79
        switch ($sectionCount) {
80 920
            case 2:
81 96
                $absval = abs($value);
82 96
                if (!self::splitFormatComparison($value, $conditionOperations[0], $conditionComparisonValues[0], '>=', 0)) {
83 46
                    $color = $colors[1];
84 46
                    $format = $sections[1];
85
                }
86
87 96
                break;
88 830
            case 3:
89 816
            case 4:
90 82
                $absval = abs($value);
91 82
                if (!self::splitFormatComparison($value, $conditionOperations[0], $conditionComparisonValues[0], '>', 0)) {
92 47
                    if (self::splitFormatComparison($value, $conditionOperations[1], $conditionComparisonValues[1], '<', 0)) {
93 34
                        $color = $colors[1];
94 34
                        $format = $sections[1];
95
                    } else {
96 15
                        $color = $colors[2];
97 15
                        $format = $sections[2];
98
                    }
99
                }
100
101 82
                break;
102
        }
103
104 920
        return [$color, $format, $absval];
105
    }
106
107
    /**
108
     * Convert a value in a pre-defined format to a PHP string.
109
     *
110
     * @param null|bool|float|int|RichText|string $value Value to format
111
     * @param string $format Format code: see = self::FORMAT_* for predefined values;
112
     *                          or can be any valid MS Excel custom format string
113
     * @param array $callBack Callback function for additional formatting of string
114
     *
115
     * @return string Formatted string
116
     */
117 1115
    public static function toFormattedString($value, $format, $callBack = null)
118
    {
119 1115
        if (is_bool($value)) {
120 11
            return $value ? Calculation::getTRUE() : Calculation::getFALSE();
121
        }
122
        // For now we do not treat strings in sections, although section 4 of a format code affects strings
123
        // Process a single block format code containing @ for text substitution
124 1115
        if (preg_match(self::SECTION_SPLIT, $format) === 0 && preg_match(self::SYMBOL_AT, $format) === 1) {
125 12
            return str_replace('"', '', preg_replace(self::SYMBOL_AT, (string) $value, $format) ?? '');
126
        }
127
128
        // If we have a text value, return it "as is"
129 1105
        if (!is_numeric($value)) {
130 187
            return (string) $value;
131
        }
132
133
        // For 'General' format code, we just pass the value although this is not entirely the way Excel does it,
134
        // it seems to round numbers to a total of 10 digits.
135 1016
        if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) {
136 121
            return (string) $value;
137
        }
138
139
        // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc
140 920
        $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format);
141
142 920
        $format = (string) preg_replace_callback(
143 920
            '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u',
144 920
            function (array $matches): string {
145 124
                return str_replace('.', chr(0x00), $matches[0]);
146 920
            },
147 920
            $format
148 920
        );
149
150
        // Convert any other escaped characters to quoted strings, e.g. (\T to "T")
151 920
        $format = (string) preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format);
152
153
        // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal)
154 920
        $sections = preg_split(self::SECTION_SPLIT, $format) ?: [];
155
156 920
        [$colors, $format, $value] = self::splitFormatForSectionSelection($sections, $value);
157
158
        // In Excel formats, "_" is used to add spacing,
159
        //    The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space
160 920
        $format = (string) preg_replace('/_.?/ui', ' ', $format);
161
162
        // Let's begin inspecting the format and converting the value to a formatted string
163
        if (
164
            //  Check for date/time characters (not inside quotes)
165 920
            (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format))
166
            // A date/time with a decimal time shouldn't have a digit placeholder before the decimal point
167 920
            && (preg_match('/[0\?#]\.(?![^\[]*\])/miu', $format) === 0)
168
        ) {
169
            // datetime format
170 146
            $value = DateFormatter::format($value, $format);
171
        } else {
172 797
            if (str_starts_with($format, '"') && str_ends_with($format, '"') && substr_count($format, '"') === 2) {
173 14
                $value = substr($format, 1, -1);
174 783
            } elseif (preg_match('/[0#, ]%/', $format)) {
175
                // % number format - avoid weird '-0' problem
176 130
                $value = PercentageFormatter::format(0 + (float) $value, $format);
177
            } else {
178 654
                $value = NumberFormatter::format($value, $format);
179
            }
180
        }
181
182
        // Additional formatting provided by callback function
183 920
        if ($callBack !== null) {
184 385
            [$writerInstance, $function] = $callBack;
185 385
            $value = $writerInstance->$function($value, $colors);
186
        }
187
188 920
        return str_replace(chr(0x00), '.', $value);
189
    }
190
}
191