Number formatting minor refactoring (#1081)
* Merge branch 'master' of C:\Projects\PHPOffice\PHPSpreadsheet\develop with conflicts. * Handle literal (non-decimal) dots in complex number format masks * Minor refactoring nd reformatting * Appease CS * Update changelog
This commit is contained in:
parent
ab1c6e53b6
commit
20f36ccd79
|
@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fix number format masks containing literal (non-decimal point) dots [Issue #1079](https://github.com/PHPOffice/PhpSpreadsheet/issues/1079)
|
||||
- Fix number format masks containing named colours that were being misinterpreted as date formats; and add support for masks that fully replace the value with a full text string [Issue #1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009)
|
||||
- Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [Issue #1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046)
|
||||
- COUPNUM should not return zero when settlement is in the last period - [Issue #1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [PR #1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021)
|
||||
|
|
|
@ -551,24 +551,32 @@ class NumberFormat extends Supervisor
|
|||
}
|
||||
}
|
||||
|
||||
private static function complexNumberFormatMask($number, $mask)
|
||||
private static function mergeComplexNumberFormatMasks($numbers, $masks)
|
||||
{
|
||||
$sign = ($number < 0.0);
|
||||
$number = abs($number);
|
||||
if (strpos($mask, '.') !== false) {
|
||||
$numbers = explode('.', $number . '.0');
|
||||
$masks = explode('.', $mask . '.0');
|
||||
$result1 = self::complexNumberFormatMask($numbers[0], $masks[0]);
|
||||
$result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1])));
|
||||
$decimalCount = strlen($numbers[1]);
|
||||
$postDecimalMasks = [];
|
||||
|
||||
return (($sign) ? '-' : '') . $result1 . '.' . $result2;
|
||||
do {
|
||||
$tempMask = array_pop($masks);
|
||||
$postDecimalMasks[] = $tempMask;
|
||||
$decimalCount -= strlen($tempMask);
|
||||
} while ($decimalCount > 0);
|
||||
|
||||
return [
|
||||
implode('.', $masks),
|
||||
implode('.', array_reverse($postDecimalMasks)),
|
||||
];
|
||||
}
|
||||
|
||||
$r = preg_match_all('/0+/', $mask, $result, PREG_OFFSET_CAPTURE);
|
||||
if ($r > 1) {
|
||||
$result = array_reverse($result[0]);
|
||||
private static function processComplexNumberFormatMask($number, $mask)
|
||||
{
|
||||
$result = $number;
|
||||
$maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($result as $block) {
|
||||
if ($maskingBlockCount > 1) {
|
||||
$maskingBlocks = array_reverse($maskingBlocks[0]);
|
||||
|
||||
foreach ($maskingBlocks as $block) {
|
||||
$divisor = 1 . $block[0];
|
||||
$size = strlen($block[0]);
|
||||
$offset = $block[1];
|
||||
|
@ -584,13 +592,125 @@ class NumberFormat extends Supervisor
|
|||
$mask = substr_replace($mask, $number, $offset, 0);
|
||||
}
|
||||
$result = $mask;
|
||||
} else {
|
||||
$result = $number;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true)
|
||||
{
|
||||
$sign = ($number < 0.0);
|
||||
$number = abs($number);
|
||||
if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
|
||||
$numbers = explode('.', $number);
|
||||
$masks = explode('.', $mask);
|
||||
if (count($masks) > 2) {
|
||||
$masks = self::mergeComplexNumberFormatMasks($numbers, $masks);
|
||||
}
|
||||
$result1 = self::complexNumberFormatMask($numbers[0], $masks[0], false);
|
||||
$result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false));
|
||||
|
||||
return (($sign) ? '-' : '') . $result1 . '.' . $result2;
|
||||
}
|
||||
|
||||
$result = self::processComplexNumberFormatMask($number, $mask);
|
||||
|
||||
return (($sign) ? '-' : '') . $result;
|
||||
}
|
||||
|
||||
private static function formatAsNumber($value, $format)
|
||||
{
|
||||
if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
|
||||
return 'EUR ' . sprintf('%1.2f', $value);
|
||||
}
|
||||
|
||||
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
|
||||
$format = str_replace(['"', '*'], '', $format);
|
||||
|
||||
// Find out if we need thousands separator
|
||||
// This is indicated by a comma enclosed by a digit placeholder:
|
||||
// #,# or 0,0
|
||||
$useThousands = preg_match('/(#,#|0,0)/', $format);
|
||||
if ($useThousands) {
|
||||
$format = preg_replace('/0,0/', '00', $format);
|
||||
$format = preg_replace('/#,#/', '##', $format);
|
||||
}
|
||||
|
||||
// Scale thousands, millions,...
|
||||
// This is indicated by a number of commas after a digit placeholder:
|
||||
// #, or 0.0,,
|
||||
$scale = 1; // same as no scale
|
||||
$matches = [];
|
||||
if (preg_match('/(#|0)(,+)/', $format, $matches)) {
|
||||
$scale = pow(1000, strlen($matches[2]));
|
||||
|
||||
// strip the commas
|
||||
$format = preg_replace('/0,+/', '0', $format);
|
||||
$format = preg_replace('/#,+/', '#', $format);
|
||||
}
|
||||
|
||||
if (preg_match('/#?.*\?\/\?/', $format, $m)) {
|
||||
if ($value != (int) $value) {
|
||||
self::formatAsFraction($value, $format);
|
||||
}
|
||||
} else {
|
||||
// Handle the number itself
|
||||
|
||||
// scale number
|
||||
$value = $value / $scale;
|
||||
|
||||
// Strip #
|
||||
$format = preg_replace('/\\#/', '0', $format);
|
||||
|
||||
// Remove locale code [$-###]
|
||||
$format = preg_replace('/\[\$\-.*\]/', '', $format);
|
||||
|
||||
$n = '/\\[[^\\]]+\\]/';
|
||||
$m = preg_replace($n, '', $format);
|
||||
$number_regex = '/(0+)(\\.?)(0*)/';
|
||||
if (preg_match($number_regex, $m, $matches)) {
|
||||
$left = $matches[1];
|
||||
$dec = $matches[2];
|
||||
$right = $matches[3];
|
||||
|
||||
// minimun width of formatted number (including dot)
|
||||
$minWidth = strlen($left) + strlen($dec) + strlen($right);
|
||||
if ($useThousands) {
|
||||
$value = number_format(
|
||||
$value,
|
||||
strlen($right),
|
||||
StringHelper::getDecimalSeparator(),
|
||||
StringHelper::getThousandsSeparator()
|
||||
);
|
||||
$value = preg_replace($number_regex, $value, $format);
|
||||
} else {
|
||||
if (preg_match('/[0#]E[+-]0/i', $format)) {
|
||||
// Scientific format
|
||||
$value = sprintf('%5.2E', $value);
|
||||
} elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) {
|
||||
$value = self::complexNumberFormatMask($value, $format);
|
||||
} else {
|
||||
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
|
||||
$value = sprintf($sprintf_pattern, $value);
|
||||
$value = preg_replace($number_regex, $value, $format);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
|
||||
// Currency or Accounting
|
||||
$currencyCode = $m[1];
|
||||
list($currencyCode) = explode('-', $currencyCode);
|
||||
if ($currencyCode == '') {
|
||||
$currencyCode = StringHelper::getCurrencyCode();
|
||||
}
|
||||
$value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value in a pre-defined format to a PHP string.
|
||||
*
|
||||
|
@ -676,92 +796,7 @@ class NumberFormat extends Supervisor
|
|||
// % number format
|
||||
self::formatAsPercentage($value, $format);
|
||||
} else {
|
||||
if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
|
||||
$value = 'EUR ' . sprintf('%1.2f', $value);
|
||||
} else {
|
||||
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
|
||||
$format = str_replace(['"', '*'], '', $format);
|
||||
|
||||
// Find out if we need thousands separator
|
||||
// This is indicated by a comma enclosed by a digit placeholder:
|
||||
// #,# or 0,0
|
||||
$useThousands = preg_match('/(#,#|0,0)/', $format);
|
||||
if ($useThousands) {
|
||||
$format = preg_replace('/0,0/', '00', $format);
|
||||
$format = preg_replace('/#,#/', '##', $format);
|
||||
}
|
||||
|
||||
// Scale thousands, millions,...
|
||||
// This is indicated by a number of commas after a digit placeholder:
|
||||
// #, or 0.0,,
|
||||
$scale = 1; // same as no scale
|
||||
$matches = [];
|
||||
if (preg_match('/(#|0)(,+)/', $format, $matches)) {
|
||||
$scale = pow(1000, strlen($matches[2]));
|
||||
|
||||
// strip the commas
|
||||
$format = preg_replace('/0,+/', '0', $format);
|
||||
$format = preg_replace('/#,+/', '#', $format);
|
||||
}
|
||||
|
||||
if (preg_match('/#?.*\?\/\?/', $format, $m)) {
|
||||
if ($value != (int) $value) {
|
||||
self::formatAsFraction($value, $format);
|
||||
}
|
||||
} else {
|
||||
// Handle the number itself
|
||||
|
||||
// scale number
|
||||
$value = $value / $scale;
|
||||
|
||||
// Strip #
|
||||
$format = preg_replace('/\\#/', '0', $format);
|
||||
|
||||
// Remove locale code [$-###]
|
||||
$format = preg_replace('/\[\$\-.*\]/', '', $format);
|
||||
|
||||
$n = '/\\[[^\\]]+\\]/';
|
||||
$m = preg_replace($n, '', $format);
|
||||
$number_regex = '/(0+)(\\.?)(0*)/';
|
||||
if (preg_match($number_regex, $m, $matches)) {
|
||||
$left = $matches[1];
|
||||
$dec = $matches[2];
|
||||
$right = $matches[3];
|
||||
|
||||
// minimun width of formatted number (including dot)
|
||||
$minWidth = strlen($left) + strlen($dec) + strlen($right);
|
||||
if ($useThousands) {
|
||||
$value = number_format(
|
||||
$value,
|
||||
strlen($right),
|
||||
StringHelper::getDecimalSeparator(),
|
||||
StringHelper::getThousandsSeparator()
|
||||
);
|
||||
$value = preg_replace($number_regex, $value, $format);
|
||||
} else {
|
||||
if (preg_match('/[0#]E[+-]0/i', $format)) {
|
||||
// Scientific format
|
||||
$value = sprintf('%5.2E', $value);
|
||||
} elseif (preg_match('/0([^\d\.]+)0/', $format)) {
|
||||
$value = self::complexNumberFormatMask($value, $format);
|
||||
} else {
|
||||
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
|
||||
$value = sprintf($sprintf_pattern, $value);
|
||||
$value = preg_replace($number_regex, $value, $format);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (preg_match('/\[\$(.*)\]/u', $format, $m)) {
|
||||
// Currency or Accounting
|
||||
$currencyCode = $m[1];
|
||||
list($currencyCode) = explode('-', $currencyCode);
|
||||
if ($currencyCode == '') {
|
||||
$currencyCode = StringHelper::getCurrencyCode();
|
||||
}
|
||||
$value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
|
||||
}
|
||||
}
|
||||
$value = self::formatAsNumber($value, $format);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,11 +33,6 @@ return [
|
|||
12,
|
||||
'#.0#',
|
||||
],
|
||||
[
|
||||
'-70',
|
||||
-70,
|
||||
'#,##0;[Red]-#,##0'
|
||||
],
|
||||
[
|
||||
'0.1',
|
||||
0.10000000000000001,
|
||||
|
@ -172,6 +167,7 @@ return [
|
|||
5.25,
|
||||
'???/???',
|
||||
],
|
||||
// Complex formats
|
||||
[
|
||||
'(001) 2-3456-789',
|
||||
123456789,
|
||||
|
@ -182,6 +178,31 @@ return [
|
|||
123456789,
|
||||
'0 (+00) 0000 00 00 00',
|
||||
],
|
||||
[
|
||||
'002-01-0035-7',
|
||||
20100357,
|
||||
'000-00-0000-0',
|
||||
],
|
||||
[
|
||||
'002-01-00.35-7',
|
||||
20100.357,
|
||||
'000-00-00.00-0',
|
||||
],
|
||||
[
|
||||
'002.01.0035.7',
|
||||
20100357,
|
||||
'000\.00\.0000\.0',
|
||||
],
|
||||
[
|
||||
'002.01.00.35.7',
|
||||
20100.357,
|
||||
'000\.00\.00.00\.0',
|
||||
],
|
||||
[
|
||||
'002.01.00.35.70',
|
||||
20100.357,
|
||||
'000\.00\.00.00\.00',
|
||||
],
|
||||
[
|
||||
'12345:67:89',
|
||||
123456789,
|
||||
|
@ -239,25 +260,33 @@ return [
|
|||
12345,
|
||||
'[Green]General',
|
||||
],
|
||||
[
|
||||
'-70',
|
||||
-70,
|
||||
'#,##0;[Red]-#,##0'
|
||||
],
|
||||
[
|
||||
'-12,345',
|
||||
-12345,
|
||||
'#,##0;[Red]-#,##0'
|
||||
],
|
||||
// Multiple colors
|
||||
[
|
||||
'12345',
|
||||
12345,
|
||||
'[Blue]0;[Red]0',
|
||||
],
|
||||
// Multiple colors
|
||||
[
|
||||
'Positive',
|
||||
12,
|
||||
'[Green]"Positive";[Red]"Negative";[Blue]"Zero"',
|
||||
],
|
||||
// Multiple colors
|
||||
// Multiple colors with text substitution
|
||||
[
|
||||
'Zero',
|
||||
0,
|
||||
'[Green]"Positive";[Red]"Negative";[Blue]"Zero"',
|
||||
],
|
||||
// Multiple colors
|
||||
[
|
||||
'Negative',
|
||||
-2,
|
||||
|
|
Loading…
Reference in New Issue