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\Shared\StringHelper;
6
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
7
 
8
class NumberFormatter extends BaseFormatter
9
{
10
    private const NUMBER_REGEX = '/(0+)(\.?)(0*)/';
11
 
12
    private static function mergeComplexNumberFormatMasks(array $numbers, array $masks): array
13
    {
14
        $decimalCount = strlen($numbers[1]);
15
        $postDecimalMasks = [];
16
 
17
        do {
18
            $tempMask = array_pop($masks);
19
            if ($tempMask !== null) {
20
                $postDecimalMasks[] = $tempMask;
21
                $decimalCount -= strlen($tempMask);
22
            }
23
        } while ($tempMask !== null && $decimalCount > 0);
24
 
25
        return [
26
            implode('.', $masks),
27
            implode('.', array_reverse($postDecimalMasks)),
28
        ];
29
    }
30
 
31
    private static function processComplexNumberFormatMask(mixed $number, string $mask): string
32
    {
33
        /** @var string $result */
34
        $result = $number;
35
        $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
36
 
37
        if ($maskingBlockCount > 1) {
38
            $maskingBlocks = array_reverse($maskingBlocks[0]);
39
 
40
            $offset = 0;
41
            foreach ($maskingBlocks as $block) {
42
                $size = strlen($block[0]);
43
                $divisor = 10 ** $size;
44
                $offset = $block[1];
45
 
46
                /** @var float $numberFloat */
47
                $numberFloat = $number;
48
                $blockValue = sprintf("%0{$size}d", fmod($numberFloat, $divisor));
49
                $number = floor($numberFloat / $divisor);
50
                $mask = substr_replace($mask, $blockValue, $offset, $size);
51
            }
52
            /** @var string $numberString */
53
            $numberString = $number;
54
            if ($number > 0) {
55
                $mask = substr_replace($mask, $numberString, $offset, 0);
56
            }
57
            $result = $mask;
58
        }
59
 
60
        return self::makeString($result);
61
    }
62
 
63
    private static function complexNumberFormatMask(mixed $number, string $mask, bool $splitOnPoint = true): string
64
    {
65
        /** @var float $numberFloat */
66
        $numberFloat = $number;
67
        if ($splitOnPoint) {
68
            $masks = explode('.', $mask);
69
            if (count($masks) <= 2) {
70
                $decmask = $masks[1] ?? '';
71
                $decpos = substr_count($decmask, '0');
72
                $numberFloat = round($numberFloat, $decpos);
73
            }
74
        }
75
        $sign = ($numberFloat < 0.0) ? '-' : '';
76
        $number = self::f2s(abs($numberFloat));
77
 
78
        if ($splitOnPoint && str_contains($mask, '.') && str_contains($number, '.')) {
79
            $numbers = explode('.', $number);
80
            $masks = explode('.', $mask);
81
            if (count($masks) > 2) {
82
                $masks = self::mergeComplexNumberFormatMasks($numbers, $masks);
83
            }
84
            $integerPart = self::complexNumberFormatMask($numbers[0], $masks[0], false);
85
            $numlen = strlen($numbers[1]);
86
            $msklen = strlen($masks[1]);
87
            if ($numlen < $msklen) {
88
                $numbers[1] .= str_repeat('0', $msklen - $numlen);
89
            }
90
            $decimalPart = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false));
91
            $decimalPart = substr($decimalPart, 0, $msklen);
92
 
93
            return "{$sign}{$integerPart}.{$decimalPart}";
94
        }
95
 
96
        if (strlen($number) < strlen($mask)) {
97
            $number = str_repeat('0', strlen($mask) - strlen($number)) . $number;
98
        }
99
        $result = self::processComplexNumberFormatMask($number, $mask);
100
 
101
        return "{$sign}{$result}";
102
    }
103
 
104
    public static function f2s(float $f): string
105
    {
106
        return self::floatStringConvertScientific((string) $f);
107
    }
108
 
109
    public static function floatStringConvertScientific(string $s): string
