From 531bd5afd926289d03d910615b46e2719a5cd001 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Wed, 17 Nov 2021 06:32:48 -0800 Subject: [PATCH] Rounding in NumberFormatter Fix #2385. NumberFormatter is using sprintf on a float, and is seeing inconsistent rounding as a result (it will also occasionally result in `-0`). Change to round the number before presenting it to sprintf. --- phpstan-baseline.neon | 30 ----------------- .../Style/NumberFormat/NumberFormatter.php | 29 +++++++++++------ .../Style/NumberFormatRoundTest.php | 32 +++++++++++++++++++ 3 files changed, 51 insertions(+), 40 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Style/NumberFormatRoundTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bcdba204ff..3440bd2356 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5995,36 +5995,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php - - - message: "#^Cannot cast mixed to int\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php - - - - message: "#^Cannot cast mixed to string\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php - - - - message: "#^Parameter \\#1 \\$number of function abs expects float\\|int\\|string, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php - - - - message: "#^Parameter \\#1 \\$number of function number_format expects float, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php - - - - message: "#^Parameter \\#1 \\$x of function fmod expects float, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php - - - - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\\\PercentageFormatter\\:\\:format\\(\\) has parameter \\$value with no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php index b2ed3c8cec..9129dd3254 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php @@ -33,6 +33,7 @@ private static function mergeComplexNumberFormatMasks(array $numbers, array $mas */ private static function processComplexNumberFormatMask($number, string $mask): string { + /** @var string */ $result = $number; $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE); @@ -45,8 +46,10 @@ private static function processComplexNumberFormatMask($number, string $mask): s $divisor = 10 ** $size; $offset = $block[1]; - $blockValue = sprintf("%0{$size}d", fmod($number, $divisor)); - $number = floor($number / $divisor); + /** @var float */ + $numberFloat = $number; + $blockValue = sprintf("%0{$size}d", fmod($numberFloat, $divisor)); + $number = floor($numberFloat / $divisor); $mask = substr_replace($mask, $blockValue, $offset, $size); } if ($number > 0) { @@ -64,7 +67,9 @@ private static function processComplexNumberFormatMask($number, string $mask): s private static function complexNumberFormatMask($number, string $mask, bool $splitOnPoint = true): string { $sign = ($number < 0.0) ? '-' : ''; - $number = (string) abs($number); + /** @var float */ + $numberFloat = $number; + $number = (string) abs($numberFloat); if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) { $numbers = explode('.', $number); @@ -88,6 +93,8 @@ private static function complexNumberFormatMask($number, string $mask, bool $spl */ private static function formatStraightNumericValue($value, string $format, array $matches, bool $useThousands): string { + /** @var float */ + $valueFloat = $value; $left = $matches[1]; $dec = $matches[2]; $right = $matches[3]; @@ -96,7 +103,7 @@ private static function formatStraightNumericValue($value, string $format, array $minWidth = strlen($left) + strlen($dec) + strlen($right); if ($useThousands) { $value = number_format( - $value, + $valueFloat, strlen($right), StringHelper::getDecimalSeparator(), StringHelper::getThousandsSeparator() @@ -107,9 +114,9 @@ private static function formatStraightNumericValue($value, string $format, array if (preg_match('/[0#]E[+-]0/i', $format)) { // Scientific format - return sprintf('%5.2E', $value); + return sprintf('%5.2E', $valueFloat); } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { - if ($value == (int) $value && substr_count($format, '.') === 1) { + if ($value == (int) $valueFloat && substr_count($format, '.') === 1) { $value *= 10 ** strlen(explode('.', $format)[1]); } @@ -117,7 +124,9 @@ private static function formatStraightNumericValue($value, string $format, array } $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; - $value = sprintf($sprintf_pattern, $value); + /** @var float */ + $valueFloat = $value; + $value = sprintf($sprintf_pattern, round($valueFloat, strlen($right))); return self::pregReplace(self::NUMBER_REGEX, $value, $format); } @@ -196,15 +205,15 @@ public static function format($value, string $format): string } /** - * @param mixed $value + * @param array|string $value */ private static function makeString($value): string { - return is_array($value) ? '' : (string) $value; + return is_array($value) ? '' : "$value"; } private static function pregReplace(string $pattern, string $replacement, string $subject): string { - return self::makeString(preg_replace($pattern, $replacement, $subject)); + return self::makeString(preg_replace($pattern, $replacement, $subject) ?? ''); } } diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormatRoundTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormatRoundTest.php new file mode 100644 index 0000000000..3401b02015 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/NumberFormatRoundTest.php @@ -0,0 +1,32 @@ +getActiveSheet(); + $sheet->getStyle('A1:H2')->getNumberFormat()->setFormatCode('0'); + $sheet->getStyle('A3:H3')->getNumberFormat()->setFormatCode('0.0'); + $sheet->fromArray( + [ + [-3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.5, 3.5], + [-3.1, -2.9, -1.4, -0.3, 0.7, 1.6, 2.4, 3.7], + [-3.15, -2.85, -1.43, -0.87, 0.72, 1.60, 2.45, 3.75], + ] + ); + $expected = [ + [-4, -3, -2, -1, 1, 2, 3, 4], + [-3, -3, -1, 0, 1, 2, 2, 4], + [-3.2, -2.9, -1.4, -0.9, 0.7, 1.6, 2.5, 3.8], + ]; + self::assertEquals($expected, $sheet->toArray()); + $spreadsheet->disconnectWorksheets(); + } +}