Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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