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:
oleibman 2021-08-18 11:09:37 -07:00 committed by GitHub
parent d0076343c4
commit 710f9f17a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 121 deletions

View File

@ -4590,101 +4590,6 @@ parameters:
count: 1
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\\.$#"
count: 1

View File

@ -6,14 +6,23 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class FractionFormatter extends BaseFormatter
{
/**
* @param mixed $value
*/
public static function format($value, string $format): string
{
$format = self::stripQuotes($format);
$value = (float) $value;
$absValue = abs($value);
$sign = ($value < 0.0) ? '-' : '';
$integerPart = floor(abs($value));
$decimalPart = trim(fmod(abs($value), 1), '0.');
$integerPart = floor($absValue);
$decimalPart = self::getDecimal((string) $absValue);
if ($decimalPart === '0') {
return "{$sign}{$integerPart}";
}
$decimalLength = strlen($decimalPart);
$decimalDivisor = 10 ** $decimalLength;
@ -42,4 +51,14 @@ class FractionFormatter extends BaseFormatter
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;
}
}

View File

@ -9,7 +9,7 @@ class NumberFormatter
{
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]);
$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;
$maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE);
@ -52,13 +55,16 @@ class NumberFormatter
$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) ? '-' : '';
$number = abs($number);
$number = (string) abs($number);
if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) {
$numbers = explode('.', $number);
@ -77,7 +83,10 @@ class NumberFormatter
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];
$dec = $matches[2];
@ -93,7 +102,7 @@ class NumberFormatter
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)) {
@ -110,10 +119,13 @@ class NumberFormatter
$sprintf_pattern = "%0$minWidth." . strlen($right) . 'f';
$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,
// 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
$format = str_replace(['"', '*'], '', $format);
$format = self::makeString(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);
$useThousands = (bool) preg_match('/(#,#|0,0)/', $format);
if ($useThousands) {
$format = preg_replace('/0,0/', '00', $format);
$format = preg_replace('/#,#/', '##', $format);
$format = self::pregReplace('/0,0/', '00', $format);
$format = self::pregReplace('/#,#/', '##', $format);
}
// Scale thousands, millions,...
@ -143,32 +155,30 @@ class NumberFormatter
$scale = 1000 ** strlen($matches[2]);
// strip the commas
$format = preg_replace('/0,+/', '0', $format);
$format = preg_replace('/#,+/', '#', $format);
$format = self::pregReplace('/0,+/', '0', $format);
$format = self::pregReplace('/#,+/', '#', $format);
}
if (preg_match('/#?.*\?\/\?/', $format, $m)) {
if ($value != (int) $value) {
$value = FractionFormatter::format($value, $format);
}
} else {
// Handle the number itself
// scale number
$value = $value / $scale;
// Strip #
$format = preg_replace('/\\#/', '0', $format);
$format = self::pregReplace('/\\#/', '0', $format);
// Remove locale code [$-###]
$format = preg_replace('/\[\$\-.*\]/', '', $format);
$format = self::pregReplace('/\[\$\-.*\]/', '', $format);
$n = '/\\[[^\\]]+\\]/';
$m = preg_replace($n, '', $format);
$m = self::pregReplace($n, '', $format);
if (preg_match(self::NUMBER_REGEX, $m, $matches)) {
// There are placeholders for digits, so inject digits from the value into the mask
$value = self::formatStraightNumericValue($value, $format, $matches, $useThousands);
} elseif ($format !== NumberFormat::FORMAT_GENERAL) {
// Yes, I know that this is basically just a hack;
// 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 == '') {
$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));
}
}

View File

@ -297,6 +297,36 @@ return [
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
[
'(001) 2-3456-789',