fix: display fraction for integer values with mandatory 0 placeholder (#3625)

This commit is contained in:
dualfroz committed 2026-09-02 10:31:44 +02:00
1 parent 49d947e039
commit daff98be25
3 files changed
+59 -4

No files matched your search

@@ -6,8 +6,13 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
class FractionFormatter extends BaseFormatter 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); $format = self::stripQuotes($format);
$value = (float) $value; $value = (float) $value;
@@ -19,7 +24,7 @@ class FractionFormatter extends BaseFormatter
$decimalPart = self::getDecimal((string) $absValue); $decimalPart = self::getDecimal((string) $absValue);
if ($decimalPart === '0') { if ($decimalPart === '0') {
return "{$sign}{$integerPart}"; return self::integerFraction($sign, (int) $integerPart, $originalFormat ?? $format);
} }
$decimalLength = strlen($decimalPart); $decimalLength = strlen($decimalPart);
$decimalDivisor = 10 ** $decimalLength; $decimalDivisor = 10 ** $decimalLength;
@@ -58,6 +63,37 @@ class FractionFormatter extends BaseFormatter
return "{$sign}{$adjustedDecimalPart}/{$adjustedDecimalDivisor}"; 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 private static function getDecimal(string $value): string
{ {
$decimalPart = '0'; $decimalPart = '0';
@@ -208,10 +208,11 @@ class NumberFormatter extends BaseFormatter
$scale = self::scaleThousandsMillions($format); $scale = self::scaleThousandsMillions($format);
if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $format)) { if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $format)) {
$originalFormat = $format;
// It's a dirty hack; but replace # and 0 digit placeholders with ? // It's a dirty hack; but replace # and 0 digit placeholders with ?
$format = (string) preg_replace('/[#0]+\//', '?/', $format); $format = (string) preg_replace('/[#0]+\//', '?/', $format);
$format = (string) preg_replace('/\/[#0]+/', '/?', $format); $format = (string) preg_replace('/\/[#0]+/', '/?', $format);
$value = FractionFormatter::format($value, $format); $value = FractionFormatter::format($value, $format, $originalFormat);
} else { } else {
// Handle the number itself // Handle the number itself
// scale number // scale number
@@ -143,4 +143,22 @@ return [
2.65, 2.65,
'# ???/12', '# ???/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,
'? ??/???',
],
]; ];