Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Some earlier branches remain supported and security fixes are applied to them; i
- `removeColumn()` no longer throws "Invalid column index 0" when it removes column A and every column up to the highest data column, e.g. `removeColumn('A')` on an empty sheet. [Issue #5049](https://github.com/PHPOffice/PhpSpreadsheet/issues/5049) [PR #5050](https://github.com/PHPOffice/PhpSpreadsheet/pull/5050)
- `removeColumn()` and `removeRow()` take the removed columns/rows out of data validation ranges: a validation that lies only on them is deleted, and a range that ends on them shrinks, instead of covering the column/row that slides into their place. [Issue #5047](https://github.com/PHPOffice/PhpSpreadsheet/issues/5047) [PR #5048](https://github.com/PHPOffice/PhpSpreadsheet/pull/5048)
- Minor cleanup in Ods Reader to fix test which was not working in Windows. [PR #5052](https://github.com/PHPOffice/PhpSpreadsheet/pull/5052)
- A fraction number format writes an integer as a fraction when the format has no integer part, so 1 is `1/1` with `0/0`, and when the last numerator placeholder is 0, so 1 is `1 0/1` with `0 0/0`, as Excel does. [Issue #3625](https://github.com/PHPOffice/PhpSpreadsheet/issues/3625) [PR #5060](https://github.com/PHPOffice/PhpSpreadsheet/pull/5060)
- Branch pruning no longer throws "internal error" for an IF nested in the condition of another IF inside a pruned branch. [Issue #3755](https://github.com/PHPOffice/PhpSpreadsheet/issues/3755) [Issue #3790](https://github.com/PHPOffice/PhpSpreadsheet/issues/3790) [PR #5055](https://github.com/PHPOffice/PhpSpreadsheet/pull/5055)
- Xls Writer no longer loses EMF and WMF images (they are written as PNG); the resulting file could not be read back (`Call to a member function getBlipType() on null`). [Issue #274](https://github.com/PHPOffice/PhpSpreadsheet/issues/274)
- Xls Reader reads EMF and WMF pictures (as PNG `MemoryDrawing`); they were ignored. [Issue #274](https://github.com/PHPOffice/PhpSpreadsheet/issues/274)
Expand Down
44 changes: 30 additions & 14 deletions src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ class FractionFormatter extends BaseFormatter
/** @param null|bool|float|int|string $value value to be formatted */
public static function format(mixed $value, string $format): string
{
// A 0 placeholder for the last digit of the numerator shows a numerator of 0
$showZeroNumerator = str_contains($format, '0/');
// 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 = self::stripQuotes($format);
$value = (float) $value;
$absValue = abs($value);
Expand All @@ -17,25 +22,36 @@ public static function format(mixed $value, string $format): string

$integerPart = floor($absValue);

$decimalPart = self::getDecimal((string) $absValue);
if ($decimalPart === '0') {
return "{$sign}{$integerPart}";
}
$decimalLength = strlen($decimalPart);
$decimalDivisor = 10 ** $decimalLength;

preg_match('/(#?.*\?)\/(\?+|\d+)/', $format, $matches);
$formatIntegerPart = $matches[1] ?? '0';
$fixedDivisor = (isset($matches[2]) && is_numeric($matches[2])) ? (int) $matches[2] : null;
$isVulgar = !str_contains($formatIntegerPart, '0')
&& !str_contains($formatIntegerPart, '#')
&& !str_starts_with($formatIntegerPart, '? ?');

if (isset($matches[2]) && is_numeric($matches[2])) {
$fractionDivisor = 100 / (int) $matches[2];
$decimalPart = self::getDecimal((string) $absValue);
if ($decimalPart === '0') {
// An integer is written as a fraction when the format has no integer part,
// or when the numerator shows 0; otherwise the fraction is left out
if (!$isVulgar && !$showZeroNumerator) {
return "{$sign}{$integerPart}";
}
$adjustedDecimalPart = 0;
$adjustedDecimalDivisor = $fixedDivisor ?? 1;
} else {
/** @var float $fractionDivisor */
$fractionDivisor = MathTrig\Gcd::evaluate((int) $decimalPart, $decimalDivisor);
}
$decimalLength = strlen($decimalPart);
$decimalDivisor = 10 ** $decimalLength;

if ($fixedDivisor !== null) {
$fractionDivisor = 100 / $fixedDivisor;
} else {
/** @var float $fractionDivisor */
$fractionDivisor = MathTrig\Gcd::evaluate((int) $decimalPart, $decimalDivisor);
}

$adjustedDecimalPart = (int) round((int) $decimalPart / $fractionDivisor, 0);
$adjustedDecimalDivisor = $decimalDivisor / $fractionDivisor;
$adjustedDecimalPart = (int) round((int) $decimalPart / $fractionDivisor, 0);
$adjustedDecimalDivisor = $decimalDivisor / $fractionDivisor;
}

if ((str_contains($formatIntegerPart, '0'))) {
return "{$sign}{$integerPart} {$adjustedDecimalPart}/{$adjustedDecimalDivisor}";
Expand Down
3 changes: 0 additions & 3 deletions src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,6 @@ public static function format(mixed $value, string $format): string
$scale = self::scaleThousandsMillions($format);

if (preg_match('/[#\?0]?.*[#\?0]\/(\?+|\d+|#)/', $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);
} else {
// Handle the number itself
Expand Down
51 changes: 51 additions & 0 deletions tests/data/Style/NumberFormatFractions.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,55 @@
2.65,
'# ???/12',
],
// Integer values (issue 3625)
'integer in vulgar fraction' => [
'1/1',
1,
'0/0',
],
'integer in vulgar fraction with ? placeholders' => [
'1/1',
1,
'??/???',
],
'larger integer in vulgar fraction' => [
'12/1',
12,
'?/?',
],
'negative integer in vulgar fraction' => [
'-3/1',
-3,
'?/?',
],
'zero in vulgar fraction' => [
'0/1',
0,
'0/0',
],
'integer in vulgar fraction with fixed denominator' => [
'500/100',
5,
'??/100',
],
'integer with 0 numerator placeholder' => [
'1 0/1',
1,
'0 0/0',
],
'integer with 0 numerator placeholder and fixed denominator' => [
'5 0/4',
5,
'0 0/4',
],
'integer with ? numerator placeholder' => [
'1',
1,
'? ??/???',
],
'integer with # whole part and ? numerator placeholder' => [
'7',
7,
'# ?/?',
],
];
Loading