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
|
### 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)
|
- 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)
|
- 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)
|
- 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);
|
$decimalCount = strlen($numbers[1]);
|
||||||
$number = abs($number);
|
$postDecimalMasks = [];
|
||||||
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])));
|
|
||||||
|
|
||||||
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);
|
private static function processComplexNumberFormatMask($number, $mask)
|
||||||
if ($r > 1) {
|
{
|
||||||
$result = array_reverse($result[0]);
|
$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];
|
$divisor = 1 . $block[0];
|
||||||
$size = strlen($block[0]);
|
$size = strlen($block[0]);
|
||||||
$offset = $block[1];
|
$offset = $block[1];
|
||||||
|
@ -584,13 +592,125 @@ class NumberFormat extends Supervisor
|
||||||
$mask = substr_replace($mask, $number, $offset, 0);
|
$mask = substr_replace($mask, $number, $offset, 0);
|
||||||
}
|
}
|
||||||
$result = $mask;
|
$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;
|
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.
|
* Convert a value in a pre-defined format to a PHP string.
|
||||||
*
|
*
|
||||||
|
@ -676,92 +796,7 @@ class NumberFormat extends Supervisor
|
||||||
// % number format
|
// % number format
|
||||||
self::formatAsPercentage($value, $format);
|
self::formatAsPercentage($value, $format);
|
||||||
} else {
|
} else {
|
||||||
if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) {
|
$value = self::formatAsNumber($value, $format);
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,6 @@ return [
|
||||||
12,
|
12,
|
||||||
'#.0#',
|
'#.0#',
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'-70',
|
|
||||||
-70,
|
|
||||||
'#,##0;[Red]-#,##0'
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'0.1',
|
'0.1',
|
||||||
0.10000000000000001,
|
0.10000000000000001,
|
||||||
|
@ -172,6 +167,7 @@ return [
|
||||||
5.25,
|
5.25,
|
||||||
'???/???',
|
'???/???',
|
||||||
],
|
],
|
||||||
|
// Complex formats
|
||||||
[
|
[
|
||||||
'(001) 2-3456-789',
|
'(001) 2-3456-789',
|
||||||
123456789,
|
123456789,
|
||||||
|
@ -182,6 +178,31 @@ return [
|
||||||
123456789,
|
123456789,
|
||||||
'0 (+00) 0000 00 00 00',
|
'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',
|
'12345:67:89',
|
||||||
123456789,
|
123456789,
|
||||||
|
@ -239,25 +260,33 @@ return [
|
||||||
12345,
|
12345,
|
||||||
'[Green]General',
|
'[Green]General',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'-70',
|
||||||
|
-70,
|
||||||
|
'#,##0;[Red]-#,##0'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'-12,345',
|
||||||
|
-12345,
|
||||||
|
'#,##0;[Red]-#,##0'
|
||||||
|
],
|
||||||
// Multiple colors
|
// Multiple colors
|
||||||
[
|
[
|
||||||
'12345',
|
'12345',
|
||||||
12345,
|
12345,
|
||||||
'[Blue]0;[Red]0',
|
'[Blue]0;[Red]0',
|
||||||
],
|
],
|
||||||
// Multiple colors
|
|
||||||
[
|
[
|
||||||
'Positive',
|
'Positive',
|
||||||
12,
|
12,
|
||||||
'[Green]"Positive";[Red]"Negative";[Blue]"Zero"',
|
'[Green]"Positive";[Red]"Negative";[Blue]"Zero"',
|
||||||
],
|
],
|
||||||
// Multiple colors
|
// Multiple colors with text substitution
|
||||||
[
|
[
|
||||||
'Zero',
|
'Zero',
|
||||||
0,
|
0,
|
||||||
'[Green]"Positive";[Red]"Negative";[Blue]"Zero"',
|
'[Green]"Positive";[Red]"Negative";[Blue]"Zero"',
|
||||||
],
|
],
|
||||||
// Multiple colors
|
|
||||||
[
|
[
|
||||||
'Negative',
|
'Negative',
|
||||||
-2,
|
-2,
|
||||||
|
|
Loading…
Reference in New Issue