Skip to content

Commit f468e78

Browse files
committed
Improved handling for ? placeholder in Number Format Masks
1 parent 4cefd7a commit f468e78

File tree

3 files changed

+117
-14
lines changed

3 files changed

+117
-14
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1717

1818
### Changed
1919

20-
- Improved handling for @ in Number Format Masks [PR #3344](https://github.com/PHPOffice/PhpSpreadsheet/pull/3344)
20+
- Improved handling for @ placeholder in Number Format Masks [PR #3344](https://github.com/PHPOffice/PhpSpreadsheet/pull/3344)
21+
- Improved handling for ? placeholder in Number Format Masks [PR #3394](https://github.com/PHPOffice/PhpSpreadsheet/pull/3394)
2122
- Improved support for locale settings and currency codes when matching formatted strings to numerics in the Calculation Engine [PR #3373](https://github.com/PHPOffice/PhpSpreadsheet/pull/3373) and [PR #3374](https://github.com/PHPOffice/PhpSpreadsheet/pull/3374)
2223
- Improved support for locale settings and matching in the Advanced Value Binder [PR #3376](https://github.com/PHPOffice/PhpSpreadsheet/pull/3376)
2324

src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ private static function formatStraightNumericValue($value, string $format, array
186186
}
187187

188188
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
189+
189190
/** @var float */
190191
$valueFloat = $value;
191192
$value = sprintf($sprintf_pattern, round($valueFloat, strlen($right)));
@@ -201,42 +202,46 @@ public static function format($value, string $format): string
201202
// The "_" in this string has already been stripped out,
202203
// so this test is never true. Furthermore, testing
203204
// on Excel shows this format uses Euro symbol, not "EUR".
204-
//if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) {
205-
// return 'EUR ' . sprintf('%1.2f', $value);
206-
//}
205+
// if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) {
206+
// return 'EUR ' . sprintf('%1.2f', $value);
207+
// }
208+
209+
$baseFormat = $format;
207210

208211
// Find out if we need thousands separator
209212
// This is indicated by a comma enclosed by a digit placeholder:
210213
// #,# or 0,0
211-
$useThousands = (bool) preg_match('/(#,#|0,0)/', $format);
214+
$useThousands = (bool) preg_match('/(#,#|0,0|\?,\?)/', $format);
212215
if ($useThousands) {
213216
$format = self::pregReplace('/0,0/', '00', $format);
214217
$format = self::pregReplace('/#,#/', '##', $format);
218+
$format = self::pregReplace('/\?,\?/', '??', $format);
215219
}
216220

217221
// Scale thousands, millions,...
218222
// This is indicated by a number of commas after a digit placeholder:
219223
// #, or 0.0,,
220224
$scale = 1; // same as no scale
221225
$matches = [];
222-
if (preg_match('/(#|0)(,+)/', $format, $matches)) {
226+
if (preg_match('/(#|0|\?)(,+)/', $format, $matches)) {
223227
$scale = 1000 ** strlen($matches[2]);
224228

225229
// strip the commas
226230
$format = self::pregReplace('/0,+/', '0', $format);
227231
$format = self::pregReplace('/#,+/', '#', $format);
232+
$format = self::pregReplace('/\?,+/', '?', $format);
228233
}
229234

230235
if (preg_match('/#?.*\?\/(\?+|\d+)/', $format)) {
231236
$value = FractionFormatter::format($value, $format);
232237
} else {
233238
// Handle the number itself
234-
235239
// scale number
236240
$value = $value / $scale;
241+
$paddingPlaceholder = (strpos($format, '?') !== false);
237242

238243
// Strip #
239-
$format = self::pregReplace('/\\#(?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format);
244+
$format = self::pregReplace('/[\\#\?](?=(?:[^"]*"[^"]*")*[^"]*\Z)/', '0', $format);
240245
// Remove locale code [$-###]
241246
$format = self::pregReplace('/\[\$\-.*\]/', '', $format);
242247

@@ -245,10 +250,12 @@ public static function format($value, string $format): string
245250

246251
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
247252
$format = self::makeString(str_replace(['"', '*'], '', $format));
248-
249253
if (preg_match(self::NUMBER_REGEX, $m, $matches)) {
250254
// There are placeholders for digits, so inject digits from the value into the mask
251255
$value = self::formatStraightNumericValue($value, $format, $matches, $useThousands);
256+
if ($paddingPlaceholder === true) {
257+
$value = self::padValue($value, $baseFormat);
258+
}
252259
} elseif ($format !== NumberFormat::FORMAT_GENERAL) {
253260
// Yes, I know that this is basically just a hack;
254261
// if there's no placeholders for digits, just return the format mask "as is"
@@ -266,6 +273,13 @@ public static function format($value, string $format): string
266273
$value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value);
267274
}
268275

276+
if (
277+
(strpos((string) $value, '0.') !== false) &&
278+
((strpos($baseFormat, '#.') !== false) || (strpos($baseFormat, '?.') !== false))
279+
) {
280+
$value = preg_replace('/(\b)0\.|([^\d])0\./', '${2}.', (string) $value);
281+
}
282+
269283
return (string) $value;
270284
}
271285

@@ -281,4 +295,20 @@ private static function pregReplace(string $pattern, string $replacement, string
281295
{
282296
return self::makeString(preg_replace($pattern, $replacement, $subject) ?? '');
283297
}
298+
299+
public static function padValue(string $value, string $baseFormat): string
300+
{
301+
/** @phpstan-ignore-next-line */
302+
[$preDecimal, $postDecimal] = preg_split('/\.(?=(?:[^"]*"[^"]*")*[^"]*\Z)/miu', $baseFormat . '.?');
303+
304+
$length = strlen($value);
305+
if (strpos($postDecimal, '?') !== false) {
306+
$value = str_pad(rtrim($value, '0. '), $length, ' ', STR_PAD_RIGHT);
307+
}
308+
if (strpos($preDecimal, '?') !== false) {
309+
$value = str_pad(ltrim($value, '0, '), $length, ' ', STR_PAD_LEFT);
310+
}
311+
312+
return $value;
313+
}
284314
}

tests/data/Style/NumberFormat.php

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@
364364
'_-€* #,##0.00_-;"-€"* #,##0.00_-;_-€* -??_-;_-@_-',
365365
],
366366
[
367-
' € - ',
367+
' € - ',
368368
0,
369369
'_-€* #,##0.00_-;"-€"* #,##0.00_-;_-€* -??_-;_-@_-',
370370
],
@@ -1326,7 +1326,7 @@
13261326
NumberFormat::FORMAT_CURRENCY_EUR_INTEGER,
13271327
],
13281328
[
1329-
' $ - ',
1329+
' $ - ',
13301330
'0',
13311331
NumberFormat::FORMAT_ACCOUNTING_USD,
13321332
],
@@ -1356,7 +1356,7 @@
13561356
NumberFormat::FORMAT_ACCOUNTING_USD,
13571357
],
13581358
[
1359-
' $ - ',
1359+
' $ - ',
13601360
'-0',
13611361
NumberFormat::FORMAT_ACCOUNTING_USD,
13621362
],
@@ -1386,7 +1386,7 @@
13861386
NumberFormat::FORMAT_ACCOUNTING_USD,
13871387
],
13881388
[
1389-
' € - ',
1389+
' € - ',
13901390
'0',
13911391
NumberFormat::FORMAT_ACCOUNTING_EUR,
13921392
],
@@ -1416,7 +1416,7 @@
14161416
NumberFormat::FORMAT_ACCOUNTING_EUR,
14171417
],
14181418
[
1419-
' € - ',
1419+
' € - ',
14201420
'-0',
14211421
NumberFormat::FORMAT_ACCOUNTING_EUR,
14221422
],
@@ -1550,4 +1550,76 @@
15501550
1025132.36,
15511551
'#,###,.##',
15521552
],
1553+
[
1554+
'.05',
1555+
50,
1556+
'#.00,',
1557+
],
1558+
[
1559+
'50.05',
1560+
50050,
1561+
'#.00,',
1562+
],
1563+
[
1564+
'555.50',
1565+
555500,
1566+
'#.00,',
1567+
],
1568+
[
1569+
'.56',
1570+
555500,
1571+
'#.00,,',
1572+
],
1573+
// decimal placement
1574+
[
1575+
' 44.398',
1576+
44.398,
1577+
'???.???',
1578+
],
1579+
[
1580+
'102.65 ',
1581+
102.65,
1582+
'???.???',
1583+
],
1584+
[
1585+
' 2.8 ',
1586+
2.8,
1587+
'???.???',
1588+
],
1589+
[
1590+
' 3',
1591+
2.8,
1592+
'???',
1593+
],
1594+
[
1595+
'12,345',
1596+
12345,
1597+
'?,???',
1598+
],
1599+
[
1600+
'123',
1601+
123,
1602+
'?,???',
1603+
],
1604+
[
1605+
'$.50',
1606+
0.5,
1607+
'$?.00',
1608+
],
1609+
[
1610+
'Part Cost $.50',
1611+
0.5,
1612+
'Part Cost $?.00',
1613+
],
1614+
// Empty Section
1615+
[
1616+
'',
1617+
-12345.6789,
1618+
'#,##0.00;',
1619+
],
1620+
[
1621+
'',
1622+
-12345.6789,
1623+
'#,##0.00;;"---"',
1624+
],
15531625
];

0 commit comments

Comments
 (0)