110
    {
111
        // convert only normalized form of scientific notation:
112
        //  optional sign, single digit 1-9,
113
        //    decimal point and digits (allowed to be omitted),
114
        //    E (e permitted), optional sign, one or more digits
115
        if (preg_match('/^([+-])?([1-9])([.]([0-9]+))?[eE]([+-]?[0-9]+)$/', $s, $matches) === 1) {
116
            $exponent = (int) $matches[5];
117
            $sign = ($matches[1] === '-') ? '-' : '';
118
            if ($exponent >= 0) {
119
                $exponentPlus1 = $exponent + 1;
120
                $out = $matches[2] . $matches[4];
121
                $len = strlen($out);
122
                if ($len < $exponentPlus1) {
123
                    $out .= str_repeat('0', $exponentPlus1 - $len);
124
                }
125
                $out = substr($out, 0, $exponentPlus1) . ((strlen($out) === $exponentPlus1) ? '' : ('.' . substr($out, $exponentPlus1)));
126
                $s = "$sign$out";
127
            } else {
128
                $s = $sign . '0.' . str_repeat('0', -$exponent - 1) . $matches[2] . $matches[4];
129
            }
130
        }
131
 
132
        return $s;
133
    }
134
 
135
    private static function formatStraightNumericValue(mixed $value, string $format, array $matches, bool $useThousands): string
136
    {
137
        /** @var float $valueFloat */
138
        $valueFloat = $value;
139
        $left = $matches[1];
140
        $dec = $matches[2];
141
        $right = $matches[3];
142
 
143
        // minimun width of formatted number (including dot)
144
        $minWidth = strlen($left) + strlen($dec) + strlen($right);
145
        if ($useThousands) {
146
            $value = number_format(
147
                $valueFloat,
148
                strlen($right),
149
                StringHelper::getDecimalSeparator(),
150
                StringHelper::getThousandsSeparator()
151
            );
152
 
153
            return self::pregReplace(self::NUMBER_REGEX, $value, $format);
154
        }
155
 
156
        if (preg_match('/[0#]E[+-]0/i', $format)) {
157
            //    Scientific format
158
            $decimals = strlen($right);
159
            $size = $decimals + 3;
160
 
161
            return sprintf("%{$size}.{$decimals}E", $valueFloat);
162
        } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) {
163
            if ($valueFloat == floor($valueFloat) && substr_count($format, '.') === 1) {
164
                $value *= 10 ** strlen(explode('.', $format)[1]);
165
            }
166
 
167
            $result = self::complexNumberFormatMask($value, $format);
168
            if (str_contains($result, 'E')) {
169
                // This is a hack and doesn't match Excel.
170
                // It will, at least, be an accurate representation,
171
                //  even if formatted incorrectly.
172
                // This is needed for absolute values >=1E18.
173
                $result = self::f2s($valueFloat);
174
            }
175
 
176
            return $result;
177
        }
178
 
179
        $sprintf_pattern = "%0$minWidth." . strlen($right) . 'F';
180
 
181
        /** @var float $valueFloat */
182
        $valueFloat = $value;
183
        $value = self::adjustSeparators(sprintf($sprintf_pattern, round($valueFloat, strlen($right))));
184
 
185
        return self::pregReplace(self::NUMBER_REGEX, $value, $format);
186
    }
187
 
188
    /** @param float|int|numeric-string $value value to be formatted */
189
    public static function format(mixed $value, string $format): string
