From daff98be25f7ed8bdefb6a67522ed5b6c7ff31d2 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Wed, 2 Sep 2026 10:31:44 +0200 Subject: [PATCH] fix: display fraction for integer values with mandatory 0 placeholder (#3625) --- .../Style/NumberFormat/FractionFormatter.php | 42 +++++++++++++++++-- .../Style/NumberFormat/NumberFormatter.php | 3 +- tests/data/Style/NumberFormatFractions.php | 18 ++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php index 7affc21..121f131 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -6,8 +6,13 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class FractionFormatter extends BaseFormatter { - /** @param null|bool|float|int|string $value value to be formatted */ - public static function format(mixed $value, string $format): string + /** + * @param null|bool|float|int|string $value value to be formatted + * @param string $format normalized fraction mask (digit placeholders reduced to ?) + * @param ?string $originalFormat unmodified mask, needed to tell mandatory (0) + * placeholders apart from optional (? #) ones + */ + public static function format(mixed $value, string $format, ?string $originalFormat = null): string { $format = self::stripQuotes($format); $value = (float) $value; @@ -19,7 +24,7 @@ class FractionFormatter extends BaseFormatter $decimalPart = self::getDecimal((string) $absValue); if ($decimalPart === '0') { - return "{$sign}{$integerPart}"; + return self::integerFraction($sign, (int) $integerPart, $originalFormat ?? $format); } $decimalLength = strlen($decimalPart); $decimalDivisor = 10 ** $decimalLength; @@ -58,6 +63,37 @@ class FractionFormatter extends BaseFormatter return "{$sign}{$adjustedDecimalPart}/{$adjustedDecimalDivisor}"; } + /** + * Format a value with no fractional remainder. Excel still displays a + * fraction when the numerator placeholder uses mandatory digits (0), so + * format "0/0" yields "1/1" and "0 0/0" yields "1 0/1"; the "?" and "#" + * placeholders suppress the fraction and leave only the integer. + */ + private static function integerFraction(string $sign, int $integerPart, string $format): string + { + if (preg_match('~(?:([#0?]+)\s+)?([#0?]+)/([#0?]+|\d+)~', $format, $matches) !== 1) { + return "{$sign}{$integerPart}"; + } + [, $integerFormat, $numeratorFormat, $denominatorFormat] = $matches; + if (!str_contains($numeratorFormat, '0')) { + return "{$sign}{$integerPart}"; + } + + if (preg_match('/[1-9]/', $denominatorFormat) === 1) { + $denominator = $denominatorFormat; + } else { + $denominator = str_pad('1', substr_count($denominatorFormat, '0'), '0', STR_PAD_LEFT); + } + + if ($integerFormat !== '') { + return "{$sign}{$integerPart} 0/{$denominator}"; + } + + $numerator = $integerPart * (int) $denominator; + + return "{$sign}{$numerator}/{$denominator}"; + } + private static function getDecimal(string $value): string { $decimalPart = '0'; diff --git a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php index 22b02b1..bf5cb64 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php @@ -208,10 +208,11 @@ class NumberFormatter extends BaseFormatter $scale = self::scaleThousandsMillions($format); if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $format)) { + $originalFormat = $format; // It's a dirty hack; but replace # and 0 digit placeholders with ? $format = (string) preg_replace('/[#0]+\//', '?/', $format); $format = (string) preg_replace('/\/[#0]+/', '/?', $format); - $value = FractionFormatter::format($value, $format); + $value = FractionFormatter::format($value, $format, $originalFormat); } else { // Handle the number itself // scale number diff --git a/tests/data/Style/NumberFormatFractions.php b/tests/data/Style/NumberFormatFractions.php index 4daafca..4be2a0e 100644 --- a/tests/data/Style/NumberFormatFractions.php +++ b/tests/data/Style/NumberFormatFractions.php @@ -143,4 +143,22 @@ return [ 2.65, '# ???/12', ], + // Integer value, fraction format (issue #3625): a mandatory-digit (0) + // numerator still displays the fraction, while ? and # placeholders + // suppress it and leave only the integer. + [ + '1/1', + 1, + '0/0', + ], + [ + '1 0/1', + 1, + '0 0/0', + ], + [ + '1', + 1, + '? ??/???', + ], ];