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
{
/** @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';
@@ -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