190
    {
191
        // The "_" in this string has already been stripped out,
192
        // so this test is never true. Furthermore, testing
193
        // on Excel shows this format uses Euro symbol, not "EUR".
194
        // if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) {
195
        //     return 'EUR ' . sprintf('%1.2f', $value);
196
        // }
197
 
198
        $baseFormat = $format;
199
 
200
        $useThousands = self::areThousandsRequired($format);
201
        $scale = self::scaleThousandsMillions($format);
202
 
203
        if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $format)) {
204
            // It's a dirty hack; but replace # and 0 digit placeholders with ?
205
            $format = (string) preg_replace('/[#0]+\//', '?/', $format);
206
            $format = (string) preg_replace('/\/[#0]+/', '/?', $format);
207
            $value = FractionFormatter::format($value, $format);
208
        } else {
209
            // Handle the number itself
210
            // scale number
211
            $value = $value / $scale;
212
            $paddingPlaceholder = (str_contains($format, '?'));
213
 
214
            // Replace # or ? with 0
215
            $format = self::pregReplace('/[\#\?](?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format);
216
            // Remove locale code [$-###] for an LCID
217
            $format = self::pregReplace('/\[\$\-.*\]/', '', $format);
218
 
219
            $n = '/\[[^\]]+\]/';
220
            $m = self::pregReplace($n, '', $format);
221
 
222
            // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
223
            $format = self::makeString(str_replace(['"', '*'], '', $format));
224
            if (preg_match(self::NUMBER_REGEX, $m, $matches)) {
225
                // There are placeholders for digits, so inject digits from the value into the mask
226
                $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands);
227
                if ($paddingPlaceholder === true) {
228
                    $value = self::padValue($value, $baseFormat);
229
                }
230
            } elseif ($format !== NumberFormat::FORMAT_GENERAL) {
231
                // Yes, I know that this is basically just a hack;
232
                //      if there's no placeholders for digits, just return the format mask "as is"
233
                $value = self::makeString(str_replace('?', '', $format));
234
            }
235
        }
236
 
237
        if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
238
            //  Currency or Accounting
239
            $value = preg_replace('/-0+(( |\xc2\xa0))?\[/', '- [', (string) $value) ?? $value;
240
            $currencyCode = $m[1];
241
            [$currencyCode] = explode('-', $currencyCode);
242
            if ($currencyCode == '') {
243
                $currencyCode = StringHelper::getCurrencyCode();
244
            }
245
            $value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value);
246
        }
247
 
248
        if (
249
            (str_contains((string) $value, '0.'))
250
            && ((str_contains($baseFormat, '#.')) || (str_contains($baseFormat, '?.')))
251
        ) {
252
            $value = preg_replace('/(\b)0\.|([^\d])0\./', '${2}.', (string) $value);
253
        }
254
 
255
        return (string) $value;
256
    }
257
 
258
    private static function makeString(array|string $value): string
259
    {
260
        return is_array($value) ? '' : "$value";
261
    }
262
 
263
    private static function pregReplace(string $pattern, string $replacement, string $subject): string
264
    {
265
        return self::makeString(preg_replace($pattern, $replacement, $subject) ?? '');
266
    }
267
 
268
    public static function padValue(string $value, string $baseFormat): string
269
    {
270
        $preDecimal = $postDecimal = '';
271
        $pregArray = preg_split('/\.(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu', $baseFormat . '.?');
272
        if (is_array($pregArray)) {
273
            $preDecimal = $pregArray[0] ?? '';
274
            $postDecimal = $pregArray[1] ?? '';
275
        }
276
 
277
        $length = strlen($value);
278
        if (str_contains($postDecimal, '?')) {
279
            $value = str_pad(rtrim($value, '0. '), $length, ' ', STR_PAD_RIGHT);
280
        }
281
        if (str_contains($preDecimal, '?')) {
282
            $value = str_pad(ltrim($value, '0, '), $length, ' ', STR_PAD_LEFT);
283
        }
284
 
285
        return $value;
286
    }
287
 
288
    /**
289
     * Find out if we need thousands separator
290
     * This is indicated by a comma enclosed by a digit placeholders: #, 0 or ?
291
     */
292
    public static function areThousandsRequired(string &$format): bool
293
    {
294
        $useThousands = (bool) preg_match('/([#\?0]),([#\?0])/', $format);
295
        if ($useThousands) {
296
            $format = self::pregReplace('/([#\?0]),([#\?0])/', '${1}${2}', $format);
297
        }
298
 
299
        return $useThousands;
300
    }
301
 
302
    /**
303
     * Scale thousands, millions,...
304
     * This is indicated by a number of commas after a digit placeholder: #, or 0.0,, or ?,.
305
     */
306
    public static function scaleThousandsMillions(string &$format): int
307
    {
308
        $scale = 1; // same as no scale
309
        if (preg_match('/(#|0|\?)(,+)/', $format, $matches)) {
310
            $scale = 1000 ** strlen($matches[2]);
311
            // strip the commas
312
            $format = self::pregReplace('/([#\?0]),+/', '${1}', $format);
313
        }
314
 
315
        return $scale;
316
    }
317
}