Fraction Formatting (#2254)
* Fraction Formatting See issue #2253. User's analysis was correct - leading zeros in the decimal portion were being stripped out, so 0.0625 and 0.625 were being treated the same. As it turns out, integers also aren't handled well (`0 0/1` anyone?). The latter problem had been hidden because caller tested for integer first and skipped call if true; but FractionFormatter::format is public and should work correctly regardless. All Phpstan baseline entries for FractionFormatter and NumberFormatter are eliminated. New test data is added; no need for changes to test code. * Scrutinizer Ensure result is string.
This commit is contained in:
parent
d0076343c4
commit
710f9f17a7
|
|
@ -4590,101 +4590,6 @@ parameters:
|
||||||
count: 1
|
count: 1
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php
|
path: src/PhpSpreadsheet/Style/NumberFormat/Formatter.php
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\FractionFormatter\\:\\:format\\(\\) has parameter \\$value with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$str of function trim expects string, float given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:mergeComplexNumberFormatMasks\\(\\) has parameter \\$masks with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:mergeComplexNumberFormatMasks\\(\\) has parameter \\$numbers with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:processComplexNumberFormatMask\\(\\) has parameter \\$mask with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:processComplexNumberFormatMask\\(\\) has parameter \\$number with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:complexNumberFormatMask\\(\\) has parameter \\$mask with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:complexNumberFormatMask\\(\\) has parameter \\$number with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:complexNumberFormatMask\\(\\) has parameter \\$splitOnPoint with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#1 \\$haystack of function strpos expects string, float\\|int given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$str of function explode expects string, float\\|int given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:formatStraightNumericValue\\(\\) has parameter \\$format with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:formatStraightNumericValue\\(\\) has parameter \\$useThousands with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:formatStraightNumericValue\\(\\) has parameter \\$value with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:format\\(\\) has parameter \\$format with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\NumberFormatter\\:\\:format\\(\\) has parameter \\$value with no typehint specified\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, array\\|string\\|null given\\.$#"
|
|
||||||
count: 6
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$subject of function preg_match expects string, array\\|string\\|null given\\.$#"
|
|
||||||
count: 4
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
|
||||||
message: "#^Parameter \\#2 \\$format of static method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\FractionFormatter\\:\\:format\\(\\) expects string, array\\|string\\|null given\\.$#"
|
|
||||||
count: 1
|
|
||||||
path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
|
|
||||||
|
|
||||||
-
|
-
|
||||||
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\PercentageFormatter\\:\\:format\\(\\) has parameter \\$value with no typehint specified\\.$#"
|
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\PercentageFormatter\\:\\:format\\(\\) has parameter \\$value with no typehint specified\\.$#"
|
||||||
count: 1
|
count: 1
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,23 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
|
||||||
|
|
||||||
class FractionFormatter extends BaseFormatter
|
class FractionFormatter extends BaseFormatter
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
public static function format($value, string $format): string
|
public static function format($value, string $format): string
|
||||||
{
|
{
|
||||||
$format = self::stripQuotes($format);
|
$format = self::stripQuotes($format);
|
||||||
|
$value = (float) $value;
|
||||||
|
$absValue = abs($value);
|
||||||
|
|
||||||
$sign = ($value < 0.0) ? '-' : '';
|
$sign = ($value < 0.0) ? '-' : '';
|
||||||
|
|
||||||
$integerPart = floor(abs($value));
|
$integerPart = floor($absValue);
|
||||||
$decimalPart = trim(fmod(abs($value), 1), '0.');
|
|
||||||
|
$decimalPart = self::getDecimal((string) $absValue);
|
||||||
|
if ($decimalPart === '0') {
|
||||||
|
return "{$sign}{$integerPart}";
|
||||||
|
}
|
||||||
$decimalLength = strlen($decimalPart);
|
$decimalLength = strlen($decimalPart);
|
||||||
$decimalDivisor = 10 ** $decimalLength;
|
$decimalDivisor = 10 ** $decimalLength;
|
||||||
|
|
||||||
|
|
@ -42,4 +51,14 @@ class FractionFormatter extends BaseFormatter
|
||||||
|
|
||||||
return "{$sign}{$adjustedDecimalPart}/{$adjustedDecimalDivisor}";
|
return "{$sign}{$adjustedDecimalPart}/{$adjustedDecimalDivisor}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function getDecimal(string $value): string
|
||||||
|
{
|
||||||
|
$decimalPart = '0';
|
||||||
|
if (preg_match('/^\\d*[.](\\d*[1-9])0*$/', $value, $matches) === 1) {
|
||||||
|
$decimalPart = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decimalPart;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class NumberFormatter
|
||||||
{
|
{
|
||||||
private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/';
|
private const NUMBER_REGEX = '/(0+)(\\.?)(0*)/';
|
||||||
|
|
||||||
private static function mergeComplexNumberFormatMasks($numbers, $masks): array
|
private static function mergeComplexNumberFormatMasks(array $numbers, array $masks): array
|
||||||
{
|
{
|
||||||
$decimalCount = strlen($numbers[1]);
|
$decimalCount = strlen($numbers[1]);
|
||||||
$postDecimalMasks = [];
|
$postDecimalMasks = [];
|
||||||
|
|
@ -28,7 +28,10 @@ class NumberFormatter
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function processComplexNumberFormatMask($number, $mask): string
|
/**
|
||||||
|
* @param mixed $number
|
||||||
|
*/
|
||||||
|
private static function processComplexNumberFormatMask($number, string $mask): string
|
||||||
{
|
{
|
||||||
$result = $number;
|
$result = $number;
|
||||||
$maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
|
$maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
|
||||||
|
|
@ -52,13 +55,16 @@ class NumberFormatter
|
||||||
$result = $mask;
|
$result = $mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return self::makeString($result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true): string
|
/**
|
||||||
|
* @param mixed $number
|
||||||
|
*/
|
||||||
|
private static function complexNumberFormatMask($number, string $mask, bool $splitOnPoint = true): string
|
||||||
{
|
{
|
||||||
$sign = ($number < 0.0) ? '-' : '';
|
$sign = ($number < 0.0) ? '-' : '';
|
||||||
$number = abs($number);
|
$number = (string) abs($number);
|
||||||
|
|
||||||
if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
|
if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
|
||||||
$numbers = explode('.', $number);
|
$numbers = explode('.', $number);
|
||||||
|
|
@ -77,7 +83,10 @@ class NumberFormatter
|
||||||
return "{$sign}{$result}";
|
return "{$sign}{$result}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function formatStraightNumericValue($value, $format, array $matches, $useThousands): string
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
private static function formatStraightNumericValue($value, string $format, array $matches, bool $useThousands): string
|
||||||
{
|
{
|
||||||
$left = $matches[1];
|
$left = $matches[1];
|
||||||
$dec = $matches[2];
|
$dec = $matches[2];
|
||||||
|
|
@ -93,7 +102,7 @@ class NumberFormatter
|
||||||
StringHelper::getThousandsSeparator()
|
StringHelper::getThousandsSeparator()
|
||||||
);
|
);
|
||||||
|
|
||||||
return preg_replace(self::NUMBER_REGEX, $value, $format);
|
return self::pregReplace(self::NUMBER_REGEX, $value, $format);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preg_match('/[0#]E[+-]0/i', $format)) {
|
if (preg_match('/[0#]E[+-]0/i', $format)) {
|
||||||
|
|
@ -110,10 +119,13 @@ class NumberFormatter
|
||||||
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
|
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
|
||||||
$value = sprintf($sprintf_pattern, $value);
|
$value = sprintf($sprintf_pattern, $value);
|
||||||
|
|
||||||
return preg_replace(self::NUMBER_REGEX, $value, $format);
|
return self::pregReplace(self::NUMBER_REGEX, $value, $format);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function format($value, $format): string
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public static function format($value, string $format): string
|
||||||
{
|
{
|
||||||
// The "_" in this string has already been stripped out,
|
// The "_" in this string has already been stripped out,
|
||||||
// so this test is never true. Furthermore, testing
|
// so this test is never true. Furthermore, testing
|
||||||
|
|
@ -123,15 +135,15 @@ class NumberFormatter
|
||||||
//}
|
//}
|
||||||
|
|
||||||
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
|
// Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols
|
||||||
$format = str_replace(['"', '*'], '', $format);
|
$format = self::makeString(str_replace(['"', '*'], '', $format));
|
||||||
|
|
||||||
// Find out if we need thousands separator
|
// Find out if we need thousands separator
|
||||||
// This is indicated by a comma enclosed by a digit placeholder:
|
// This is indicated by a comma enclosed by a digit placeholder:
|
||||||
// #,# or 0,0
|
// #,# or 0,0
|
||||||
$useThousands = preg_match('/(#,#|0,0)/', $format);
|
$useThousands = (bool) preg_match('/(#,#|0,0)/', $format);
|
||||||
if ($useThousands) {
|
if ($useThousands) {
|
||||||
$format = preg_replace('/0,0/', '00', $format);
|
$format = self::pregReplace('/0,0/', '00', $format);
|
||||||
$format = preg_replace('/#,#/', '##', $format);
|
$format = self::pregReplace('/#,#/', '##', $format);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale thousands, millions,...
|
// Scale thousands, millions,...
|
||||||
|
|
@ -143,32 +155,30 @@ class NumberFormatter
|
||||||
$scale = 1000 ** strlen($matches[2]);
|
$scale = 1000 ** strlen($matches[2]);
|
||||||
|
|
||||||
// strip the commas
|
// strip the commas
|
||||||
$format = preg_replace('/0,+/', '0', $format);
|
$format = self::pregReplace('/0,+/', '0', $format);
|
||||||
$format = preg_replace('/#,+/', '#', $format);
|
$format = self::pregReplace('/#,+/', '#', $format);
|
||||||
}
|
}
|
||||||
if (preg_match('/#?.*\?\/\?/', $format, $m)) {
|
if (preg_match('/#?.*\?\/\?/', $format, $m)) {
|
||||||
if ($value != (int) $value) {
|
$value = FractionFormatter::format($value, $format);
|
||||||
$value = FractionFormatter::format($value, $format);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Handle the number itself
|
// Handle the number itself
|
||||||
|
|
||||||
// scale number
|
// scale number
|
||||||
$value = $value / $scale;
|
$value = $value / $scale;
|
||||||
// Strip #
|
// Strip #
|
||||||
$format = preg_replace('/\\#/', '0', $format);
|
$format = self::pregReplace('/\\#/', '0', $format);
|
||||||
// Remove locale code [$-###]
|
// Remove locale code [$-###]
|
||||||
$format = preg_replace('/\[\$\-.*\]/', '', $format);
|
$format = self::pregReplace('/\[\$\-.*\]/', '', $format);
|
||||||
|
|
||||||
$n = '/\\[[^\\]]+\\]/';
|
$n = '/\\[[^\\]]+\\]/';
|
||||||
$m = preg_replace($n, '', $format);
|
$m = self::pregReplace($n, '', $format);
|
||||||
if (preg_match(self::NUMBER_REGEX, $m, $matches)) {
|
if (preg_match(self::NUMBER_REGEX, $m, $matches)) {
|
||||||
// There are placeholders for digits, so inject digits from the value into the mask
|
// There are placeholders for digits, so inject digits from the value into the mask
|
||||||
$value = self::formatStraightNumericValue($value, $format, $matches, $useThousands);
|
$value = self::formatStraightNumericValue($value, $format, $matches, $useThousands);
|
||||||
} elseif ($format !== NumberFormat::FORMAT_GENERAL) {
|
} elseif ($format !== NumberFormat::FORMAT_GENERAL) {
|
||||||
// Yes, I know that this is basically just a hack;
|
// Yes, I know that this is basically just a hack;
|
||||||
// if there's no placeholders for digits, just return the format mask "as is"
|
// if there's no placeholders for digits, just return the format mask "as is"
|
||||||
$value = str_replace('?', '', $format ?? '');
|
$value = self::makeString(str_replace('?', '', $format));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,9 +189,22 @@ class NumberFormatter
|
||||||
if ($currencyCode == '') {
|
if ($currencyCode == '') {
|
||||||
$currencyCode = StringHelper::getCurrencyCode();
|
$currencyCode = StringHelper::getCurrencyCode();
|
||||||
}
|
}
|
||||||
$value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value);
|
$value = self::pregReplace('/\[\$([^\]]*)\]/u', $currencyCode, (string) $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
private static function makeString($value): string
|
||||||
|
{
|
||||||
|
return is_array($value) ? '' : (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function pregReplace(string $pattern, string $replacement, string $subject): string
|
||||||
|
{
|
||||||
|
return self::makeString(preg_replace($pattern, $replacement, $subject));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,36 @@ return [
|
||||||
0.75,
|
0.75,
|
||||||
'? ??/???',
|
'? ??/???',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
' 3/4',
|
||||||
|
'0.75000',
|
||||||
|
'? ??/???',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'5 1/16',
|
||||||
|
5.0625,
|
||||||
|
'? ??/???',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'- 5/8',
|
||||||
|
-0.625,
|
||||||
|
'? ??/???',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'0',
|
||||||
|
0,
|
||||||
|
'? ??/???',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'0',
|
||||||
|
'0.000',
|
||||||
|
'? ??/???',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'-16',
|
||||||
|
'-016.0',
|
||||||
|
'? ??/???',
|
||||||
|
],
|
||||||
// Complex formats
|
// Complex formats
|
||||||
[
|
[
|
||||||
'(001) 2-3456-789',
|
'(001) 2-3456-789',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue