Merge pull request #3111 from PHPOffice/Issue-3102_Allow-AutoFilter-on-Single-Cell

Allow AutoFilter on single cell, or a single row
This commit is contained in:
Mark Baker 2022-10-11 09:15:04 +02:00 committed by GitHub
commit 4adbfd86e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 72 additions and 10 deletions

View File

@ -9,7 +9,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule;
@ -104,7 +104,7 @@ class AutoFilter
* Set AutoFilter Cell Range. * Set AutoFilter Cell Range.
* *
* @param AddressRange|array<int>|string $range * @param AddressRange|array<int>|string $range
* A simple string containing a Cell range like 'A1:E10' is permitted * A simple string containing a Cell range like 'A1:E10' or a Cell address like 'A1' is permitted
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object. * or an AddressRange object.
*/ */
@ -115,6 +115,7 @@ class AutoFilter
if ($range !== '') { if ($range !== '') {
[, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
} }
if (empty($range)) { if (empty($range)) {
// Discard all column rules // Discard all column rules
$this->columns = []; $this->columns = [];
@ -123,8 +124,8 @@ class AutoFilter
return $this; return $this;
} }
if (strpos($range, ':') === false) { if (ctype_digit($range) || ctype_alpha($range)) {
throw new PhpSpreadsheetException('Autofilter must be set on a range of cells.'); throw new Exception("{$range} is an invalid range for AutoFilter");
} }
$this->range = $range; $this->range = $range;
@ -174,13 +175,13 @@ class AutoFilter
public function testColumnInRange($column) public function testColumnInRange($column)
{ {
if (empty($this->range)) { if (empty($this->range)) {
throw new PhpSpreadsheetException('No autofilter range is defined.'); throw new Exception('No autofilter range is defined.');
} }
$columnIndex = Coordinate::columnIndexFromString($column); $columnIndex = Coordinate::columnIndexFromString($column);
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) { if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
throw new PhpSpreadsheetException('Column is outside of current autofilter range.'); throw new Exception('Column is outside of current autofilter range.');
} }
return $columnIndex - $rangeStart[0]; return $columnIndex - $rangeStart[0];
@ -247,7 +248,7 @@ class AutoFilter
} elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) { } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) {
$column = $columnObjectOrString->getColumnIndex(); $column = $columnObjectOrString->getColumnIndex();
} else { } else {
throw new PhpSpreadsheetException('Column is not within the autofilter range.'); throw new Exception('Column is not within the autofilter range.');
} }
$this->testColumnInRange($column); $this->testColumnInRange($column);
@ -1031,6 +1032,8 @@ class AutoFilter
} }
} }
$rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]);
// Execute the column tests for each row in the autoFilter range to determine show/hide, // Execute the column tests for each row in the autoFilter range to determine show/hide,
for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) { for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) {
$result = true; $result = true;
@ -1053,6 +1056,29 @@ class AutoFilter
return $this; return $this;
} }
/**
* Magic Range Auto-sizing.
* For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range.
*/
public function autoExtendRange(int $startRow, int $endRow): int
{
if ($startRow === $endRow && $this->workSheet !== null) {
try {
$rowIterator = $this->workSheet->getRowIterator($startRow + 1);
} catch (Exception $e) {
// If there are no rows below $startRow
return $startRow;
}
foreach ($rowIterator as $row) {
if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) {
return $row->getRowIndex() - 1;
}
}
}
return $endRow;
}
/** /**
* Implement PHP __clone to create a deep clone, not just a shallow copy. * Implement PHP __clone to create a deep clone, not just a shallow copy.
*/ */

View File

@ -115,7 +115,9 @@ class DefinedNames
[, $range[0]] = Worksheet::extractSheetTitle($range[0], true); [, $range[0]] = Worksheet::extractSheetTitle($range[0], true);
$range[0] = Coordinate::absoluteCoordinate($range[0]); $range[0] = Coordinate::absoluteCoordinate($range[0]);
$range[1] = Coordinate::absoluteCoordinate($range[1]); if (count($range) > 1) {
$range[1] = Coordinate::absoluteCoordinate($range[1]);
}
$range = implode(':', $range); $range = implode(':', $range);
$this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range); $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range);

View File

@ -65,6 +65,7 @@ class AutoFilterTest extends SetupTeardown
$ranges = [ $ranges = [
'G1:J512' => "$title!G1:J512", 'G1:J512' => "$title!G1:J512",
'K1:N20' => 'K1:N20', 'K1:N20' => 'K1:N20',
'B10' => 'B10',
]; ];
foreach ($ranges as $actualRange => $fullRange) { foreach ($ranges as $actualRange => $fullRange) {
@ -94,11 +95,22 @@ class AutoFilterTest extends SetupTeardown
self::assertEquals($expectedResult, $result); self::assertEquals($expectedResult, $result);
} }
public function testSetRangeInvalidRange(): void public function testSetRangeInvalidRowRange(): void
{ {
$this->expectException(PhpSpreadsheetException::class); $this->expectException(PhpSpreadsheetException::class);
$expectedResult = 'A1'; $expectedResult = '999';
$sheet = $this->getSheet();
$autoFilter = $sheet->getAutoFilter();
$autoFilter->setRange($expectedResult);
}
public function testSetRangeInvalidColumnRange(): void
{
$this->expectException(PhpSpreadsheetException::class);
$expectedResult = 'ABC';
$sheet = $this->getSheet(); $sheet = $this->getSheet();
$autoFilter = $sheet->getAutoFilter(); $autoFilter = $sheet->getAutoFilter();
@ -498,4 +510,26 @@ class AutoFilterTest extends SetupTeardown
self::assertArrayHasKey('K', $columns); self::assertArrayHasKey('K', $columns);
self::assertArrayHasKey('M', $columns); self::assertArrayHasKey('M', $columns);
} }
public function testAutoExtendRange(): void
{
$spreadsheet = $this->getSpreadsheet();
$worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet, 'Autosized AutoFilter'));
$worksheet->getCell('A1')->setValue('Col 1');
$worksheet->getCell('B1')->setValue('Col 2');
$worksheet->setAutoFilter('A1:B1');
$lastRow = $worksheet->getAutoFilter()->autoExtendRange(1, 1);
self::assertSame(1, $lastRow, 'No data below AutoFilter, so there should ne no resize');
$lastRow = $worksheet->getAutoFilter()->autoExtendRange(1, 999);
self::assertSame(999, $lastRow, 'Filter range is already correctly sized');
$data = [['A', 'A'], ['B', 'A'], ['A', 'B'], ['C', 'B'], ['B', null], [null, null], ['D', 'D'], ['E', 'E']];
$worksheet->fromArray($data, null, 'A2', true);
$lastRow = $worksheet->getAutoFilter()->autoExtendRange(1, 1);
self::assertSame(6, $lastRow, 'Filter range has been re-sized incorrectly');
}
} }