Passed
Pull Request — master (#3349)
by Mark
12:23 queued 01:16
created

Formatter::splitFormatComparison()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 30
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

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