PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Style / NumberFormat / NumberFormatter.php

NumberFormatter.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php

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