First steps in the implementation of AutoFilters for ODS Reader and Writer (#2053)

* First steps in the implementation of AutoFilters for ODS Reader and Writer, starting with reading a basic AutoFilter range (ignoring row visibility, filter types and active filters for the moment).

And also some additional refactoring to extract the DefinedNames Reader into its own dedicated class as a part of overall code improvement... on the principle of "when working on a class, always try to leave the library codebase in a better state than you found it"

* Provide a basic Ods Writer implementation for AutoFilters
* AutoFilter Reader Test
* AutoFilter Writer Test
* Update Change Log
This commit is contained in:
Mark Baker 2021-05-02 22:00:48 +02:00 committed by GitHub
parent defef9a4ff
commit 83e55cffcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 325 additions and 82 deletions

View File

@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added
- Implemented basic AutoFiltering for Ods Reader and Writer [PR #2053](https://github.com/PHPOffice/PhpSpreadsheet/pull/2053)
- Improved support for Row and Column ranges in formulae [Issue #1755](https://github.com/PHPOffice/PhpSpreadsheet/issues/1755) [PR #2028](https://github.com/PHPOffice/PhpSpreadsheet/pull/2028)
- Implemented URLENCODE() Web Function
- Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions.
- Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908)

View File

@ -1313,13 +1313,13 @@
<td style="text-align: center; color: orange;"></td>
<td style="text-align: center; color: orange;"></td>
<td></td>
<td></td>
<td style="text-align: center; color: orange;"></td>
<td></td>
<td></td>
<td></td>
<td style="text-align: center; color: orange;"></td>
<td style="text-align: center; color: orange;"></td>
<td></td>
<td style="text-align: center; color: orange;"></td>
<td></td>
<td></td>
<td></td>

View File

@ -2812,12 +2812,7 @@ parameters:
-
message: "#^Parameter \\#3 \\$subject of function preg_replace expects array\\|string, string\\|null given\\.$#"
count: 7
path: src/PhpSpreadsheet/Reader/Ods.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Ods\\:\\:convertToExcelAddressValue\\(\\) should return string but returns string\\|null\\.$#"
count: 1
count: 3
path: src/PhpSpreadsheet/Reader/Ods.php
-

View File

@ -11,8 +11,9 @@ use DOMNode;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter;
use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames;
use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings;
use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
@ -22,7 +23,6 @@ use PhpOffice\PhpSpreadsheet\Shared\Date;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Throwable;
use XMLReader;
use ZipArchive;
@ -304,8 +304,10 @@ class Ods extends BaseReader
$pageSettings->readStyleCrossReferences($dom);
// Content
$autoFilterReader = new AutoFilter($spreadsheet, $tableNs);
$definedNameReader = new DefinedNames($spreadsheet, $tableNs);
// Content
$spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body')
->item(0)
->getElementsByTagNameNS($officeNs, 'spreadsheet');
@ -642,8 +644,8 @@ class Ods extends BaseReader
++$worksheetID;
}
$this->readDefinedRanges($spreadsheet, $workbookData, $tableNs);
$this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs);
$autoFilterReader->read($workbookData);
$definedNameReader->read($workbookData);
}
$spreadsheet->setActiveSheetIndex(0);
@ -771,26 +773,6 @@ class Ods extends BaseReader
return $value;
}
private function convertToExcelAddressValue(string $openOfficeAddress): string
{
$excelAddress = $openOfficeAddress;
// Cell range 3-d reference
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
// and assume that the second worksheet reference is the same as the first
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress);
// Cell range reference in another sheet
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress);
// Cell reference in another sheet
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress);
// Cell range reference
$excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress);
// Simple cell reference
$excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress);
return $excelAddress;
}
private function convertToExcelFormulaValue(string $openOfficeFormula): string
{
$temp = explode('"', $openOfficeFormula);
@ -816,53 +798,4 @@ class Ods extends BaseReader
return $excelFormula;
}
/**
* Read any Named Ranges that are defined in this spreadsheet.
*/
private function readDefinedRanges(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void
{
$namedRanges = $workbookData->getElementsByTagNameNS($tableNs, 'named-range');
foreach ($namedRanges as $definedNameElement) {
$definedName = $definedNameElement->getAttributeNS($tableNs, 'name');
$baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address');
$range = $definedNameElement->getAttributeNS($tableNs, 'cell-range-address');
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
$range = $this->convertToExcelAddressValue($range);
$this->addDefinedName($spreadsheet, $baseAddress, $definedName, $range);
}
}
/**
* Read any Named Formulae that are defined in this spreadsheet.
*/
private function readDefinedExpressions(Spreadsheet $spreadsheet, DOMElement $workbookData, string $tableNs): void
{
$namedExpressions = $workbookData->getElementsByTagNameNS($tableNs, 'named-expression');
foreach ($namedExpressions as $definedNameElement) {
$definedName = $definedNameElement->getAttributeNS($tableNs, 'name');
$baseAddress = $definedNameElement->getAttributeNS($tableNs, 'base-cell-address');
$expression = $definedNameElement->getAttributeNS($tableNs, 'expression');
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
$expression = $this->convertToExcelFormulaValue($expression);
$this->addDefinedName($spreadsheet, $baseAddress, $definedName, $expression);
}
}
/**
* Assess scope and store the Defined Name.
*/
private function addDefinedName(Spreadsheet $spreadsheet, string $baseAddress, string $definedName, string $value): void
{
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true);
$worksheet = $spreadsheet->getSheetByName($sheetReference);
// Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
if ($worksheet !== null) {
$spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value));
}
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMElement;
use DOMNode;
class AutoFilter extends BaseReader
{
public function read(DOMElement $workbookData): void
{
$this->readAutoFilters($workbookData);
}
protected function readAutoFilters(DOMElement $workbookData): void
{
$databases = $workbookData->getElementsByTagNameNS($this->tableNs, 'database-ranges');
foreach ($databases as $autofilters) {
foreach ($autofilters->childNodes as $autofilter) {
$autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address');
if ($autofilterRange !== null) {
$baseAddress = $this->convertToExcelAddressValue($autofilterRange);
$this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress);
}
}
}
}
protected function getAttributeValue(?DOMNode $node, string $attributeName): ?string
{
if ($node !== null && $node->attributes !== null) {
$attribute = $node->attributes->getNamedItemNS(
$this->tableNs,
$attributeName
);
if ($attribute !== null) {
return $attribute->nodeValue;
}
}
return null;
}
}

