diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 05b2e9a0..d6041985 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -9,7 +9,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; @@ -104,7 +104,7 @@ class AutoFilter * Set AutoFilter Cell Range. * * @param AddressRange|array|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 an AddressRange object. */ @@ -115,6 +115,7 @@ class AutoFilter if ($range !== '') { [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); } + if (empty($range)) { // Discard all column rules $this->columns = []; @@ -123,8 +124,8 @@ class AutoFilter return $this; } - if (strpos($range, ':') === false) { - throw new PhpSpreadsheetException('Autofilter must be set on a range of cells.'); + if (ctype_digit($range) || ctype_alpha($range)) { + throw new Exception("{$range} is an invalid range for AutoFilter"); } $this->range = $range; @@ -174,13 +175,13 @@ class AutoFilter public function testColumnInRange($column) { if (empty($this->range)) { - throw new PhpSpreadsheetException('No autofilter range is defined.'); + throw new Exception('No autofilter range is defined.'); } $columnIndex = Coordinate::columnIndexFromString($column); [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); 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]; @@ -247,7 +248,7 @@ class AutoFilter } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) { $column = $columnObjectOrString->getColumnIndex(); } else { - throw new PhpSpreadsheetException('Column is not within the autofilter range.'); + throw new Exception('Column is not within the autofilter range.'); } $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, for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) { $result = true; @@ -1053,6 +1056,29 @@ class AutoFilter 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. */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index b3338c07..0ca9f64f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -115,7 +115,9 @@ class DefinedNames [, $range[0]] = Worksheet::extractSheetTitle($range[0], true); $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); $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range); diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index a80d3d63..ebec3686 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -65,6 +65,7 @@ class AutoFilterTest extends SetupTeardown $ranges = [ 'G1:J512' => "$title!G1:J512", 'K1:N20' => 'K1:N20', + 'B10' => 'B10', ]; foreach ($ranges as $actualRange => $fullRange) { @@ -94,11 +95,22 @@ class AutoFilterTest extends SetupTeardown self::assertEquals($expectedResult, $result); } - public function testSetRangeInvalidRange(): void + public function testSetRangeInvalidRowRange(): void { $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(); $autoFilter = $sheet->getAutoFilter(); @@ -498,4 +510,26 @@ class AutoFilterTest extends SetupTeardown self::assertArrayHasKey('K', $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'); + } }