Skip to content

Fraction Formatting #2254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 0 additions & 95 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@

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;

Expand Down Expand Up @@ -42,4 +51,14 @@ public static function format($value, string $format): string

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;
}
}
71 changes: 47 additions & 24 deletions src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -28,7 +28,10 @@ private static function mergeComplexNumberFormatMasks($numbers, $masks): array
];
}

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);
Expand All @@ -52,13 +55,16 @@ private static function processComplexNumberFormatMask($number, $mask): string
$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);
Expand All @@ -77,7 +83,10 @@ private static function complexNumberFormatMask($number, $mask, $splitOnPoint =
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];
Expand All @@ -93,7 +102,7 @@ private static function formatStraightNumericValue($value, $format, array $match
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)) {
Expand All @@ -110,10 +119,13 @@ private static function formatStraightNumericValue($value, $format, array $match
$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
Expand All @@ -123,15 +135,15 @@ public static function format($value, $format): string
//}

// 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,...
Expand All @@ -143,32 +155,30 @@ public static function format($value, $format): string
$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);
}
$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));
}
}

Expand All @@ -179,9 +189,22 @@ public static function format($value, $format): string
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));
}
}
30 changes: 30 additions & 0 deletions tests/data/Style/NumberFormat.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,36 @@
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',
Expand Down