View File

@ -0,0 +1,75 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMElement;
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
abstract class BaseReader
{
/**
* @var Spreadsheet
*/
protected $spreadsheet;
/**
* @var string
*/
protected $tableNs;
public function __construct(Spreadsheet $spreadsheet, string $tableNs)
{
$this->spreadsheet = $spreadsheet;
$this->tableNs = $tableNs;
}
abstract public function read(DOMElement $workbookData): void;
protected function convertToExcelAddressValue(string $openOfficeAddress): string
{
$excelAddress = $openOfficeAddress;
// Cell range 3-d reference
// As we don't support 3-d ranges, we're just going to take a quick and dirty approach
// and assume that the second worksheet reference is the same as the first
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress);
// Cell range reference in another sheet
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? '');
// Cell reference in another sheet
$excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? '');
// Cell range reference
$excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? '');
// Simple cell reference
$excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? '');
return $excelAddress ?? '';
}
protected function convertToExcelFormulaValue(string $openOfficeFormula): string
{
$temp = explode('"', $openOfficeFormula);
$tKey = false;
foreach ($temp as &$value) {
// @var string $value
// Only replace in alternate array entries (i.e. non-quoted blocks)
if ($tKey = !$tKey) {
// Cell range reference in another sheet
$value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value);
// Cell reference in another sheet
$value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? '');
// Cell range reference
$value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? '');
// Simple cell reference
$value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? '');
$value = Calculation::translateSeparator(';', ',', $value ?? '', $inBraces);
}
}
// Then rebuild the formula string
$excelFormula = implode('"', $temp);
return $excelFormula;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Reader\Ods;
use DOMElement;
use PhpOffice\PhpSpreadsheet\DefinedName;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class DefinedNames extends BaseReader
{
public function read(DOMElement $workbookData): void
{
$this->readDefinedRanges($workbookData);
$this->readDefinedExpressions($workbookData);
}
/**
* Read any Named Ranges that are defined in this spreadsheet.
*/
protected function readDefinedRanges(DOMElement $workbookData): void
{
$namedRanges = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-range');
foreach ($namedRanges as $definedNameElement) {
$definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name');
$baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address');
$range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address');
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
$range = $this->convertToExcelAddressValue($range);
$this->addDefinedName($baseAddress, $definedName, $range);
}
}
/**
* Read any Named Formulae that are defined in this spreadsheet.
*/
protected function readDefinedExpressions(DOMElement $workbookData): void
{
$namedExpressions = $workbookData->getElementsByTagNameNS($this->tableNs, 'named-expression');
foreach ($namedExpressions as $definedNameElement) {
$definedName = $definedNameElement->getAttributeNS($this->tableNs, 'name');
$baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address');
$expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression');
$baseAddress = $this->convertToExcelAddressValue($baseAddress);
$expression = $this->convertToExcelFormulaValue($expression);
$this->addDefinedName($baseAddress, $definedName, $expression);
}
}
/**
* Assess scope and store the Defined Name.
*/
private function addDefinedName(string $baseAddress, string $definedName, string $value): void
{
[$sheetReference] = Worksheet::extractSheetTitle($baseAddress, true);
$worksheet = $this->spreadsheet->getSheetByName($sheetReference);
// Worksheet might still be null if we're only loading selected sheets rather than the full spreadsheet
if ($worksheet !== null) {
$this->spreadsheet->addDefinedName(DefinedName::createInstance((string) $definedName, $worksheet, $value));
}
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class AutoFilters
{
/**
* @var XMLWriter
*/
private $objWriter;
/**
* @var Spreadsheet
*/
private $spreadsheet;
public function __construct(XMLWriter $objWriter, Spreadsheet $spreadsheet)
{
$this->objWriter = $objWriter;
$this->spreadsheet = $spreadsheet;
}
public function write(): void
{
$wrapperWritten = false;
$sheetCount = $this->spreadsheet->getSheetCount();
for ($i = 0; $i < $sheetCount; ++$i) {
$worksheet = $this->spreadsheet->getSheet($i);
$autofilter = $worksheet->getAutoFilter();
if ($autofilter !== null && !empty($autofilter->getRange())) {
if ($wrapperWritten === false) {
$this->objWriter->startElement('table:database-ranges');
$wrapperWritten = true;
}
$this->objWriter->startElement('table:database-range');
$this->objWriter->writeAttribute('table:orientation', 'column');
$this->objWriter->writeAttribute('table:display-filter-buttons', 'true');
$this->objWriter->writeAttribute(
'table:target-range-address',
$this->formatRange($worksheet, $autofilter)
);
$this->objWriter->endElement();
}
}
if ($wrapperWritten === true) {
$this->objWriter->endElement();
}
}
protected function formatRange(Worksheet $worksheet, Autofilter $autofilter): string
{
$title = $worksheet->getTitle();
$range = $autofilter->getRange();
return "'{$title}'.{$range}";
}
}

View File

@ -101,6 +101,7 @@ class Content extends WriterPart
$this->writeSheets($objWriter);
(new AutoFilters($objWriter, $this->getParentWriter()->getSpreadsheet()))->write();
// Defined names (ranges and formulae)
(new NamedExpressions($objWriter, $this->getParentWriter()->getSpreadsheet(), $this->formulaConvertor))->write();

View File

@ -0,0 +1,31 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Reader\Ods;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PHPUnit\Framework\TestCase;
class AutoFilterTest extends TestCase
{
/**
* @var Spreadsheet
*/
private $spreadsheet;
protected function setUp(): void
{
$filename = 'tests/data/Reader/Ods/AutoFilter.ods';
$reader = new Ods();
$this->spreadsheet = $reader->load($filename);
}
public function testAutoFilterRange(): void
{
$worksheet = $this->spreadsheet->getActiveSheet();
$autoFilterRange = $worksheet->getAutoFilter()->getRange();
self::assertSame('A1:C9', $autoFilterRange);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Writer\Ods;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class AutoFilterTest extends AbstractFunctional
{
public function testAutoFilterWriter(): void
{
$spreadsheet = new Spreadsheet();
$worksheet = $spreadsheet->getActiveSheet();
$dataSet = [
['Year', 'Quarter', 'Sales'],
[2020, 'Q1', 100],
[2020, 'Q2', 120],
[2020, 'Q3', 140],
[2020, 'Q4', 160],
[2021, 'Q1', 180],
[2021, 'Q2', 75],
[2021, 'Q3', 0],
[2021, 'Q4', 0],
];
$worksheet->fromArray($dataSet, null, 'A1');
$worksheet->getAutoFilter()->setRange('A1:C9');
$reloaded = $this->writeAndReload($spreadsheet, 'Ods');
self::assertSame('A1:C9', $reloaded->getActiveSheet()->getAutoFilter()->getRange());
}
}

Binary file not shown.