From b62de98bf70e42652a50dafd2c2aad6441459054 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 7 Jun 2022 20:22:33 +0200 Subject: [PATCH 001/156] New functionality to allow checking whether a row or column is "empty", with flags to allow variation in the definition of "empty" --- CHANGELOG.md | 4 + phpstan-baseline.neon | 10 -- src/PhpSpreadsheet/Worksheet/CellIterator.php | 4 + src/PhpSpreadsheet/Worksheet/Column.php | 57 ++++++- src/PhpSpreadsheet/Worksheet/Row.php | 41 ++++- src/PhpSpreadsheet/Worksheet/Worksheet.php | 60 +++++++ .../Worksheet/ColumnIteratorEmptyTest.php | 154 ++++++++++++++++++ .../Worksheet/RowIteratorEmptyTest.php | 154 ++++++++++++++++++ .../Worksheet/WorksheetTest.php | 98 +++++++++++ 9 files changed, 566 insertions(+), 16 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 290474a1..503f1121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) - Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) - Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) +- Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty": + - Treat rows/columns containing no cell records as empty (default) + - Treat cells containing a null value as empty + - Treat cells containing an empty string as empty ### Changed diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 33a3ea69..0a131911 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3655,11 +3655,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/CellIterator.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Column\\:\\:\\$parent \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Column.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\:\\:\\$color \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\|null\\.$#" count: 1 @@ -3685,11 +3680,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Row\\:\\:\\$worksheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Row.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\SheetView\\:\\:\\$sheetViewTypes has no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Worksheet/CellIterator.php b/src/PhpSpreadsheet/Worksheet/CellIterator.php index 142639ce..17286f9c 100644 --- a/src/PhpSpreadsheet/Worksheet/CellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/CellIterator.php @@ -12,6 +12,10 @@ use PhpOffice\PhpSpreadsheet\Collection\Cells; */ abstract class CellIterator implements Iterator { + public const TREAT_NULL_VALUE_AS_EMPTY_CELL = 1; + + public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 2; + /** * Worksheet to iterate. * diff --git a/src/PhpSpreadsheet/Worksheet/Column.php b/src/PhpSpreadsheet/Worksheet/Column.php index b6f30f1f..e6e332ae 100644 --- a/src/PhpSpreadsheet/Worksheet/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Column.php @@ -9,7 +9,7 @@ class Column * * @var Worksheet */ - private $parent; + private $worksheet; /** * Column index. @@ -23,10 +23,10 @@ class Column * * @param string $columnIndex */ - public function __construct(?Worksheet $parent = null, $columnIndex = 'A') + public function __construct(Worksheet $worksheet, $columnIndex = 'A') { // Set parent and column index - $this->parent = $parent; + $this->worksheet = $worksheet; $this->columnIndex = $columnIndex; } @@ -36,7 +36,7 @@ class Column public function __destruct() { // @phpstan-ignore-next-line - $this->parent = null; + $this->worksheet = null; } /** @@ -57,6 +57,53 @@ class Column */ public function getCellIterator($startRow = 1, $endRow = null) { - return new ColumnCellIterator($this->parent, $this->columnIndex, $startRow, $endRow); + return new ColumnCellIterator($this->worksheet, $this->columnIndex, $startRow, $endRow); + } + + /** + * Returns a boolean true if the column contains no cells. By default, this means that no cell records exist in the + * collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + + /** + * Returns bound worksheet. + */ + public function getWorksheet(): Worksheet + { + return $this->worksheet; } } diff --git a/src/PhpSpreadsheet/Worksheet/Row.php b/src/PhpSpreadsheet/Worksheet/Row.php index a5f8f326..5c162752 100644 --- a/src/PhpSpreadsheet/Worksheet/Row.php +++ b/src/PhpSpreadsheet/Worksheet/Row.php @@ -23,7 +23,7 @@ class Row * * @param int $rowIndex */ - public function __construct(?Worksheet $worksheet = null, $rowIndex = 1) + public function __construct(Worksheet $worksheet, $rowIndex = 1) { // Set parent and row index $this->worksheet = $worksheet; @@ -59,6 +59,45 @@ class Row return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn); } + /** + * Returns a boolean true if the row contains no cells. By default, this means that no cell records exist in the + * collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + /** * Returns bound worksheet. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 4a441a93..54785bfa 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3232,6 +3232,66 @@ class Worksheet implements IComparable return clone $this; } + /** + * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records + * exist in the collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new RowIterator($this, $rowId, $rowId); + $iterator->seek($rowId); + $row = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $row->isEmpty($definitionOfEmptyFlags); + } + + /** + * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records + * exist in the collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new ColumnIterator($this, $columnId, $columnId); + $iterator->seek($columnId); + $column = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $column->isEmpty($definitionOfEmptyFlags); + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php new file mode 100644 index 00000000..beb87028 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php @@ -0,0 +1,154 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('D2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('E2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('E3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('F2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('F3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('H3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H4', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyColumnBasic + */ + public function testIteratorEmptyColumn(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnBasic(): array + { + return [ + ['A', false], + ['B', true], + ['C', false], + ['D', false], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnNullAsEmpty + */ + public function testIteratorEmptyColumnWithNull(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnNullAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', false], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnEmptyStringAsEmpty + */ + public function testIteratorEmptyColumnWithEmptyString(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnEmptyStringAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', false], + ['D', true], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnNullAndEmptyStringAsEmpty + */ + public function testIteratorEmptyColumnWithNullAndEmptyString(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty( + CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + ); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnNullAndEmptyStringAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', true], + ['E', true], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php new file mode 100644 index 00000000..9bee6b59 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php @@ -0,0 +1,154 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B3', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('B4', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B5', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C5', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B6', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C6', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B7', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C7', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B8', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C8', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('D8', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyRowBasic + */ + public function testIteratorEmptyRow(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowBasic(): array + { + return [ + [1, false], + [2, true], + [3, false], + [4, false], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowNullAsEmpty + */ + public function testIteratorEmptyRowWithNull(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowNullAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, false], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowEmptyStringAsEmpty + */ + public function testIteratorEmptyRowWithEmptyString(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowEmptyStringAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, false], + [4, true], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowNullAndEmptyStringAsEmpty + */ + public function testIteratorEmptyRowWithNullAndEmptyString(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty( + CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + ); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowNullAndEmptyStringAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, true], + [5, true], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 5377444d..17de5c32 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -3,7 +3,9 @@ namespace PhpOffice\PhpSpreadsheetTests\Worksheet; use Exception; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\CellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -405,4 +407,100 @@ class WorksheetTest extends TestCase self::assertSame($expectedData, $worksheet->toArray()); self::assertSame($expectedHighestRow, $worksheet->getHighestRow()); } + + private static function getPopulatedSheetForEmptyRowTest(Spreadsheet $spreadsheet): Worksheet + { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B3', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('B4', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B5', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C5', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B6', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C6', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B7', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C7', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B8', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C8', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('D8', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + private static function getPopulatedSheetForEmptyColumnTest(Spreadsheet $spreadsheet): Worksheet + { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('D2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('E2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('E3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('F2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('F3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('H3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H4', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyRowProvider + */ + public function testIsEmptyRow(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheetForEmptyRowTest($spreadsheet); + + $isEmpty = $sheet->isEmptyRow($rowId, CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowProvider(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, true], + [5, true], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyColumnProvider + */ + public function testIsEmptyColumn(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheetForEmptyColumnTest($spreadsheet); + + $isEmpty = $sheet->isEmptyColumn($columnId, CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnProvider(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', true], + ['E', true], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } } From 391e6308d7956d3c76a10aa5ae5a02879de795df Mon Sep 17 00:00:00 2001 From: dgeppo <107109388+dgeppo@users.noreply.github.com> Date: Wed, 8 Jun 2022 11:24:56 +0200 Subject: [PATCH 002/156] Add removeCommentByColumnAndRow function --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 4a441a93..d7288aa4 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2581,6 +2581,22 @@ class Worksheet implements IComparable return $this; } + + + /** + * Remove comment. + * + * @param int $columnIndex Numeric column coordinate of the cell + * @param int $row Numeric row coordinate of the cell + * + */ + public function removeCommentByColumnAndRow($columnIndex, $row) + { + $pCellCoordinate = strtoupper(Coordinate::stringFromColumnIndex($columnIndex) . $row); + if(isset($this->comments[$pCellCoordinate])) { + unset($this->comments[$pCellCoordinate]); + } + } /** * Get comment for cell. From 13437384afd58acd64833cbd28fdf13070f82d2d Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:08:02 +0200 Subject: [PATCH 003/156] Change removeComment to use CellCoordinate --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 27 ++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d7288aa4..14d24265 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2581,22 +2581,31 @@ class Worksheet implements IComparable return $this; } - /** - * Remove comment. + * Remove comment from cell. * - * @param int $columnIndex Numeric column coordinate of the cell - * @param int $row Numeric row coordinate of the cell + * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * + * @return Comment */ - public function removeCommentByColumnAndRow($columnIndex, $row) + public function removeComment($cellCoordinate) { - $pCellCoordinate = strtoupper(Coordinate::stringFromColumnIndex($columnIndex) . $row); - if(isset($this->comments[$pCellCoordinate])) { - unset($this->comments[$pCellCoordinate]); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); + + if (Coordinate::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells.'); + } elseif (strpos($cellAddress, '$') !== false) { + throw new Exception('Cell coordinate string must not be absolute.'); + } elseif ($cellAddress == '') { + throw new Exception('Cell coordinate can not be zero-length string.'); } - } + // Check if we have a comment for this cell and delete it + if (isset($this->comments[$cellAddress])) { + unset($this->comments[$cellAddress]); + } + } /** * Get comment for cell. From 56ddbc4dd50533ebed3d2a224385f0a1ba80c0a2 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:08:52 +0200 Subject: [PATCH 004/156] Add testRemoveComment --- tests/PhpSpreadsheetTests/CommentTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index e58afad4..1d7825f4 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -83,4 +83,12 @@ class CommentTest extends TestCase $comment->setText($test); self::assertEquals('This is a test comment', (string) $comment); } + + public function testRemoveComment(): void { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getComment('A2')->getText()->createText('Comment to delete'); + $sheet->removeComment('A2'); + self::assertEmpty($sheet->getComments()); + } } From 52da0dc0fcc846a2bd53137f298a2f646b692b77 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:18:48 +0200 Subject: [PATCH 005/156] Add use PhpOffice\PhpSpreadsheet\Spreadsheet; --- tests/PhpSpreadsheetTests/CommentTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index 1d7825f4..7d06162a 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\TextElement; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Color; use PHPUnit\Framework\TestCase; From 801f5296e9fb2e4d8a30effc58abc0250831818a Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:21:18 +0200 Subject: [PATCH 006/156] Update Worksheet.php --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 14d24265..b3a0bf2a 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2590,7 +2590,7 @@ class Worksheet implements IComparable * * @return Comment */ - public function removeComment($cellCoordinate) + public function removeComment($cellCoordinate):void { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); From adf4531c997630fe6153d47ffd81a9899394787f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 9 Jun 2022 19:54:25 +0200 Subject: [PATCH 007/156] Adjust worksheet regexps for CellRef and Row/Column Ranges for improved handling of quoted worksheet names --- .../Calculation/Calculation.php | 10 ++++---- .../Calculation/ParseFormulaTest.php | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2bba56f6..c4d59d39 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -33,17 +33,17 @@ class Calculation // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) - const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative - const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; - const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; - const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; + const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\".(?:[^\"]|\"\")?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; + const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative // Cell ranges ensuring absolute/relative const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})'; // Defined Names: Named Range of cells, or Named Formulae - const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; + const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; diff --git a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php index 92330b7d..57b9271b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php @@ -162,6 +162,30 @@ class ParseFormulaTest extends TestCase ], '=B:C', ], + 'Combined Cell Reference and Column Range' => [ + [ + ['type' => 'Column Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'sheet1'!A1048576", 'reference' => "'sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Binary Operator', 'value' => '+','reference' => null], + ], + "=MIN('sheet1'!A:A) + 'sheet1'!A1", + ], + 'Combined Column Range and Cell Reference' => [ + [ + ['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'sheet1'!A1048576", 'reference' => "'sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Binary Operator', 'value' => '+','reference' => null], + ], + "='sheet1'!A1 + MIN('sheet1'!A:A)", + ], 'Range with Defined Names' => [ [ ['type' => 'Defined Name', 'value' => 'GROUP1', 'reference' => 'GROUP1'], From 189152ee078e15fcd2f94ad51063402de0c697b2 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 11 Jun 2022 14:32:29 +0200 Subject: [PATCH 008/156] Apply some coercive type-hinting --- phpstan-baseline.neon | 25 ----- .../Calculation/LookupRef/HLookup.php | 2 +- .../Calculation/LookupRef/VLookup.php | 2 +- src/PhpSpreadsheet/Shared/StringHelper.php | 101 ++++++------------ 4 files changed, 37 insertions(+), 93 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3cc183e2..5c1f074e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3115,26 +3115,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/StringHelper.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbIsUpper\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbIsUpper\\(\\) has parameter \\$character with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbStrSplit\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbStrSplit\\(\\) has parameter \\$string with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:sanitizeUTF8\\(\\) should return string but returns string\\|false\\.$#" count: 1 @@ -3160,11 +3140,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/StringHelper.php - - - message: "#^Variable \\$textValue on left side of \\?\\? always exists and is not nullable\\.$#" - count: 3 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - message: "#^Static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:validateTimeZone\\(\\) is unused\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index c7e87315..d67718ce 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -73,7 +73,7 @@ class HLookup extends LookupBase // break if we have passed possible keys $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData); - $cellDataLower = StringHelper::strToLower($rowData); + $cellDataLower = StringHelper::strToLower((string) $rowData); if ( $notExactMatch && diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 52d5a191..53a7badc 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -90,7 +90,7 @@ class VLookup extends LookupBase foreach ($lookupArray as $rowKey => $rowData) { $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData[$column]); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData[$column]); - $cellDataLower = StringHelper::strToLower($rowData[$column]); + $cellDataLower = StringHelper::strToLower((string) $rowData[$column]); // break if we have passed possible keys if ( diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 7d6a990f..2ccb2424 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -329,12 +329,8 @@ class StringHelper /** * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. - * - * @param string $textValue - * - * @return string */ - public static function sanitizeUTF8($textValue) + public static function sanitizeUTF8(string $textValue): string { if (self::getIsIconvEnabled()) { $textValue = @iconv('UTF-8', 'UTF-8', $textValue); @@ -349,12 +345,8 @@ class StringHelper /** * Check if a string contains UTF8 data. - * - * @param string $textValue - * - * @return bool */ - public static function isUTF8($textValue) + public static function isUTF8(string $textValue): bool { return $textValue === '' || preg_match('/^./su', $textValue) === 1; } @@ -364,10 +356,8 @@ class StringHelper * point as decimal separator in case locale is other than English. * * @param mixed $numericValue - * - * @return string */ - public static function formatNumber($numericValue) + public static function formatNumber($numericValue): string { if (is_float($numericValue)) { return str_replace(',', '.', $numericValue); @@ -385,10 +375,8 @@ class StringHelper * * @param string $textValue UTF-8 encoded string * @param mixed[] $arrcRuns Details of rich text runs in $value - * - * @return string */ - public static function UTF8toBIFF8UnicodeShort($textValue, $arrcRuns = []) + public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -419,10 +407,8 @@ class StringHelper * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function UTF8toBIFF8UnicodeLong($textValue) + public static function UTF8toBIFF8UnicodeLong(string $textValue): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -436,13 +422,10 @@ class StringHelper /** * Convert string from one encoding to another. * - * @param string $textValue * @param string $to Encoding to convert to, e.g. 'UTF-8' * @param string $from Encoding to convert from, e.g. 'UTF-16LE' - * - * @return string */ - public static function convertEncoding($textValue, $to, $from) + public static function convertEncoding(string $textValue, string $to, string $from): string { if (self::getIsIconvEnabled()) { $result = iconv($from, $to . self::$iconvOptions, $textValue); @@ -457,52 +440,45 @@ class StringHelper /** * Get character count. * - * @param string $textValue * @param string $encoding Encoding * * @return int Character count */ - public static function countCharacters($textValue, $encoding = 'UTF-8') + public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int { - return mb_strlen($textValue ?? '', $encoding); + return mb_strlen($textValue, $encoding); } /** * Get a substring of a UTF-8 encoded string. * - * @param null|string $textValue UTF-8 encoded string + * @param string $textValue UTF-8 encoded string * @param int $offset Start offset * @param int $length Maximum number of characters in substring - * - * @return string */ - public static function substring($textValue, $offset, $length = 0) + public static function substring(string $textValue, int $offset, int $length = 0): string { - return mb_substr($textValue ?? '', $offset, $length, 'UTF-8'); + return mb_substr($textValue, $offset, $length, 'UTF-8'); } /** * Convert a UTF-8 encoded string to upper case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToUpper($textValue) + public static function strToUpper(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_UPPER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8'); } /** * Convert a UTF-8 encoded string to lower case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToLower($textValue) + public static function strToLower(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_LOWER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_LOWER, 'UTF-8'); } /** @@ -510,24 +486,27 @@ class StringHelper * (uppercase every first character in each word, lower case all other characters). * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToTitle($textValue) + public static function strToTitle(string $textValue): string { return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8'); } - public static function mbIsUpper($character) + public static function mbIsUpper(string $character): bool { - return mb_strtolower($character, 'UTF-8') != $character; + return mb_strtolower($character, 'UTF-8') !== $character; } - public static function mbStrSplit($string) + /** + * Splits a UTF-8 string into an array of individual characters. + */ + public static function mbStrSplit(string $string): array { // Split at all position not after the start: ^ // and not before the end: $ - return preg_split('/(? Date: Sun, 12 Jun 2022 19:21:43 +0200 Subject: [PATCH 009/156] Apply some coercive type-hinting --- .../Calculation/Calculation.php | 2 +- src/PhpSpreadsheet/Calculation/Functions.php | 2 +- .../Calculation/Information/ErrorValue.php | 2 +- .../Calculation/Information/ExcelError.php | 20 +++++++++---------- src/PhpSpreadsheet/Cell/DataType.php | 4 ++-- src/PhpSpreadsheet/ReferenceHelper.php | 6 +++--- src/PhpSpreadsheet/Shared/TimeZone.php | 10 +++++----- src/PhpSpreadsheet/Writer/Html.php | 2 +- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2bba56f6..588841dc 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3088,7 +3088,7 @@ class Calculation } // Test whether we have any language data for this language (any locale) - if (in_array($language, self::$validLocaleLanguages)) { + if (in_array($language, self::$validLocaleLanguages, true)) { // initialise language/locale settings self::$localeFunctions = []; self::$localeArgumentSeparator = ','; diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 1cc980b8..ddd3e200 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -152,7 +152,7 @@ class Functions if ($condition === '') { return '=""'; } - if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) { + if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) { $condition = self::operandSpecialHandling($condition); if (is_bool($condition)) { return '=' . ($condition ? 'TRUE' : 'FALSE'); diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index cffad6a6..869350ed 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes) || $value === ExcelError::CALC(); + return in_array($value, ExcelError::$errorCodes, true) || $value === ExcelError::CALC(); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 585dfdc8..88de7e54 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -11,7 +11,7 @@ class ExcelError /** * List of error codes. * - * @var array + * @var array */ public static $errorCodes = [ 'null' => '#NULL!', @@ -60,7 +60,7 @@ class ExcelError * * @return string #NULL! */ - public static function null() + public static function null(): string { return self::$errorCodes['null']; } @@ -72,7 +72,7 @@ class ExcelError * * @return string #NUM! */ - public static function NAN() + public static function NAN(): string { return self::$errorCodes['num']; } @@ -84,7 +84,7 @@ class ExcelError * * @return string #REF! */ - public static function REF() + public static function REF(): string { return self::$errorCodes['reference']; } @@ -100,7 +100,7 @@ class ExcelError * * @return string #N/A! */ - public static function NA() + public static function NA(): string { return self::$errorCodes['na']; } @@ -112,7 +112,7 @@ class ExcelError * * @return string #VALUE! */ - public static function VALUE() + public static function VALUE(): string { return self::$errorCodes['value']; } @@ -124,7 +124,7 @@ class ExcelError * * @return string #NAME? */ - public static function NAME() + public static function NAME(): string { return self::$errorCodes['name']; } @@ -134,7 +134,7 @@ class ExcelError * * @return string #DIV/0! */ - public static function DIV0() + public static function DIV0(): string { return self::$errorCodes['divisionbyzero']; } @@ -142,9 +142,9 @@ class ExcelError /** * CALC. * - * @return string #Not Yet Implemented + * @return string #CALC! */ - public static function CALC() + public static function CALC(): string { return '#CALC!'; } diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 390dcde5..16de2a00 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -21,7 +21,7 @@ class DataType /** * List of error codes. * - * @var array + * @var array */ private static $errorCodes = [ '#NULL!' => 0, @@ -36,7 +36,7 @@ class DataType /** * Get list of error codes. * - * @return array + * @return array */ public static function getErrorCodes() { diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 046c5894..36c85edf 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -399,7 +399,7 @@ class ReferenceHelper return $highestColumn . $row; }, range(1, $highestRow)), function ($coordinate) use ($allCoordinates) { - return !in_array($coordinate, $allCoordinates); + return in_array($coordinate, $allCoordinates, true) === false; } ); @@ -929,7 +929,7 @@ class ReferenceHelper $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; $worksheet->removeConditionalStyles($coordinate); if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); $worksheet->getCell($coordinate)->setXfIndex(0); } } @@ -945,7 +945,7 @@ class ReferenceHelper $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; $worksheet->removeConditionalStyles($coordinate); if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); $worksheet->getCell($coordinate)->setXfIndex(0); } } diff --git a/src/PhpSpreadsheet/Shared/TimeZone.php b/src/PhpSpreadsheet/Shared/TimeZone.php index dabb88f2..734c076d 100644 --- a/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/src/PhpSpreadsheet/Shared/TimeZone.php @@ -21,9 +21,9 @@ class TimeZone * * @return bool Success or failure */ - private static function validateTimeZone($timezoneName) + private static function validateTimeZone(string $timezoneName): bool { - return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)); + return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true); } /** @@ -33,7 +33,7 @@ class TimeZone * * @return bool Success or failure */ - public static function setTimeZone($timezoneName) + public static function setTimeZone(string $timezoneName): bool { if (self::validateTimezone($timezoneName)) { self::$timezone = $timezoneName; @@ -49,7 +49,7 @@ class TimeZone * * @return string Timezone (e.g. 'Europe/London') */ - public static function getTimeZone() + public static function getTimeZone(): string { return self::$timezone; } @@ -63,7 +63,7 @@ class TimeZone * * @return int Number of seconds for timezone adjustment */ - public static function getTimeZoneAdjustment($timezoneName, $timestamp) + public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int { $timezoneName = $timezoneName ?? self::$timezone; $dtobj = Date::dateTimeFromTimestamp("$timestamp"); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 3ef65928..da32025a 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1744,7 +1744,7 @@ class Html extends BaseWriter while ($c++ < $e) { $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; - if (!in_array($baseCell, $adjustedBaseCells)) { + if (!in_array($baseCell, $adjustedBaseCells, true)) { // subtract rowspan by 1 --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; $adjustedBaseCells[] = $baseCell; From 861b955b3b39cee6b7f4a373d8d6359b56f3dd84 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:12:08 +0200 Subject: [PATCH 010/156] Add feature removeComment Use $cellCoordinate as argument Return $this to support the fluent interface --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index b3a0bf2a..b9a168c2 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2588,9 +2588,9 @@ class Worksheet implements IComparable * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * - * @return Comment + * @return $this */ - public function removeComment($cellCoordinate):void + public function removeComment($cellCoordinate) { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); @@ -2605,6 +2605,8 @@ class Worksheet implements IComparable if (isset($this->comments[$cellAddress])) { unset($this->comments[$cellAddress]); } + + return $this; } /** From 7f62fba7ef95891967a99e745e71bfac689a2fc4 Mon Sep 17 00:00:00 2001 From: Dams <107109388+dgeppo@users.noreply.github.com> Date: Tue, 14 Jun 2022 10:44:42 +0200 Subject: [PATCH 011/156] Update the method testRemoveComment Adding test to check if comment exists before delete it --- tests/PhpSpreadsheetTests/CommentTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index 7d06162a..e55fe9cc 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -89,6 +89,7 @@ class CommentTest extends TestCase $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->getComment('A2')->getText()->createText('Comment to delete'); + self::assertArrayHasKey('A2',$sheet->getComments()); $sheet->removeComment('A2'); self::assertEmpty($sheet->getComments()); } From 11a94dabec2ed0085dc3c033fa615b6b388bc363 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 14 Jun 2022 12:01:08 +0200 Subject: [PATCH 012/156] Potential improvements for insert/delete column/row for performance testing --- src/PhpSpreadsheet/ReferenceHelper.php | 38 ++++++++++++++------------ 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 36c85edf..2a349607 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -367,7 +367,6 @@ class ReferenceHelper Worksheet $worksheet ): void { $remove = ($numberOfColumns < 0 || $numberOfRows < 0); - $allCoordinates = $worksheet->getCoordinates(); if ( $this->cellReferenceHelper === null || @@ -394,12 +393,13 @@ class ReferenceHelper } // Find missing coordinates. This is important when inserting column before the last column + $cellCollection = $worksheet->getCellCollection(); $missingCoordinates = array_filter( array_map(function ($row) use ($highestColumn) { return $highestColumn . $row; }, range(1, $highestRow)), - function ($coordinate) use ($allCoordinates) { - return in_array($coordinate, $allCoordinates, true) === false; + function ($coordinate) use ($cellCollection) { + return $cellCollection->has($coordinate) === false; } ); @@ -408,16 +408,15 @@ class ReferenceHelper foreach ($missingCoordinates as $coordinate) { $worksheet->createNewCell($coordinate); } - - // Refresh all coordinates - $allCoordinates = $worksheet->getCoordinates(); } - // Loop through cells, bottom-up, and change cell coordinate + $allCoordinates = $worksheet->getCoordinates(); if ($remove) { // It's faster to reverse and pop than to use unshift, especially with large cell collections $allCoordinates = array_reverse($allCoordinates); } + + // Loop through cells, bottom-up, and change cell coordinate while ($coordinate = array_pop($allCoordinates)) { $cell = $worksheet->getCell($coordinate); $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); @@ -927,11 +926,7 @@ class ReferenceHelper for ($i = 1; $i <= $highestRow - 1; ++$i) { for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } + $this->clearStripCell($worksheet, $coordinate); } } } @@ -943,15 +938,24 @@ class ReferenceHelper for ($i = $beforeColumn - 1; $i <= $lastColumnIndex; ++$i) { for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } + $this->clearStripCell($worksheet, $coordinate); } } } + private function clearStripCell(Worksheet $worksheet, string $coordinate) + { + // TODO - Should also clear down comments, but wait until after comment removal PR-2875 is merged + $worksheet->removeConditionalStyles($coordinate); + $worksheet->setHyperlink($coordinate); + $worksheet->setDataValidation($coordinate); + + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void { $autoFilter = $worksheet->getAutoFilter(); From 4d2b00dafc9d0015084354e9a064d4a1515ed88f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 14 Jun 2022 12:19:54 +0200 Subject: [PATCH 013/156] phpcs fxes --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- tests/PhpSpreadsheetTests/CommentTest.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index b9a168c2..c8236993 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2605,7 +2605,7 @@ class Worksheet implements IComparable if (isset($this->comments[$cellAddress])) { unset($this->comments[$cellAddress]); } - + return $this; } diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index e55fe9cc..aacd538f 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -84,13 +84,14 @@ class CommentTest extends TestCase $comment->setText($test); self::assertEquals('This is a test comment', (string) $comment); } - - public function testRemoveComment(): void { + + public function testRemoveComment(): void + { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->getComment('A2')->getText()->createText('Comment to delete'); - self::assertArrayHasKey('A2',$sheet->getComments()); + self::assertArrayHasKey('A2', $sheet->getComments()); $sheet->removeComment('A2'); self::assertEmpty($sheet->getComments()); - } + } } From 8e31dbaabe2c69aefa6d7cee4db14f56e4688209 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 14 Jun 2022 12:21:44 +0200 Subject: [PATCH 014/156] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 290474a1..2c65f4ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) - Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) - Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) From 90bdc7c12e709f5603e4003a07605aa4b8143cce Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 14 Jun 2022 08:33:36 -0700 Subject: [PATCH 015/156] Test For Excel File Saved With Ribbon Data (#2883) File from https://www.rondebruin.nl/win/s2/win003.htm. I have been in conversation with the author, who has no objection to its use. I have not actually opened the file in Excel (at least not with macros enabled); I am using it merely to demonstrate that the ribbon data is read and written correctly. Test added; no source code changed. This should slightly increase coverage for Reader/Xlsx (moderate), Writer/Xlsx (slight), and Spreadsheet (substantial). Note that this file has no Ribbon Bin objects, so some coverage is still lacking. --- .../Reader/Xlsx/RibbonTest.php | 47 ++++++++++++++++++ tests/data/Reader/XLSX/ribbon.donotopen.zip | Bin 0 -> 15340 bytes 2 files changed, 47 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php create mode 100644 tests/data/Reader/XLSX/ribbon.donotopen.zip diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php new file mode 100644 index 00000000..197ad47f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -0,0 +1,47 @@ +load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen($data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + self::assertNull($spreadsheet->getRibbonBinObjects()); + self::assertNull($spreadsheet->getRibbonBinObjects('names')); + self::assertNull($spreadsheet->getRibbonBinObjects('data')); + self::assertEmpty($spreadsheet->getRibbonBinObjects('types')); + self::assertNull($spreadsheet->getRibbonBinObjects('xxxxx')); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertSame($vbaCode, $reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/ribbon.donotopen.zip b/tests/data/Reader/XLSX/ribbon.donotopen.zip new file mode 100644 index 0000000000000000000000000000000000000000..97d58bdcf63e278ecdd03bdcd454e39fc95f14e6 GIT binary patch literal 15340 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 literal 0 HcmV?d00001 From 04f46676584abcc144a441aa516a8e2a2035eb1b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 14 Jun 2022 08:45:12 -0700 Subject: [PATCH 016/156] Expand Chart Support for schemeClr and prstClr (#2879) * Expand Chart Support for schemeClr and prstClr Fix #2219. Address some, but not all, issues mentioned in #2863. For Pie Charts, fill color is stored in XML as part of dPt node, which had been ignored by Reader/Xlsx/Chart. Add support for it, including when specified as schemeClr or prstClr rather than srgbClr. Add support for prstClr in other cases where schemeClr is supported. * Update Change Log Add this PR. --- CHANGELOG.md | 2 +- samples/templates/32readwriteAreaChart4.xlsx | Bin 0 -> 12474 bytes src/PhpSpreadsheet/Chart/DataSeriesValues.php | 25 ++- src/PhpSpreadsheet/Chart/Properties.php | 5 + src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 90 ++++++---- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 34 +++- .../Chart/Charts32XmlTest.php | 95 ++++++----- .../PhpSpreadsheetTests/Chart/PieFillTest.php | 160 ++++++++++++++++++ 8 files changed, 325 insertions(+), 86 deletions(-) create mode 100644 samples/templates/32readwriteAreaChart4.xlsx create mode 100644 tests/PhpSpreadsheetTests/Chart/PieFillTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a12675..4664f049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) -- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) ## 1.23.0 - 2022-04-24 diff --git a/samples/templates/32readwriteAreaChart4.xlsx b/samples/templates/32readwriteAreaChart4.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4548bf2e0dec273a37ceaf11b27e1a914c8978c5 GIT binary patch literal 12474 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 U5@^0Zw_jus05G`}qyIVjKiJn^X8-^I literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 6747934a..0a2f5a85 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -76,6 +76,9 @@ class DataSeriesValues /** @var string */ private $schemeClr = ''; + /** @var string */ + private $prstClr = ''; + /** * Line Width. * @@ -262,7 +265,7 @@ class DataSeriesValues /** * Set fill color for series. * - * @param null|string|string[] $color HEX color or array with HEX colors + * @param string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ @@ -270,10 +273,14 @@ class DataSeriesValues { if (is_array($color)) { foreach ($color as $colorValue) { - $this->validateColor($colorValue); + if (substr($colorValue, 0, 1) !== '*' && substr($colorValue, 0, 1) !== '/') { + $this->validateColor($colorValue); + } } } else { - $this->validateColor("$color"); + if (substr($color, 0, 1) !== '*' && substr($color, 0, 1) !== '/') { + $this->validateColor("$color"); + } } $this->fillColor = $color; @@ -470,4 +477,16 @@ class DataSeriesValues return $this; } + + public function getPrstClr(): string + { + return $this->prstClr; + } + + public function setPrstClr(string $prstClr): self + { + $this->prstClr = $prstClr; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 01a83915..6db04809 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -14,6 +14,11 @@ abstract class Properties EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; + const EXCEL_COLOR_TYPES = [ + self::EXCEL_COLOR_TYPE_ARGB, + self::EXCEL_COLOR_TYPE_SCHEME, + self::EXCEL_COLOR_TYPE_STANDARD, + ]; const AXIS_LABELS_LOW = 'low'; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 55a150b7..8e3d6386 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -359,7 +359,9 @@ class Chart $pointSize = null; $noFill = false; $schemeClr = ''; + $prstClr = ''; $bubble3D = false; + $dPtColors = []; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -383,7 +385,24 @@ class Chart $noFill = true; } if (isset($children->solidFill)) { - $this->readColor($children->solidFill, $srgbClr, $schemeClr); + $this->readColor($children->solidFill, $srgbClr, $schemeClr, $prstClr); + } + + break; + case 'dPt': + $dptIdx = (int) self::getAttribute($seriesDetail->idx, 'val', 'string'); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $arrayColors = $this->readColor($children->solidFill); + if ($arrayColors['type'] === 'srgbClr') { + $dptColors[$dptIdx] = $arrayColors['value']; + } elseif ($arrayColors['type'] === 'prstClr') { + $dptColors[$dptIdx] = '/' . $arrayColors['value']; + } else { + $dptColors[$dptIdx] = '*' . $arrayColors['value']; + } + } } break; @@ -394,7 +413,7 @@ class Chart if (count($seriesDetail->spPr) === 1) { $ln = $seriesDetail->spPr->children($this->aNamespace); if (isset($ln->solidFill)) { - $this->readColor($ln->solidFill, $srgbClr, $schemeClr); + $this->readColor($ln->solidFill, $srgbClr, $schemeClr, $prstClr); } } @@ -461,6 +480,16 @@ class Chart if (isset($seriesValues[$seriesIndex])) { $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); } + } elseif ($prstClr) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setPrstClr($prstClr); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setPrstClr($prstClr); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setPrstClr($prstClr); + } } if ($bubble3D) { if (isset($seriesLabel[$seriesIndex])) { @@ -473,6 +502,17 @@ class Chart $seriesValues[$seriesIndex]->setBubble3D($bubble3D); } } + if (!empty($dptColors)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setFillColor($dptColors); + } + } } } /** @phpstan-ignore-next-line */ @@ -1001,39 +1041,31 @@ class Chart 'innerShdw', ]; - private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null): array + private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null, ?string &$prstClr = null): array { $result = [ 'type' => null, 'value' => null, 'alpha' => null, ]; - if (isset($colorXml->srgbClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_ARGB; - $result['value'] = $srgbClr = self::getAttribute($colorXml->srgbClr, 'val', 'string'); - if (isset($colorXml->srgbClr->alpha)) { - /** @var string */ - $alpha = self::getAttribute($colorXml->srgbClr->alpha, 'val', 'string'); - $alpha = Properties::alphaFromXml($alpha); - $result['alpha'] = $alpha; - } - } elseif (isset($colorXml->schemeClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_SCHEME; - $result['value'] = $schemeClr = self::getAttribute($colorXml->schemeClr, 'val', 'string'); - if (isset($colorXml->schemeClr->alpha)) { - /** @var string */ - $alpha = self::getAttribute($colorXml->schemeClr->alpha, 'val', 'string'); - $alpha = Properties::alphaFromXml($alpha); - $result['alpha'] = $alpha; - } - } elseif (isset($colorXml->prstClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_STANDARD; - $result['value'] = self::getAttribute($colorXml->prstClr, 'val', 'string'); - if (isset($colorXml->prstClr->alpha)) { - /** @var string */ - $alpha = self::getAttribute($colorXml->prstClr->alpha, 'val', 'string'); - $alpha = Properties::alphaFromXml($alpha); - $result['alpha'] = $alpha; + foreach (Properties::EXCEL_COLOR_TYPES as $type) { + if (isset($colorXml->$type)) { + $result['type'] = $type; + $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string'); + if ($type === Properties::EXCEL_COLOR_TYPE_ARGB) { + $srgbClr = $result['value']; + } elseif ($type === Properties::EXCEL_COLOR_TYPE_SCHEME) { + $schemeClr = $result['value']; + } elseif ($type === Properties::EXCEL_COLOR_TYPE_STANDARD) { + $prstClr = $result['value']; + } + if (isset($colorXml->$type->alpha)) { + $alpha = (int) self::getAttribute($colorXml->$type->alpha, 'val', 'string'); + $alpha = 100 - (int) ($alpha / 1000); + $result['alpha'] = $alpha; + } + + break; } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index e24afbac..dda395ac 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -942,6 +942,9 @@ class Chart extends WriterPart */ private function writePlotSeriesValuesElement(XMLWriter $objWriter, $val = 3, $fillColor = 'FF9900'): void { + if ($fillColor === '') { + return; + } $objWriter->startElement('c:dPt'); $objWriter->startElement('c:idx'); $objWriter->writeAttribute('val', $val); @@ -953,8 +956,16 @@ class Chart extends WriterPart $objWriter->startElement('c:spPr'); $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); + if (substr($fillColor, 0, 1) === '*') { + $objWriter->startElement('a:schemeClr'); + $objWriter->writeAttribute('val', substr($fillColor, 1)); + } elseif (substr($fillColor, 0, 1) === '/') { + $objWriter->startElement('a:prstClr'); + $objWriter->writeAttribute('val', substr($fillColor, 1)); + } else { + $objWriter->startElement('a:srgbClr'); + $objWriter->writeAttribute('val', $fillColor); + } $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); @@ -1039,7 +1050,7 @@ class Chart extends WriterPart $fillColorValues = $plotSeriesValues->getFillColor(); if ($fillColorValues !== null && is_array($fillColorValues)) { foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { - $this->writePlotSeriesValuesElement($objWriter, $dataKey, ($fillColorValues[$dataKey] ?? 'FF9900')); + $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? ''); } } else { $this->writePlotSeriesValuesElement($objWriter); @@ -1061,7 +1072,7 @@ class Chart extends WriterPart $groupType == DataSeries::TYPE_LINECHART || $groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) - || ($plotSeriesValues !== false && $plotSeriesValues->getSchemeClr()) + || ($plotSeriesValues !== false && ($plotSeriesValues->getSchemeClr() || $plotSeriesValues->getPrstClr())) ) { $plotLineWidth = 12700; if ($plotSeriesValues) { @@ -1069,10 +1080,21 @@ class Chart extends WriterPart } $objWriter->startElement('c:spPr'); - $schemeClr = $plotLabel ? $plotLabel->getSchemeClr() : null; + $schemeClr = $typeClr = ''; + if ($plotLabel) { + $schemeClr = $plotLabel->getSchemeClr(); + if ($schemeClr) { + $typeClr = 'schemeClr'; + } else { + $schemeClr = $plotLabel->getPrstClr(); + if ($schemeClr) { + $typeClr = 'prstClr'; + } + } + } if ($schemeClr) { $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:schemeClr'); + $objWriter->startElement("a:$typeClr"); $objWriter->writeAttribute('val', $schemeClr); $objWriter->endElement(); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php index a193ca7d..3123278f 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; -use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; use PHPUnit\Framework\TestCase; @@ -13,17 +12,6 @@ class Charts32XmlTest extends TestCase // These tests can only be performed by examining xml. private const DIRECTORY = 'samples' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR; - /** @var string */ - private $outputFileName = ''; - - protected function tearDown(): void - { - if ($this->outputFileName !== '') { - unlink($this->outputFileName); - $this->outputFileName = ''; - } - } - /** * @dataProvider providerScatterCharts */ @@ -33,25 +21,21 @@ class Charts32XmlTest extends TestCase $reader = new XlsxReader(); $reader->setIncludeCharts(true); $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart2.xml'; - $data = file_get_contents($file); - // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } else { - self::assertSame(1, substr_count($data, '')); - self::assertSame($expectedCount, substr_count($data, '')); - } + self::assertSame(1, substr_count($data, '')); + self::assertSame($expectedCount, substr_count($data, '')); } public function providerScatterCharts(): array @@ -69,23 +53,20 @@ class Charts32XmlTest extends TestCase $reader = new XlsxReader(); $reader->setIncludeCharts(true); $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart1.xml'; - $data = file_get_contents($file); // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } else { - self::assertSame(0, substr_count($data, '')); - } + self::assertSame(0, substr_count($data, '')); } /** @@ -116,18 +97,11 @@ class Charts32XmlTest extends TestCase $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart2.xml'; - $data = file_get_contents($file); - // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } elseif ($numeric === true) { + if ($numeric === true) { self::assertSame(0, substr_count($data, '')); self::assertSame(2, substr_count($data, '')); } else { @@ -144,4 +118,31 @@ class Charts32XmlTest extends TestCase [null], ]; } + + public function testAreaPrstClr(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/PieFillTest.php b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php new file mode 100644 index 00000000..452fcb93 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php @@ -0,0 +1,160 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testPieFill(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + // Custom colors for dataSeries (gray, blue, red, orange) + $colors = [ + 'cccccc', + '*accent1', // use schemeClr, was '00abb8', + '/green', // use prstClr, was 'b8292f', + 'eb8500', + ]; + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels1 = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + 'Worksheet!$C$1', + null, + 1 + ), // 2011 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + // Custom Colors + $dataSeriesValues1Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4); + $dataSeriesValues1Element->setFillColor($colors); + $dataSeriesValues1 = [$dataSeriesValues1Element]; + + // Build the dataseries + $series1 = new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ); + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + $layout1->setShowPercent(true); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, [$series1]); + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // no Y-Axis for Pie Chart + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + $dataSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $dataSeries2); + $plotValues = $dataSeries2[0]->getPlotValues(); + self::assertCount(1, $plotValues); + $fillColors = $plotValues[0]->getFillColor(); + self::assertSame($colors, $fillColors); + + $writer = new XlsxWriter($reloadedSpreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart2); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(4, substr_count($data, '')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} From 09c66ab3029dd072ef8de8c8b809716706efc4d0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 11:19:02 +0200 Subject: [PATCH 017/156] Use column address rather than index for remove column loop to eliminate extra math and repeated calls to stringFromColumnIndex() --- src/PhpSpreadsheet/ReferenceHelper.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 2a349607..8caaab18 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -923,9 +923,12 @@ class ReferenceHelper private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void { - for ($i = 1; $i <= $highestRow - 1; ++$i) { - for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns); + $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + + for ($row = 1; $row <= $highestRow - 1; ++$row) { + for ($column = $startColumnId; $column !== $endColumnId; ++$column) { + $coordinate = $column . $row; $this->clearStripCell($worksheet, $coordinate); } } @@ -933,22 +936,23 @@ class ReferenceHelper private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void { - $lastColumnIndex = Coordinate::columnIndexFromString($highestColumn) - 1; + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + ++$highestColumn; - for ($i = $beforeColumn - 1; $i <= $lastColumnIndex; ++$i) { - for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; + for ($column = $startColumnId; $column !== $highestColumn; ++$column) { + for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) { + $coordinate = $column . $row; $this->clearStripCell($worksheet, $coordinate); } } } - private function clearStripCell(Worksheet $worksheet, string $coordinate) + private function clearStripCell(Worksheet $worksheet, string $coordinate): void { - // TODO - Should also clear down comments, but wait until after comment removal PR-2875 is merged $worksheet->removeConditionalStyles($coordinate); $worksheet->setHyperlink($coordinate); $worksheet->setDataValidation($coordinate); + $worksheet->removeComment($coordinate); if ($worksheet->cellExists($coordinate)) { $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); From f51f4bc0ea37124f16055591b7287ee7ce9ce1da Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 12:29:47 +0200 Subject: [PATCH 018/156] Update change log and documentation --- CHANGELOG.md | 1 + docs/topics/recipes.md | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a12675..67359728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Memory and speed improvements, particularly for the Cell Collection, and the Writers. See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions +- Improved performance for removing rows/columns from a worksheet ### Deprecated diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index f25a9119..e4313382 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1348,7 +1348,7 @@ Removing a merge can be done using the unmergeCells method: $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` -## Inserting rows/columns +## Inserting or Removing rows/columns You can insert/remove rows/columns at a specific position. The following code inserts 2 new rows, right before row 7: @@ -1356,6 +1356,23 @@ code inserts 2 new rows, right before row 7: ```php $spreadsheet->getActiveSheet()->insertNewRowBefore(7, 2); ``` +while +```php +$spreadsheet->getActiveSheet()->removeRow(7, 2); +``` +will remove 2 rows starting at row number 7 (ie. rows 7 and 8). + +Equivalent methods exist for inserting/removing columns: + +```php +$spreadsheet->getActiveSheet()->removeColumn('C', 2); +``` + +All subsequent rows (or columns) will be moved to allow the insertion (or removal) with all formulae referencing thise cells adjusted accordingly. + +Note that this is a fairly intensive process, particularly with large worksheets, and especially if you are inserting/removing rows/columns from near beginning of the worksheet. + +If you need to insert/remove several consecutive rows/columns, always use the second argument rather than making multiple calls to insert/remove a single row/column if possible. ## Add a drawing to a worksheet From 3b55689ec17f27ce3fdb2fceb3126b6e6506eeb0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 13:33:42 +0200 Subject: [PATCH 019/156] phpcs update to version 3.7.0 to ensure it catches all the PHP 8.1 updates --- composer.json | 2 +- composer.lock | 176 +++----------------------------------------------- 2 files changed, 10 insertions(+), 168 deletions(-) diff --git a/composer.json b/composer.json index 4cf5e77d..f0c40cf8 100644 --- a/composer.json +++ b/composer.json @@ -87,7 +87,7 @@ "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.6", + "squizlabs/php_codesniffer": "^3.7", "tecnickcom/tcpdf": "^6.4" }, "suggest": { diff --git a/composer.lock b/composer.lock index ee0d79d1..50011b16 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e09b120ad90fc0fbbc055e1f341910e", + "content-hash": "fa9fa4814df8320d600551ca8ec11883", "packages": [ { "name": "ezyang/htmlpurifier", @@ -279,10 +279,6 @@ "keywords": [ "enum" ], - "support": { - "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.3" - }, "funding": [ { "url": "https://github.com/mnapoli", @@ -811,12 +807,12 @@ "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", "shasum": "" }, @@ -830,7 +826,6 @@ "php-parallel-lint/php-parallel-lint": "^1.3.1", "phpcompatibility/php-compatibility": "^9.0" }, - "default-branch": true, "type": "composer-plugin", "extra": { "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" @@ -999,10 +994,6 @@ "constructor", "instantiate" ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1162,10 +1153,6 @@ ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", - "support": { - "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v1.2.2" - }, "time": "2022-04-27T13:50:54+00:00" }, { @@ -1366,11 +1353,6 @@ "php", "utf-8" ], - "support": { - "docs": "http://mpdf.github.io", - "issues": "https://github.com/mpdf/mpdf/issues", - "source": "https://github.com/mpdf/mpdf" - }, "funding": [ { "url": "https://www.paypal.me/mpdf", @@ -1426,10 +1408,6 @@ "object", "object graph" ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" - }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", @@ -1488,10 +1466,6 @@ "parser", "php" ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" - }, "time": "2021-11-30T19:35:32+00:00" }, { @@ -1649,10 +1623,6 @@ } ], "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, "time": "2022-02-21T01:04:05+00:00" }, { @@ -1693,10 +1663,6 @@ ], "description": "A library to read, parse, export and make subsets of different types of font files.", "homepage": "https://github.com/PhenX/php-font-lib", - "support": { - "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/0.5.4" - }, "time": "2021-12-17T19:44:54+00:00" }, { @@ -1739,10 +1705,6 @@ ], "description": "A library to read, parse and export to PDF SVG files.", "homepage": "https://github.com/PhenX/php-svg-lib", - "support": { - "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" - }, "time": "2022-03-07T12:52:04+00:00" }, { @@ -1845,10 +1807,6 @@ "stream", "uri" ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/master" - }, "time": "2015-12-19T14:08:53+00:00" }, { @@ -2067,10 +2025,6 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" - }, "time": "2022-03-15T21:29:03+00:00" }, { @@ -2134,10 +2088,6 @@ "spy", "stub" ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, "time": "2021-12-08T12:19:24+00:00" }, { @@ -2245,10 +2195,6 @@ "MIT" ], "description": "PHPUnit extensions and rules for PHPStan", - "support": { - "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1" - }, "time": "2022-04-20T15:24:25+00:00" }, { @@ -2316,10 +2262,6 @@ "testing", "xunit" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2376,10 +2318,6 @@ "filesystem", "iterator" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2439,10 +2377,6 @@ "keywords": [ "process" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2498,10 +2432,6 @@ "keywords": [ "template" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2557,10 +2487,6 @@ "keywords": [ "timer" ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2656,10 +2582,6 @@ "testing", "xunit" ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" - }, "funding": [ { "url": "https://phpunit.de/sponsors.html", @@ -2916,10 +2838,6 @@ "parser", "stylesheet" ], - "support": { - "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", - "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" - }, "time": "2021-12-11T13:40:54+00:00" }, { @@ -2966,10 +2884,6 @@ ], "description": "Library for parsing CLI options", "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3022,10 +2936,6 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3077,10 +2987,6 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3151,10 +3057,6 @@ "compare", "equality" ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3208,10 +3110,6 @@ ], "description": "Library for calculating the complexity of PHP code units", "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3274,10 +3172,6 @@ "unidiff", "unified diff" ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3337,10 +3231,6 @@ "environment", "hhvm" ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3414,10 +3304,6 @@ "export", "exporter" ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3478,10 +3364,6 @@ "keywords": [ "global state" ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3535,10 +3417,6 @@ ], "description": "Library for counting the lines of code in PHP source code", "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3592,10 +3470,6 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3647,10 +3521,6 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3710,10 +3580,6 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3765,10 +3631,6 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3821,10 +3683,6 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3874,10 +3732,6 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3960,16 +3814,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563", + "reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563", "shasum": "" }, "require": { @@ -4007,12 +3861,7 @@ "phpcs", "standards" ], - "support": { - "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", - "source": "https://github.com/squizlabs/PHP_CodeSniffer", - "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" - }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2022-06-13T06:31:38+00:00" }, { "name": "symfony/console", @@ -4603,9 +4452,6 @@ "polyfill", "portable" ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5381,10 +5227,6 @@ "pdf417", "qrcode" ], - "support": { - "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.4.4" - }, "funding": [ { "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", @@ -5526,5 +5368,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "1.1.0" } From 0a8c97cf8ae3c7a1c60aea03703b68c2fecb4aa7 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 15 Jun 2022 13:59:58 +0200 Subject: [PATCH 020/156] Add PHP 8.2 with allow fail --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b36bf6c8..29a55f44 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,10 @@ jobs: - '8.0' - '8.1' + include: + - php-version: '8.2' + experimental: true + name: PHP ${{ matrix.php-version }} steps: @@ -39,7 +43,7 @@ jobs: - name: Delete composer lock file id: composer-lock - if: ${{ matrix.php-version == '8.1' }} + if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' }} run: | rm composer.lock echo "::set-output name=flags::--ignore-platform-reqs" From 1829dea91e55e0dcf96f98c81cbd2ecac108366c Mon Sep 17 00:00:00 2001 From: FlameStorm Date: Thu, 16 Jun 2022 00:55:17 +0300 Subject: [PATCH 021/156] Ignore square-$-brackets prefix in format string (#2886) * Ignore square-$-brackets prefix in format string * Test for square-$-brackets prefix in format string issue fixed * Fix for phpstan compliance * Additional assert for checking number format of tested source cell --- .../Style/NumberFormat/Formatter.php | 3 ++ .../Reader/Xlsx/Issue2885Test.php | 30 ++++++++++++++++++ tests/data/Reader/XLSX/issue.2885.xlsx | Bin 0 -> 5436 bytes 3 files changed, 33 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php create mode 100644 tests/data/Reader/XLSX/issue.2885.xlsx diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 3e4bdc46..be195a88 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -112,6 +112,9 @@ class Formatter return $value; } + // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc + $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format); + $format = (string) preg_replace_callback( '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', function ($matches) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php new file mode 100644 index 00000000..82727ef8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php @@ -0,0 +1,30 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('[$-809]0%', $sheet->getStyle('A1')->getNumberFormat()->getFormatCode()); + + $finishColumns = $sheet->getHighestColumn(); + $rowsCount = $sheet->getHighestRow(); + $rows = $sheet->rangeToArray("A1:{$finishColumns}{$rowsCount}"); + self::assertSame('8%', $rows[0][0]); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.2885.xlsx b/tests/data/Reader/XLSX/issue.2885.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d7f4d48901ded48529578065895e60651d79015d GIT binary patch literal 5436 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 F{{w(ZFa7`k literal 0 HcmV?d00001 From 481cef2def54226368b35ea9de3867fac8101f33 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 15 Jun 2022 18:47:35 -0700 Subject: [PATCH 022/156] Php8.2 Deprecation in Reader/Xlsx (#2894) * Php8.2 Deprecation in Reader/Xlsx Using `${var}` will be deprecated, with the suggested resolution being to use `{$var}`. This appears to be the only place in PhpSpreadsheet which does this. Some vendor packages will need to change for 8.2 for this and other reasons. * mb_convert_encoding and HTML_ENTITIES Also scheduled to be deprecated with 8.2. It appears to have not been needed in PhpSpreadsheet in the first place. --- src/PhpSpreadsheet/Reader/Html.php | 18 ++++-------------- src/PhpSpreadsheet/Reader/Xlsx.php | 7 +------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 4edf3cf8..3d859e15 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -632,16 +632,6 @@ class Html extends BaseReader } } - /** - * Make sure mb_convert_encoding returns string. - * - * @param mixed $result - */ - private static function ensureString($result): string - { - return is_string($result) ? $result : ''; - } - /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. * @@ -660,8 +650,8 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scanFile($filename), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scanFile($filename); + $loaded = $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -683,8 +673,8 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scan($content); + $loaded = $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 8562339b..52df94e4 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -414,7 +414,7 @@ class Xlsx extends BaseReader [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; - $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); + $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS); $theme = null; $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { @@ -1849,11 +1849,6 @@ class Xlsx extends BaseReader private static function dirAdd($base, $add): string { - $add = "$add"; - if (substr($add, 0, 4) === '/xl/') { - $add = substr($add, 4); - } - return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } From 97381d43071677afdc032d94cfb5098ff8dab281 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 15 Jun 2022 19:00:33 -0700 Subject: [PATCH 023/156] Complete Support for Chart/Axis and Gridlines (#2881) Unit testing now results in 100% coverage for Axis and Properties. All the properties in methods in Gridlines were more or less duplicated in Axis, and these duplications are moved to the common ancestor Properties. So, there isn't anything left in Gridlines. PhpSpreadsheet Chart is now over 85% covered (it was below 35% until recently). Properties are in many cases set to default to null/null-string, rather than the default values they receive from Excel, and are not written to Xml if unchanged. This is consistent with how Excel behaves. A new property `crossBetween` is added to Axis, and, with support for that added to Xlsx Reader and Writer, some minor Sample peculiarities are corrected, in particular, the charts were sometimes slightly truncated on the left and right edges. --- phpstan-baseline.neon | 65 --- src/PhpSpreadsheet/Chart/Axis.php | 454 ++------------- src/PhpSpreadsheet/Chart/GridLines.php | 446 --------------- src/PhpSpreadsheet/Chart/Properties.php | 532 +++++++++++++++++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 128 +++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 348 ++++++------ .../Chart/AxisPropertiesTest.php | 226 ++++++++ .../Chart/GridlinesLineStyleTest.php | 215 +++++++ 8 files changed, 1302 insertions(+), 1112 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5c1f074e..1491a665 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1170,46 +1170,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Chart/DataSeries.php - - - message: "#^Parameter \\#1 \\$color of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Parameter \\#2 \\$alpha of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Parameter \\#3 \\$colorType of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$glowProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$lineProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$objectState has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$shadowProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$softEdges has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" count: 1 @@ -4385,11 +4345,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#1 \\$plotSeriesValues of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeBubbles\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|null, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|false given\\.$#" count: 1 @@ -4400,16 +4355,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, array\\|int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, array\\|int\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float given\\.$#" count: 6 @@ -4425,11 +4370,6 @@ parameters: count: 42 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#6 \\$yAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" count: 1 @@ -4450,11 +4390,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - message: "#^Part \\$xAxis\\-\\>getShadowProperty\\(\\['color', 'type'\\]\\) \\(array\\|int\\|string\\|null\\) of encapsed string cannot be cast to string\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:\\$calculateCellValues has no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 69607216..09a4febc 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -50,70 +50,6 @@ class Axis extends Properties 'alpha' => 0, ]; - /** - * Line Properties. - * - * @var mixed[] - */ - private $lineProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; - - /** - * Line Style Properties. - * - * @var mixed[] - */ - private $lineStyleProperties = [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ]; - - /** - * Shadow Properties. - * - * @var mixed[] - */ - private $shadowProperties = Properties::PRESETS_OPTIONS[0]; - - /** - * Glow Properties. - * - * @var mixed[] - */ - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - /** - * Soft Edge Properties. - * - * @var mixed[] - */ - private $softEdges = [ - 'size' => null, - ]; - private const NUMERIC_FORMAT = [ Properties::FORMAT_CODE_NUMBER, Properties::FORMAT_CODE_DATE, @@ -161,33 +97,39 @@ class Axis extends Properties return (bool) $this->axisNumber['numeric']; } + public function setAxisOption(string $key, ?string $value): void + { + if (!empty($value)) { + $this->axisOptions[$key] = $value; + } + } + /** * Set Axis Options Properties. - * - * @param string $axisLabels - * @param string $horizontalCrossesValue - * @param string $horizontalCrosses - * @param string $axisOrientation - * @param string $majorTmt - * @param string $minorTmt - * @param string $minimum - * @param string $maximum - * @param string $majorUnit - * @param string $minorUnit */ - public function setAxisOptionsProperties($axisLabels, $horizontalCrossesValue = null, $horizontalCrosses = null, $axisOrientation = null, $majorTmt = null, $minorTmt = null, $minimum = null, $maximum = null, $majorUnit = null, $minorUnit = null): void - { - $this->axisOptions['axis_labels'] = (string) $axisLabels; - ($horizontalCrossesValue !== null) ? $this->axisOptions['horizontal_crosses_value'] = (string) $horizontalCrossesValue : null; - ($horizontalCrosses !== null) ? $this->axisOptions['horizontal_crosses'] = (string) $horizontalCrosses : null; - ($axisOrientation !== null) ? $this->axisOptions['orientation'] = (string) $axisOrientation : null; - ($majorTmt !== null) ? $this->axisOptions['major_tick_mark'] = (string) $majorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minimum !== null) ? $this->axisOptions['minimum'] = (string) $minimum : null; - ($maximum !== null) ? $this->axisOptions['maximum'] = (string) $maximum : null; - ($majorUnit !== null) ? $this->axisOptions['major_unit'] = (string) $majorUnit : null; - ($minorUnit !== null) ? $this->axisOptions['minor_unit'] = (string) $minorUnit : null; + public function setAxisOptionsProperties( + string $axisLabels, + ?string $horizontalCrossesValue = null, + ?string $horizontalCrosses = null, + ?string $axisOrientation = null, + ?string $majorTmt = null, + ?string $minorTmt = null, + ?string $minimum = null, + ?string $maximum = null, + ?string $majorUnit = null, + ?string $minorUnit = null + ): void { + $this->axisOptions['axis_labels'] = $axisLabels; + $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); + $this->setAxisOption('horizontal_crosses', $horizontalCrosses); + $this->setAxisOption('orientation', $axisOrientation); + $this->setAxisOption('major_tick_mark', $majorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minimum', $minimum); + $this->setAxisOption('maximum', $maximum); + $this->setAxisOption('major_unit', $majorUnit); + $this->setAxisOption('minor_unit', $minorUnit); } /** @@ -195,7 +137,7 @@ class Axis extends Properties * * @param string $property * - * @return string + * @return ?string */ public function getAxisOptionsProperty($property) { @@ -215,27 +157,15 @@ class Axis extends Properties /** * Set Fill Property. * - * @param string $color - * @param int $alpha - * @param string $AlphaType + * @param ?string $color + * @param ?int $alpha + * @param ?string $AlphaType */ - public function setFillParameters($color, $alpha = 0, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function setFillParameters($color, $alpha = null, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void { $this->fillProperties = $this->setColorProperties($color, $alpha, $AlphaType); } - /** - * Set Line Property. - * - * @param string $color - * @param int $alpha - * @param string $alphaType - */ - public function setLineParameters($color, $alpha = 0, $alphaType = self::EXCEL_COLOR_TYPE_ARGB): void - { - $this->lineProperties = $this->setColorProperties($color, $alpha, $alphaType); - } - /** * Get Fill Property. * @@ -245,323 +175,33 @@ class Axis extends Properties */ public function getFillProperty($property) { - return $this->fillProperties[$property]; + return (string) $this->fillProperties[$property]; } /** - * Get Line Property. + * Get Line Color Property. * - * @param string $property + * @param string $propertyName * - * @return string + * @return null|int|string */ - public function getLineProperty($property) + public function getLineProperty($propertyName) { - return $this->lineProperties[$property]; + return $this->lineProperties['color'][$propertyName]; } - /** - * Set Line Style Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - ($lineWidth !== null) ? $this->lineStyleProperties['width'] = $this->getExcelPointsWidth((float) $lineWidth) : null; - ($compoundType !== null) ? $this->lineStyleProperties['compound'] = (string) $compoundType : null; - ($dashType !== null) ? $this->lineStyleProperties['dash'] = (string) $dashType : null; - ($capType !== null) ? $this->lineStyleProperties['cap'] = (string) $capType : null; - ($joinType !== null) ? $this->lineStyleProperties['join'] = (string) $joinType : null; - ($headArrowType !== null) ? $this->lineStyleProperties['arrow']['head']['type'] = (string) $headArrowType : null; - ($headArrowSize !== null) ? $this->lineStyleProperties['arrow']['head']['size'] = (string) $headArrowSize : null; - ($endArrowType !== null) ? $this->lineStyleProperties['arrow']['end']['type'] = (string) $endArrowType : null; - ($endArrowSize !== null) ? $this->lineStyleProperties['arrow']['end']['size'] = (string) $endArrowSize : null; - } + /** @var string */ + private $crossBetween = ''; // 'between' or 'midCat' might be better - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) + public function setCrossBetween(string $crossBetween): self { - return $this->getArrayElementsValue($this->lineStyleProperties, $elements); - } - - /** - * Get Line Style Arrow Excel Width. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowWidth($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'w'); - } - - /** - * Get Line Style Arrow Excel Length. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowLength($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'len'); - } - - /** - * @param mixed $value - */ - public function setShadowProperty(string $propertyName, $value): self - { - if ($propertyName === 'color' && is_array($value)) { - $this->setShadowColor($value['value'], $value['alpha'], $value['type']); - } else { - $this->shadowProperties[$propertyName] = $value; - } + $this->crossBetween = $crossBetween; return $this; } - /** - * Set Shadow Properties. - * - * @param int $shadowPresets - * @param string $colorValue - * @param string $colorType - * @param null|int|string $colorAlpha - * @param null|float $blur - * @param null|int $angle - * @param null|float $distance - */ - public function setShadowProperties($shadowPresets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + public function getCrossBetween(): string { - $this->setShadowPresetsProperties((int) $shadowPresets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - (int) ($colorAlpha ?? $this->shadowProperties['color']['alpha']), - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Color. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); - - return $this; - } - - private const SHADOW_ARRAY_KEYS = ['size', 'color']; - - /** - * Set Shadow Properties from Mapped Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) - { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { - $reference = &$this->shadowProperties[$property_key]; - $this->setShadowPropertiesMapValues($property_val, $reference); - } - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } - - return $this; - } - - /** - * Set Shadow Color. - * - * @param null|string $color - * @param null|int $alpha - * @param null|string $alphaType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $alphaType) - { - $this->shadowProperties['color'] = $this->setColorProperties($color, $alpha, $alphaType); - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param null|float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = $blur; - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param null|float|int $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if (is_numeric($angle)) { - $this->shadowProperties['direction'] = $angle; - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param null|float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = $distance; - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return null|array|int|string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param null|string $colorValue - * @param null|int $colorAlpha - * @param null|string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this->setGlowSize($size) - ->setGlowColor( - $colorValue ?? $this->glowProperties['color']['value'], - $colorAlpha ?? (int) $this->glowProperties['color']['alpha'], - $colorType ?? $this->glowProperties['color']['type'] - ); - } - - /** - * Get Glow Property. - * - * @param array|string $property - * - * @return null|string - */ - public function getGlowProperty($property) - { - return $this->getArrayElementsValue($this->glowProperties, $property); - } - - /** - * Set Glow Color. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - if ($size !== null) { - $this->glowProperties['size'] = $size; - } - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - $this->glowProperties['color'] = $this->setColorProperties($color, $alpha, $colorType); - - return $this; - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $this->softEdges['size'] = $size; - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; + return $this->crossBetween; } } diff --git a/src/PhpSpreadsheet/Chart/GridLines.php b/src/PhpSpreadsheet/Chart/GridLines.php index ad254c8c..8b86ccbd 100644 --- a/src/PhpSpreadsheet/Chart/GridLines.php +++ b/src/PhpSpreadsheet/Chart/GridLines.php @@ -10,450 +10,4 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class GridLines extends Properties { - /** - * Properties of Class: - * Object State (State for Minor Tick Mark) @var bool - * Line Properties @var array of mixed - * Shadow Properties @var array of mixed - * Glow Properties @var array of mixed - * Soft Properties @var array of mixed. - */ - private $objectState = false; - - private $lineProperties = [ - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => null, - 'alpha' => 0, - ], - 'style' => [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ], - ]; - - private $shadowProperties = Properties::PRESETS_OPTIONS[0]; - - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - private $softEdges = [ - 'size' => null, - ]; - - /** - * Get Object State. - * - * @return bool - */ - public function getObjectState() - { - return $this->objectState; - } - - /** - * Change Object State to True. - * - * @return $this - */ - private function activateObject() - { - $this->objectState = true; - - return $this; - } - - /** - * Set Line Color Properties. - * - * @param string $value - * @param int $alpha - * @param string $colorType - */ - public function setLineColorProperties($value, $alpha = 0, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void - { - $this->activateObject() - ->lineProperties['color'] = $this->setColorProperties( - $value, - $alpha, - $colorType - ); - } - - /** - * Set Line Color Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - $this->activateObject(); - ($lineWidth !== null) - ? $this->lineProperties['style']['width'] = $this->getExcelPointsWidth((float) $lineWidth) - : null; - ($compoundType !== null) - ? $this->lineProperties['style']['compound'] = (string) $compoundType - : null; - ($dashType !== null) - ? $this->lineProperties['style']['dash'] = (string) $dashType - : null; - ($capType !== null) - ? $this->lineProperties['style']['cap'] = (string) $capType - : null; - ($joinType !== null) - ? $this->lineProperties['style']['join'] = (string) $joinType - : null; - ($headArrowType !== null) - ? $this->lineProperties['style']['arrow']['head']['type'] = (string) $headArrowType - : null; - ($headArrowSize !== null) - ? $this->lineProperties['style']['arrow']['head']['size'] = (string) $headArrowSize - : null; - ($endArrowType !== null) - ? $this->lineProperties['style']['arrow']['end']['type'] = (string) $endArrowType - : null; - ($endArrowSize !== null) - ? $this->lineProperties['style']['arrow']['end']['size'] = (string) $endArrowSize - : null; - } - - /** - * Get Line Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getLineColorProperty($propertyName) - { - return $this->lineProperties['color'][$propertyName]; - } - - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) - { - return $this->getArrayElementsValue($this->lineProperties['style'], $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param string $colorValue - * @param int $colorAlpha - * @param string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this - ->activateObject() - ->setGlowSize($size) - ->setGlowColor($colorValue, $colorAlpha, $colorType); - } - - /** - * Get Glow Property. - * - * @param array|string $property - * - * @return null|string - */ - public function getGlowProperty($property) - { - return $this->getArrayElementsValue($this->glowProperties, $property); - } - - /** - * Get Glow Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getGlowColor($propertyName) - { - return $this->glowProperties['color'][$propertyName]; - } - - /** - * Get Glow Size. - * - * @return string - */ - public function getGlowSize() - { - return $this->glowProperties['size']; - } - - /** - * Set Glow Size. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - $this->glowProperties['size'] = $size; - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->glowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->glowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->glowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Get Line Style Arrow Parameters. - * - * @param string $arrowSelector - * @param string $propertySelector - * - * @return string - */ - public function getLineStyleArrowParameters($arrowSelector, $propertySelector) - { - return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); - } - - /** - * @param mixed $value - */ - public function setShadowProperty(string $propertyName, $value): self - { - $this->activateObject(); - $this->shadowProperties[$propertyName] = $value; - - return $this; - } - - /** - * Set Shadow Properties. - * - * @param int $presets - * @param string $colorValue - * @param string $colorType - * @param null|float|int|string $colorAlpha - * @param null|float $blur - * @param null|int $angle - * @param null|float $distance - */ - public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void - { - $this->activateObject() - ->setShadowPresetsProperties((int) $presets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Presets Properties. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); - - return $this; - } - - private const SHADOW_ARRAY_KEYS = ['size', 'color']; - - /** - * Set Shadow Properties Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) - { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { - $reference = &$this->shadowProperties[$property_key]; - $this->setShadowPropertiesMapValues($property_val, $reference); - } - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } - - return $this; - } - - /** - * Set Shadow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->shadowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->shadowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->shadowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param ?float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = $blur; - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param null|float|int|string $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if (is_numeric($angle)) { - $this->shadowProperties['direction'] = $angle; - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param ?float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = $distance; - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $this->activateObject(); - $this->softEdges['size'] = $size; - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; - } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 6db04809..c6b2d15b 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -120,14 +120,47 @@ abstract class Properties const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy + /** @var bool */ + protected $objectState = false; // used only for minor gridlines + + /** @var array */ + protected $glowProperties = [ + 'size' => null, + 'color' => [ + 'type' => self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + ]; + + /** @var array */ + protected $softEdges = [ + 'size' => null, + ]; + + /** @var array */ + protected $shadowProperties = self::PRESETS_OPTIONS[0]; + /** - * @param float $width + * Get Object State. * - * @return float + * @return bool */ - protected function getExcelPointsWidth($width) + public function getObjectState() { - return $width * self::POINTS_WIDTH_MULTIPLIER; + return $this->objectState; + } + + /** + * Change Object State to True. + * + * @return $this + */ + protected function activateObject() + { + $this->objectState = true; + + return $this; } public static function pointsToXml(float $width): string @@ -181,27 +214,10 @@ abstract class Properties return [ 'type' => $colorType, 'value' => $color, - 'alpha' => (int) $alpha, + 'alpha' => ($alpha === null) ? null : (int) $alpha, ]; } - protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) - { - $sizes = [ - 1 => ['w' => 'sm', 'len' => 'sm'], - 2 => ['w' => 'sm', 'len' => 'med'], - 3 => ['w' => 'sm', 'len' => 'lg'], - 4 => ['w' => 'med', 'len' => 'sm'], - 5 => ['w' => 'med', 'len' => 'med'], - 6 => ['w' => 'med', 'len' => 'lg'], - 7 => ['w' => 'lg', 'len' => 'sm'], - 8 => ['w' => 'lg', 'len' => 'med'], - 9 => ['w' => 'lg', 'len' => 'lg'], - ]; - - return $sizes[$arraySelector][$arrayKaySelector]; - } - protected const PRESETS_OPTIONS = [ //NONE 0 => [ @@ -428,4 +444,476 @@ abstract class Properties return $reference; } + + /** + * Set Glow Properties. + * + * @param float $size + * @param ?string $colorValue + * @param ?int $colorAlpha + * @param ?string $colorType + */ + public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void + { + $this + ->activateObject() + ->setGlowSize($size) + ->setGlowColor($colorValue, $colorAlpha, $colorType); + } + + /** + * Get Glow Property. + * + * @param array|string $property + * + * @return null|string + */ + public function getGlowProperty($property) + { + return $this->getArrayElementsValue($this->glowProperties, $property); + } + + /** + * Get Glow Color Property. + * + * @param string $propertyName + * + * @return string + */ + public function getGlowColor($propertyName) + { + return $this->glowProperties['color'][$propertyName]; + } + + /** + * Get Glow Size. + * + * @return string + */ + public function getGlowSize() + { + return $this->glowProperties['size']; + } + + /** + * Set Glow Size. + * + * @param float $size + * + * @return $this + */ + protected function setGlowSize($size) + { + $this->glowProperties['size'] = $size; + + return $this; + } + + /** + * Set Glow Color. + * + * @param ?string $color + * @param ?int $alpha + * @param ?string $colorType + * + * @return $this + */ + protected function setGlowColor($color, $alpha, $colorType) + { + if ($color !== null) { + $this->glowProperties['color']['value'] = (string) $color; + } + if ($alpha !== null) { + $this->glowProperties['color']['alpha'] = (int) $alpha; + } + if ($colorType !== null) { + $this->glowProperties['color']['type'] = (string) $colorType; + } + + return $this; + } + + /** + * Set Soft Edges Size. + * + * @param float $size + */ + public function setSoftEdges($size): void + { + if ($size !== null) { + $this->activateObject(); + $this->softEdges['size'] = $size; + } + } + + /** + * Get Soft Edges Size. + * + * @return string + */ + public function getSoftEdgesSize() + { + return $this->softEdges['size']; + } + + /** + * @param mixed $value + */ + public function setShadowProperty(string $propertyName, $value): self + { + $this->activateObject(); + $this->shadowProperties[$propertyName] = $value; + + return $this; + } + + /** + * Set Shadow Properties. + * + * @param int $presets + * @param string $colorValue + * @param string $colorType + * @param null|float|int|string $colorAlpha + * @param null|float $blur + * @param null|int $angle + * @param null|float $distance + */ + public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + { + $this->activateObject() + ->setShadowPresetsProperties((int) $presets) + ->setShadowColor( + $colorValue ?? $this->shadowProperties['color']['value'], + $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, + $colorType ?? $this->shadowProperties['color']['type'] + ) + ->setShadowBlur($blur) + ->setShadowAngle($angle) + ->setShadowDistance($distance); + } + + /** + * Set Shadow Presets Properties. + * + * @param int $presets + * + * @return $this + */ + protected function setShadowPresetsProperties($presets) + { + $this->shadowProperties['presets'] = $presets; + $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + + return $this; + } + + protected const SHADOW_ARRAY_KEYS = ['size', 'color']; + + /** + * Set Shadow Properties Values. + * + * @param mixed $reference + * + * @return $this + */ + protected function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + { + $base_reference = $reference; + foreach ($propertiesMap as $property_key => $property_val) { + if (is_array($property_val)) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { + $reference = &$this->shadowProperties[$property_key]; + $this->setShadowPropertiesMapValues($property_val, $reference); + } + } else { + if ($base_reference === null) { + $this->shadowProperties[$property_key] = $property_val; + } else { + $reference[$property_key] = $property_val; + } + } + } + + return $this; + } + + /** + * Set Shadow Color. + * + * @param string $color + * @param int $alpha + * @param string $colorType + * + * @return $this + */ + protected function setShadowColor($color, $alpha, $colorType) + { + if ($color !== null) { + $this->shadowProperties['color']['value'] = (string) $color; + } + if ($alpha !== null) { + $this->shadowProperties['color']['alpha'] = (int) $alpha; + } + if ($colorType !== null) { + $this->shadowProperties['color']['type'] = (string) $colorType; + } + + return $this; + } + + /** + * Set Shadow Blur. + * + * @param ?float $blur + * + * @return $this + */ + protected function setShadowBlur($blur) + { + if ($blur !== null) { + $this->shadowProperties['blur'] = $blur; + } + + return $this; + } + + /** + * Set Shadow Angle. + * + * @param null|float|int|string $angle + * + * @return $this + */ + protected function setShadowAngle($angle) + { + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; + } + + return $this; + } + + /** + * Set Shadow Distance. + * + * @param ?float $distance + * + * @return $this + */ + protected function setShadowDistance($distance) + { + if ($distance !== null) { + $this->shadowProperties['distance'] = $distance; + } + + return $this; + } + + /** + * Get Shadow Property. + * + * @param string|string[] $elements + * + * @return string + */ + public function getShadowProperty($elements) + { + return $this->getArrayElementsValue($this->shadowProperties, $elements); + } + + /** @var array */ + protected $lineProperties = [ + 'color' => [ + 'type' => '', //self::EXCEL_COLOR_TYPE_STANDARD, + 'value' => '', //null, + 'alpha' => null, + ], + 'style' => [ + 'width' => null, //'9525', + 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => '', //self::LINE_STYLE_DASH_SOLID, + 'cap' => '', //self::LINE_STYLE_CAP_FLAT, + 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, + 'w' => '', + 'len' => '', + ], + 'end' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, + 'w' => '', + 'len' => '', + ], + ], + ], + ]; + + /** + * Set Line Color Properties. + * + * @param string $value + * @param ?int $alpha + * @param string $colorType + */ + public function setLineColorProperties($value, $alpha = null, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void + { + $this->activateObject() + ->lineProperties['color'] = $this->setColorProperties( + $value, + $alpha, + $colorType + ); + } + + public function setColorPropertiesArray(array $color): void + { + $this->activateObject() + ->lineProperties['color'] = $color; + } + + /** + * Get Line Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getLineColorProperty($propertyName) + { + return $this->lineProperties['color'][$propertyName]; + } + + /** + * Set Line Style Properties. + * + * @param null|float|int|string $lineWidth + * @param string $compoundType + * @param string $dashType + * @param string $capType + * @param string $joinType + * @param string $headArrowType + * @param string $headArrowSize + * @param string $endArrowType + * @param string $endArrowSize + * @param string $headArrowWidth + * @param string $headArrowLength + * @param string $endArrowWidth + * @param string $endArrowLength + */ + public function setLineStyleProperties($lineWidth = null, $compoundType = '', $dashType = '', $capType = '', $joinType = '', $headArrowType = '', $headArrowSize = '', $endArrowType = '', $endArrowSize = '', $headArrowWidth = '', $headArrowLength = '', $endArrowWidth = '', $endArrowLength = ''): void + { + $this->activateObject(); + if (is_numeric($lineWidth)) { + $this->lineProperties['style']['width'] = $lineWidth; + } + if ($compoundType !== '') { + $this->lineProperties['style']['compound'] = $compoundType; + } + if ($dashType !== '') { + $this->lineProperties['style']['dash'] = $dashType; + } + if ($capType !== '') { + $this->lineProperties['style']['cap'] = $capType; + } + if ($joinType !== '') { + $this->lineProperties['style']['join'] = $joinType; + } + if ($headArrowType !== '') { + $this->lineProperties['style']['arrow']['head']['type'] = $headArrowType; + } + if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { + $this->lineProperties['style']['arrow']['head']['size'] = $headArrowSize; + $this->lineProperties['style']['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; + $this->lineProperties['style']['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; + } + if ($endArrowType !== '') { + $this->lineProperties['style']['arrow']['end']['type'] = $endArrowType; + } + if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { + $this->lineProperties['style']['arrow']['end']['size'] = $endArrowSize; + $this->lineProperties['style']['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; + $this->lineProperties['style']['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; + } + if ($headArrowWidth !== '') { + $this->lineProperties['style']['arrow']['head']['w'] = $headArrowWidth; + } + if ($headArrowLength !== '') { + $this->lineProperties['style']['arrow']['head']['len'] = $headArrowLength; + } + if ($endArrowWidth !== '') { + $this->lineProperties['style']['arrow']['end']['w'] = $endArrowWidth; + } + if ($endArrowLength !== '') { + $this->lineProperties['style']['arrow']['end']['len'] = $endArrowLength; + } + } + + /** + * Get Line Style Property. + * + * @param array|string $elements + * + * @return string + */ + public function getLineStyleProperty($elements) + { + return $this->getArrayElementsValue($this->lineProperties['style'], $elements); + } + + protected const ARROW_SIZES = [ + 1 => ['w' => 'sm', 'len' => 'sm'], + 2 => ['w' => 'sm', 'len' => 'med'], + 3 => ['w' => 'sm', 'len' => 'lg'], + 4 => ['w' => 'med', 'len' => 'sm'], + 5 => ['w' => 'med', 'len' => 'med'], + 6 => ['w' => 'med', 'len' => 'lg'], + 7 => ['w' => 'lg', 'len' => 'sm'], + 8 => ['w' => 'lg', 'len' => 'med'], + 9 => ['w' => 'lg', 'len' => 'lg'], + ]; + + protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) + { + return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? ''; + } + + /** + * Get Line Style Arrow Parameters. + * + * @param string $arrowSelector + * @param string $propertySelector + * + * @return string + */ + public function getLineStyleArrowParameters($arrowSelector, $propertySelector) + { + return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); + } + + /** + * Get Line Style Arrow Width. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowWidth($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'w']); + } + + /** + * Get Line Style Arrow Excel Length. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowLength($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'len']); + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 8e3d6386..b046bc24 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -100,6 +100,14 @@ class Chart $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } $this->readEffects($chartDetail, $xAxis); + if (isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } + $this->setAxisProperties($chartDetail, $xAxis); break; case 'dateAx': @@ -144,18 +152,28 @@ class Chart } } $this->readEffects($chartDetail, $whichAxis); + if ($whichAxis !== null && isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } if (isset($chartDetail->majorGridlines)) { $majorGridlines = new GridLines(); if (isset($chartDetail->majorGridlines->spPr)) { $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); } } if (isset($chartDetail->minorGridlines)) { $minorGridlines = new GridLines(); if (isset($chartDetail->minorGridlines->spPr)) { $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); } } + $this->setAxisProperties($chartDetail, $whichAxis); break; case 'barChart': @@ -1071,4 +1089,114 @@ class Chart return $result; } + + /** + * @param null|GridLines $chartObject may be extended to include other types + */ + private function readLineStyle(SimpleXMLElement $chartDetail, $chartObject): void + { + if (!isset($chartObject, $chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + + if (!isset($sppr->ln)) { + return; + } + $lineWidth = null; + /** @var string */ + $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string'); + if (is_numeric($lineWidthTemp)) { + $lineWidth = Properties::xmlToPoints($lineWidthTemp); + } + /** @var string */ + $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string'); + /** @var string */ + $dashType = self::getAttribute($sppr->ln->prstDash, 'val', 'string'); + /** @var string */ + $capType = self::getAttribute($sppr->ln, 'cap', 'string'); + if (isset($sppr->ln->miter)) { + $joinType = Properties::LINE_STYLE_JOIN_MITER; + } elseif (isset($sppr->ln->bevel)) { + $joinType = Properties::LINE_STYLE_JOIN_BEVEL; + } else { + $joinType = ''; + } + $headArrowType = ''; + $headArrowSize = ''; + $endArrowType = ''; + $endArrowSize = ''; + /** @var string */ + $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string'); + /** @var string */ + $headArrowWidth = self::getAttribute($sppr->ln->headEnd, 'w', 'string'); + /** @var string */ + $headArrowLength = self::getAttribute($sppr->ln->headEnd, 'len', 'string'); + /** @var string */ + $endArrowType = self::getAttribute($sppr->ln->tailEnd, 'type', 'string'); + /** @var string */ + $endArrowWidth = self::getAttribute($sppr->ln->tailEnd, 'w', 'string'); + /** @var string */ + $endArrowLength = self::getAttribute($sppr->ln->tailEnd, 'len', 'string'); + $chartObject->setLineStyleProperties( + $lineWidth, + $compoundType, + $dashType, + $capType, + $joinType, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize, + $headArrowWidth, + $headArrowLength, + $endArrowWidth, + $endArrowLength + ); + $colorArray = $this->readColor($sppr->ln->solidFill); + $chartObject->setColorPropertiesArray($colorArray); + } + + private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void + { + if (!isset($whichAxis)) { + return; + } + if (isset($chartDetail->crossBetween)) { + $whichAxis->setCrossBetween((string) self::getAttribute($chartDetail->crossBetween, 'val', 'string')); + } + if (isset($chartDetail->majorTickMark)) { + $whichAxis->setAxisOption('major_tick_mark', (string) self::getAttribute($chartDetail->majorTickMark, 'val', 'string')); + } + if (isset($chartDetail->minorTickMark)) { + $whichAxis->setAxisOption('minor_tick_mark', (string) self::getAttribute($chartDetail->minorTickMark, 'val', 'string')); + } + if (isset($chartDetail->tickLblPos)) { + $whichAxis->setAxisOption('axis_labels', (string) self::getAttribute($chartDetail->tickLblPos, 'val', 'string')); + } + if (isset($chartDetail->crosses)) { + $whichAxis->setAxisOption('horizontal_crosses', (string) self::getAttribute($chartDetail->crosses, 'val', 'string')); + } + if (isset($chartDetail->crossesAt)) { + $whichAxis->setAxisOption('horizontal_crosses_value', (string) self::getAttribute($chartDetail->crossesAt, 'val', 'string')); + } + if (isset($chartDetail->scaling->orientation)) { + $whichAxis->setAxisOption('orientation', (string) self::getAttribute($chartDetail->scaling->orientation, 'val', 'string')); + } + if (isset($chartDetail->scaling->max)) { + $whichAxis->setAxisOption('maximum', (string) self::getAttribute($chartDetail->scaling->max, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->majorUnit)) { + $whichAxis->setAxisOption('major_unit', (string) self::getAttribute($chartDetail->majorUnit, 'val', 'string')); + } + if (isset($chartDetail->minorUnit)) { + $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index dda395ac..1bdf4fe1 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -249,11 +249,11 @@ class Chart extends WriterPart $groupType = $plotGroup->getPlotType(); if ($groupType == $chartType) { $plotStyle = $plotGroup->getPlotStyle(); - if ($groupType === DataSeries::TYPE_RADARCHART) { + if (!empty($plotStyle) && $groupType === DataSeries::TYPE_RADARCHART) { $objWriter->startElement('c:radarStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); - } elseif ($groupType === DataSeries::TYPE_SCATTERCHART) { + } elseif (!empty($plotStyle) && $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); @@ -431,10 +431,22 @@ class Chart extends WriterPart } $objWriter->startElement('c:scaling'); - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); - $objWriter->endElement(); - $objWriter->endElement(); + if ($yAxis->getAxisOptionsProperty('maximum') !== null) { + $objWriter->startElement('c:max'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('maximum')); + $objWriter->endElement(); + } + if ($yAxis->getAxisOptionsProperty('minimum') !== null) { + $objWriter->startElement('c:min'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minimum')); + $objWriter->endElement(); + } + if (!empty($yAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); $objWriter->writeAttribute('val', 0); @@ -486,19 +498,38 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } $objWriter->startElement('c:spPr'); + if (!empty($yAxis->getFillProperty('value'))) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement('a:' . $yAxis->getFillProperty('type')); + $objWriter->writeAttribute('val', $yAxis->getFillProperty('value')); + $alpha = $yAxis->getFillProperty('alpha'); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); + $objWriter->endElement(); + } $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $yAxis); $this->writeShadow($objWriter, $yAxis); @@ -506,14 +537,28 @@ class Chart extends WriterPart $objWriter->endElement(); // effectLst $objWriter->endElement(); // spPr + if ($yAxis->getAxisOptionsProperty('major_unit') !== null) { + $objWriter->startElement('c:majorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_unit')); + $objWriter->endElement(); + } + + if ($yAxis->getAxisOptionsProperty('minor_unit') !== null) { + $objWriter->startElement('c:minorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_unit')); + $objWriter->endElement(); + } + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('horizontal_crosses'))) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); + $objWriter->endElement(); + } } $objWriter->startElement('c:auto'); @@ -568,11 +613,13 @@ class Chart extends WriterPart $objWriter->endElement(); } - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + if (!empty($xAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); $objWriter->writeAttribute('val', 0); @@ -585,48 +632,8 @@ class Chart extends WriterPart $objWriter->startElement('c:majorGridlines'); $objWriter->startElement('c:spPr'); - if ($majorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$majorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill + $this->writeGridlinesLn($objWriter, $majorGridlines); - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $majorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($majorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $majorGridlines); $this->writeShadow($objWriter, $majorGridlines); @@ -640,48 +647,7 @@ class Chart extends WriterPart $objWriter->startElement('c:minorGridlines'); $objWriter->startElement('c:spPr'); - if ($minorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$minorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $minorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($minorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } + $this->writeGridlinesLn($objWriter, $minorGridlines); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $minorGridlines); @@ -737,78 +703,41 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } $objWriter->startElement('c:spPr'); - if ($xAxis->getFillProperty('value') !== null) { + if (!empty($xAxis->getFillProperty('value'))) { $objWriter->startElement('a:solidFill'); $objWriter->startElement('a:' . $xAxis->getFillProperty('type')); $objWriter->writeAttribute('val', $xAxis->getFillProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('alpha')); - $objWriter->endElement(); + $alpha = $xAxis->getFillProperty('alpha'); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } $objWriter->endElement(); $objWriter->endElement(); } - $objWriter->startElement('a:ln'); - - $objWriter->writeAttribute('w', $xAxis->getLineStyleProperty('width')); - $objWriter->writeAttribute('cap', $xAxis->getLineStyleProperty('cap')); - $objWriter->writeAttribute('cmpd', $xAxis->getLineStyleProperty('compound')); - - if ($xAxis->getLineProperty('value') !== null) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getLineProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('alpha')); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $xAxis->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($xAxis->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('head')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('head')); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('end')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('end')); - $objWriter->endElement(); - } - - $objWriter->endElement(); + $this->writeGridlinesLn($objWriter, $xAxis); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $xAxis); @@ -828,14 +757,20 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value')); $objWriter->endElement(); } else { - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + $crosses = $xAxis->getAxisOptionsProperty('horizontal_crosses'); + if ($crosses) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $crosses); + $objWriter->endElement(); + } } - $objWriter->startElement('c:crossBetween'); - $objWriter->writeAttribute('val', 'midCat'); - $objWriter->endElement(); + $crossBetween = $xAxis->getCrossBetween(); + if ($crossBetween !== '') { + $objWriter->startElement('c:crossBetween'); + $objWriter->writeAttribute('val', $crossBetween); + $objWriter->endElement(); + } if ($xAxis->getAxisOptionsProperty('major_unit') !== null) { $objWriter->startElement('c:majorUnit'); @@ -1521,7 +1456,7 @@ class Chart extends WriterPart */ private function writeShadow(XMLWriter $objWriter, $xAxis): void { - if ($xAxis->getShadowProperty('effect') === null) { + if (empty($xAxis->getShadowProperty('effect'))) { return; } /** @var string */ @@ -1609,4 +1544,73 @@ class Chart extends WriterPart $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize)); $objWriter->endElement(); //end softEdge } + + /** + * Write Line Style for Gridlines. + * + * @param Axis|GridLines $gridlines + */ + private function writeGridlinesLn(XMLWriter $objWriter, $gridlines): void + { + $objWriter->startElement('a:ln'); + $widthTemp = $gridlines->getLineStyleProperty('width'); + if (is_numeric($widthTemp)) { + $objWriter->writeAttribute('w', Properties::pointsToXml((float) $widthTemp)); + } + $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap')); + $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound')); + if (!empty($gridlines->getLineColorProperty('value'))) { + $objWriter->startElement('a:solidFill'); + $objWriter->startElement("a:{$gridlines->getLineColorProperty('type')}"); + $objWriter->writeAttribute('val', (string) $gridlines->getLineColorProperty('value')); + $alpha = $gridlines->getLineColorProperty('alpha'); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); + $objWriter->endElement(); // alpha + } + $objWriter->endElement(); //end srgbClr + $objWriter->endElement(); //end solidFill + } + + $dash = $gridlines->getLineStyleProperty('dash'); + if (!empty($dash)) { + $objWriter->startElement('a:prstDash'); + $this->writeNotEmpty($objWriter, 'val', $dash); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty('join') === 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } elseif ($gridlines->getLineStyleProperty('join') === 'bevel') { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowParameters('head', 'w')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowParameters('head', 'len')); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowParameters('end', 'w')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowParameters('end', 'len')); + $objWriter->endElement(); + } + $objWriter->endElement(); //end ln + } + + private function writeNotEmpty(XMLWriter $objWriter, string $name, ?string $value): void + { + if ($value !== null && $value !== '') { + $objWriter->writeAttribute($name, $value); + } + } } diff --git a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php new file mode 100644 index 00000000..91df25cb --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php @@ -0,0 +1,226 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testAxisProperties(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $xAxis = new Axis(); + $xAxis->setFillParameters('FF0000', null, 'srgbClr'); + self::assertSame('FF0000', $xAxis->getFillProperty('value')); + self::assertSame('', $xAxis->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis->getFillProperty('type')); + + $xAxis->setAxisOptionsProperties( + Properties::AXIS_LABELS_HIGH, // axisLabels, + null, // $horizontalCrossesValue, + Properties::HORIZONTAL_CROSSES_MAXIMUM, //horizontalCrosses + Properties::ORIENTATION_REVERSED, //axisOrientation + Properties::TICK_MARK_INSIDE, //majorTmt + Properties::TICK_MARK_OUTSIDE, //minorTmt + '8', //minimum + '68', //maximum + '20', //majorUnit + '5' //minorUnit + ); + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis->getAxisOptionsProperty('axis_labels')); + self::assertNull($xAxis->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis->getAxisOptionsProperty('orientation')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis->getAxisOptionsProperty('minor_tick_mark')); + self::assertSame('8', $xAxis->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis->getAxisOptionsProperty('minor_unit')); + + $yAxis = new Axis(); + $yAxis->setFillParameters('accent1', 30, 'schemeClr'); + self::assertSame('accent1', $yAxis->getFillProperty('value')); + self::assertSame('30', $yAxis->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis->getFillProperty('type')); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + $xAxis, // xAxis + $yAxis, // yAxis + null, //majorGridlines, + null // minorGridlines + ); + $xAxis2 = $chart->getChartAxisX(); + self::assertSame('FF0000', $xAxis2->getFillProperty('value')); + self::assertSame('', $xAxis2->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis2->getFillProperty('type')); + + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis2->getAxisOptionsProperty('axis_labels')); + self::assertNull($xAxis2->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis2->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis2->getAxisOptionsProperty('orientation')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis2->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis2->getAxisOptionsProperty('minor_tick_mark')); + self::assertSame('8', $xAxis2->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis2->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis2->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis2->getAxisOptionsProperty('minor_unit')); + + $yAxis2 = $chart->getChartAxisY(); + self::assertSame('accent1', $yAxis2->getFillProperty('value')); + self::assertSame('30', $yAxis2->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis2->getFillProperty('type')); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $xAxis3 = $chart2->getChartAxisX(); + self::assertSame('FF0000', $xAxis3->getFillProperty('value')); + self::assertSame('', $xAxis3->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis3->getFillProperty('type')); + + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis3->getAxisOptionsProperty('axis_labels')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis3->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis3->getAxisOptionsProperty('minor_tick_mark')); + self::assertNull($xAxis3->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis3->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis3->getAxisOptionsProperty('orientation')); + self::assertSame('8', $xAxis3->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis3->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis3->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis3->getAxisOptionsProperty('minor_unit')); + + $yAxis3 = $chart2->getChartAxisY(); + self::assertSame('accent1', $yAxis3->getFillProperty('value')); + self::assertSame('30', $yAxis3->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis3->getFillProperty('type')); + + $xAxis3->setAxisOrientation(Properties::ORIENTATION_NORMAL); + self::assertSame(Properties::ORIENTATION_NORMAL, $xAxis3->getAxisOptionsProperty('orientation')); + $xAxis3->setAxisOptionsProperties( + Properties::AXIS_LABELS_HIGH, // axisLabels, + '5' // $horizontalCrossesValue, + ); + self::assertSame('5', $xAxis3->getAxisOptionsProperty('horizontal_crosses_value')); + + $yAxis3->setLineColorProperties('0000FF', null, 'srgbClr'); + self::assertSame('0000FF', $yAxis3->getLineProperty('value')); + self::assertNull($yAxis3->getLineProperty('alpha')); + self::assertSame('srgbClr', $yAxis3->getLineProperty('type')); + $yAxis3->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL); + self::assertFalse($yAxis3->getAxisIsNumericFormat()); + $yAxis3->setAxisNumberProperties(Properties::FORMAT_CODE_NUMBER); + self::assertTrue($yAxis3->getAxisIsNumericFormat()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php new file mode 100644 index 00000000..997413c5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php @@ -0,0 +1,215 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testLineStyles(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $majorGridlines = new GridLines(); + $width = 2; + $compound = Properties::LINE_STYLE_COMPOUND_THICKTHIN; + $dash = Properties::LINE_STYLE_DASH_ROUND_DOT; + $cap = Properties::LINE_STYLE_CAP_ROUND; + $join = Properties::LINE_STYLE_JOIN_MITER; + $headArrowType = Properties::LINE_STYLE_ARROW_TYPE_DIAMOND; + $headArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_2; + $endArrowType = Properties::LINE_STYLE_ARROW_TYPE_OVAL; + $endArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_3; + $majorGridlines->setLineStyleProperties( + $width, + $compound, + $dash, + $cap, + $join, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize + ); + $minorGridlines = new GridLines(); + $minorGridlines->setLineColorProperties('00FF00', 30, 'srgbClr'); + + self::assertEquals($width, $majorGridlines->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleArrowWidth('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowLength('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowParameters('end', 'len')); + + self::assertSame('00FF00', $minorGridlines->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines->getLineColorProperty('type')); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + null, // xAxis + null, // yAxis + $majorGridlines, + $minorGridlines // minorGridlines + ); + $majorGridlines2 = $chart->getMajorGridlines(); + self::assertEquals($width, $majorGridlines2->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines2->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines2->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines2->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines2->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines2 = $chart->getMinorGridlines(); + self::assertSame('00FF00', $minorGridlines2->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines2->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines2->getLineColorProperty('type')); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $majorGridlines3 = $chart2->getMajorGridlines(); + self::assertEquals($width, $majorGridlines3->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines3->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines3->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines3->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines3->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($endArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines3 = $chart2->getMinorGridlines(); + self::assertSame('00FF00', $minorGridlines3->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines3->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines3->getLineColorProperty('type')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} From 23e207ccb43254afd5095d042de3ed32c8078477 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 17 Jun 2022 12:30:04 +0200 Subject: [PATCH 024/156] Adjust calc engine identification of quoted worksheet names to fix bug with multiple quoted worksheets in formula --- .../Calculation/Calculation.php | 10 ++-- .../Calculation/ParseFormulaTest.php | 52 ++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index c4d59d39..6fbf5da4 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -33,17 +33,17 @@ class Calculation // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) - const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative - const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; - const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\".(?:[^\"]|\"\")?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; - const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; + const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; + const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative // Cell ranges ensuring absolute/relative const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})'; // Defined Names: Named Range of cells, or Named Formulae - const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'\')+?\')|(\"(?:[^\"]|\"\")+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; + const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; diff --git a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php index 57b9271b..a9b79710 100644 --- a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php @@ -170,10 +170,34 @@ class ParseFormulaTest extends TestCase ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], ['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], - ['type' => 'Binary Operator', 'value' => '+','reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], ], "=MIN('sheet1'!A:A) + 'sheet1'!A1", ], + 'Combined Cell Reference and Column Range with quote' => [ + [ + ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1", 'reference' => "'Mark''s sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1048576", 'reference' => "'Mark''s sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "=MIN('Mark''s sheet1'!A:A) + 'Mark''s sheet1'!A1", + ], + 'Combined Cell Reference and Column Range with unescaped quote' => [ + [ + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "=MIN('Mark's sheet1'!A:A) + 'Mark's sheet1'!A1", + ], 'Combined Column Range and Cell Reference' => [ [ ['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], @@ -182,10 +206,34 @@ class ParseFormulaTest extends TestCase ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], - ['type' => 'Binary Operator', 'value' => '+','reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], ], "='sheet1'!A1 + MIN('sheet1'!A:A)", ], + 'Combined Column Range and Cell Reference with quote' => [ + [ + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1", 'reference' => "'Mark''s sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1048576", 'reference' => "'Mark''s sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "='Mark''s sheet1'!A1 + MIN('Mark''s sheet1'!A:A)", + ], + 'Combined Column Range and Cell Reference with unescaped quote' => [ + [ + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "='Mark's sheet1'!A1 + MIN('Mark's sheet1'!A:A)", + ], 'Range with Defined Names' => [ [ ['type' => 'Defined Name', 'value' => 'GROUP1', 'reference' => 'GROUP1'], From 02c6e8cfa81bc4c9e400a76af38a917e67761790 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 17 Jun 2022 13:34:20 +0200 Subject: [PATCH 025/156] Escape double quotes in worksheet names for column range and row range references --- src/PhpSpreadsheet/Calculation/Calculation.php | 6 +++++- .../PhpSpreadsheetTests/Calculation/ParseFormulaTest.php | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 6fbf5da4..1b7f985e 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4203,7 +4203,7 @@ class Calculation $expectingOperator = false; } $stack->push('Brace', '('); - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) { // Watch for this case-change when modifying to allow cell references in different worksheets... // Should only be applied to the actual cell column, not the worksheet name // If the last entry on the stack was a : operator, then we have a cell range reference @@ -4326,6 +4326,8 @@ class Calculation $val = $rowRangeReference[1]; $length = strlen($rowRangeReference[1]); $stackItemType = 'Row Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); $column = 'A'; if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { $column = $pCellParent->getHighestDataColumn($val); @@ -4338,6 +4340,8 @@ class Calculation $val = $columnRangeReference[1]; $length = strlen($val); $stackItemType = 'Column Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); $row = '1'; if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { $row = $pCellParent->getHighestDataRow($val); diff --git a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php index a9b79710..0ecb3f8b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php @@ -176,8 +176,8 @@ class ParseFormulaTest extends TestCase ], 'Combined Cell Reference and Column Range with quote' => [ [ - ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1", 'reference' => "'Mark''s sheet1'!A1"], - ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1048576", 'reference' => "'Mark''s sheet1'!A1048576"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], @@ -213,8 +213,8 @@ class ParseFormulaTest extends TestCase 'Combined Column Range and Cell Reference with quote' => [ [ ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], - ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1", 'reference' => "'Mark''s sheet1'!A1"], - ['type' => 'Column Reference', 'value' => "'Mark''s sheet1'!A1048576", 'reference' => "'Mark''s sheet1'!A1048576"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], From 27f815a9f1dfb49c829745c96d506acbac07506b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 17 Jun 2022 14:10:35 +0200 Subject: [PATCH 026/156] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6736bfb..9148425d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) - Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) +- Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) ## 1.23.0 - 2022-04-24 From 4ae947ce648d831d350b27a8228c5fedc39dd51b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 19 Jun 2022 11:33:09 +0200 Subject: [PATCH 027/156] Update php codesniffer --- composer.lock | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index 50011b16..9945b9a2 100644 --- a/composer.lock +++ b/composer.lock @@ -2125,10 +2125,6 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "support": { - "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.7.7" - }, "funding": [ { "url": "https://github.com/ondrejmirtes", @@ -3814,16 +3810,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.7.0", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563" + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563", - "reference": "a2cd51b45bcaef9c1f2a4bda48f2dd2fa2b95563", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", "shasum": "" }, "require": { @@ -3861,7 +3857,7 @@ "phpcs", "standards" ], - "time": "2022-06-13T06:31:38+00:00" + "time": "2022-06-18T07:21:10+00:00" }, { "name": "symfony/console", From 6fae406aca4e355af88758a5f0150d63ab45bbd0 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 19 Jun 2022 19:11:40 -0700 Subject: [PATCH 028/156] New Class ChartColor and Refactoring (#2898) * New Class ChartColor and Refactoring Chart colors are written to Xml in a different manner than font colors, and there are several variations. It will simplify things to create a new class for them. This PR will make use of the new class in Property/Axis/Gridline glow, shadow, and line colors; in Axis fill color; and in Font underline color (used only in charts). It will be used elsewhere in future; in particular, DataSeriesValues, which I will tackle next, will use it for at least one existing and two new properties. This PR is a refactoring; no functionality is added. Some public functions are moved from Properties to ChartColor, but all of these have been introduced after the last release 1.23, so there isn't really any compatibility break. No tests needed to be revised as a result of the source changes. * Simplify Logic in Xlsx/Writer/Chart Minor change. --- src/PhpSpreadsheet/Chart/Axis.php | 30 +- src/PhpSpreadsheet/Chart/ChartColor.php | 133 ++++++++ src/PhpSpreadsheet/Chart/Properties.php | 284 +++++++++--------- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 24 +- src/PhpSpreadsheet/Style/Font.php | 62 ++-- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 97 +++--- .../Writer/Xlsx/StringTable.php | 21 +- 7 files changed, 388 insertions(+), 263 deletions(-) create mode 100644 src/PhpSpreadsheet/Chart/ChartColor.php diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 09a4febc..69a25d92 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -10,6 +10,12 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class Axis extends Properties { + public function __construct() + { + parent::__construct(); + $this->fillColor = new ChartColor(); + } + /** * Axis Number. * @@ -42,13 +48,9 @@ class Axis extends Properties /** * Fill Properties. * - * @var mixed[] + * @var ChartColor */ - private $fillProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; + private $fillColor; private const NUMERIC_FORMAT = [ Properties::FORMAT_CODE_NUMBER, @@ -163,7 +165,7 @@ class Axis extends Properties */ public function setFillParameters($color, $alpha = null, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void { - $this->fillProperties = $this->setColorProperties($color, $alpha, $AlphaType); + $this->fillColor->setColorProperties($color, $alpha, $AlphaType); } /** @@ -175,19 +177,29 @@ class Axis extends Properties */ public function getFillProperty($property) { - return (string) $this->fillProperties[$property]; + return (string) $this->fillColor->getColorProperty($property); + } + + public function getFillColorObject(): ChartColor + { + return $this->fillColor; } /** * Get Line Color Property. * + * @Deprecated 1.24.0 + * + * @See Properties::getLineColorProperty() + * Use the getLineColor property in the Properties class instead + * * @param string $propertyName * * @return null|int|string */ public function getLineProperty($propertyName) { - return $this->lineProperties['color'][$propertyName]; + return $this->getLineColorProperty($propertyName); } /** @var string */ diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php new file mode 100644 index 00000000..05c1bb9a --- /dev/null +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -0,0 +1,133 @@ +value; + } + + public function setValue(string $value): self + { + $this->value = $value; + + return $this; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getAlpha(): ?int + { + return $this->alpha; + } + + public function setAlpha(?int $alpha): self + { + $this->alpha = $alpha; + + return $this; + } + + /** + * @param null|float|int|string $alpha + */ + public function setColorProperties(?string $color, $alpha, ?string $type): self + { + if ($color !== null) { + $this->setValue($color); + } + if ($type !== null) { + $this->setType($type); + } + if ($alpha === null) { + $this->setAlpha(null); + } elseif (is_numeric($alpha)) { + $this->setAlpha((int) $alpha); + } + + return $this; + } + + public function setColorPropertiesArray(array $color): self + { + if (array_key_exists('value', $color) && is_string($color['value'])) { + $this->setValue($color['value']); + } + if (array_key_exists('type', $color) && is_string($color['type'])) { + $this->setType($color['type']); + } + if (array_key_exists('alpha', $color)) { + if ($color['alpha'] === null) { + $this->setAlpha(null); + } elseif (is_numeric($color['alpha'])) { + $this->setAlpha((int) $color['alpha']); + } + } + + return $this; + } + + /** + * Get Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getColorProperty($propertyName) + { + $retVal = null; + if ($propertyName === 'value') { + $retVal = $this->value; + } elseif ($propertyName === 'type') { + $retVal = $this->type; + } elseif ($propertyName === 'alpha') { + $retVal = $this->alpha; + } + + return $retVal; + } + + public static function alphaToXml(int $alpha): string + { + return (string) (100 - $alpha) . '000'; + } + + /** + * @param float|int|string $alpha + */ + public static function alphaFromXml($alpha): int + { + return 100 - ((int) $alpha / 1000); + } +} diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index c6b2d15b..a64a826f 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -10,15 +10,12 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ abstract class Properties { - const - EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; - const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; - const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; - const EXCEL_COLOR_TYPES = [ - self::EXCEL_COLOR_TYPE_ARGB, - self::EXCEL_COLOR_TYPE_SCHEME, - self::EXCEL_COLOR_TYPE_STANDARD, - ]; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_STANDARD = ChartColor::EXCEL_COLOR_TYPE_STANDARD; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_SCHEME = ChartColor::EXCEL_COLOR_TYPE_SCHEME; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_ARGB = ChartColor::EXCEL_COLOR_TYPE_ARGB; const AXIS_LABELS_LOW = 'low'; @@ -123,15 +120,11 @@ abstract class Properties /** @var bool */ protected $objectState = false; // used only for minor gridlines - /** @var array */ - protected $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; + /** @var ?float */ + protected $glowSize; + + /** @var ChartColor */ + protected $glowColor; /** @var array */ protected $softEdges = [ @@ -141,6 +134,19 @@ abstract class Properties /** @var array */ protected $shadowProperties = self::PRESETS_OPTIONS[0]; + /** @var ChartColor */ + protected $shadowColor; + + public function __construct() + { + $this->lineColor = new ChartColor(); + $this->glowColor = new ChartColor(); + $this->shadowColor = new ChartColor(); + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + /** * Get Object State. * @@ -193,19 +199,6 @@ abstract class Properties return ((float) $value) / self::PERCENTAGE_MULTIPLIER; } - public static function alphaToXml(int $alpha): string - { - return (string) (100 - $alpha) . '000'; - } - - /** - * @param float|int|string $alpha - */ - public static function alphaFromXml($alpha): int - { - return 100 - ((int) $alpha / 1000); - } - /** * @param null|float|int|string $alpha */ @@ -223,11 +216,11 @@ abstract class Properties 0 => [ 'presets' => self::SHADOW_PRESETS_NOSHADOW, 'effect' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], + //'color' => [ + // 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, + // 'value' => 'black', + // 'alpha' => 40, + //], 'size' => [ 'sx' => null, 'sy' => null, @@ -457,8 +450,14 @@ abstract class Properties { $this ->activateObject() - ->setGlowSize($size) - ->setGlowColor($colorValue, $colorAlpha, $colorType); + ->setGlowSize($size); + $this->glowColor->setColorPropertiesArray( + [ + 'value' => $colorValue, + 'type' => $colorType, + 'alpha' => $colorAlpha, + ] + ); } /** @@ -466,11 +465,24 @@ abstract class Properties * * @param array|string $property * - * @return null|string + * @return null|array|float|int|string */ public function getGlowProperty($property) { - return $this->getArrayElementsValue($this->glowProperties, $property); + $retVal = null; + if ($property === 'size') { + $retVal = $this->glowSize; + } elseif ($property === 'color') { + $retVal = [ + 'value' => $this->glowColor->getColorProperty('value'), + 'type' => $this->glowColor->getColorProperty('type'), + 'alpha' => $this->glowColor->getColorProperty('alpha'), + ]; + } elseif (is_array($property) && count($property) >= 2 && $property[0] === 'color') { + $retVal = $this->glowColor->getColorProperty($property[1]); + } + + return $retVal; } /** @@ -478,57 +490,38 @@ abstract class Properties * * @param string $propertyName * - * @return string + * @return null|int|string */ public function getGlowColor($propertyName) { - return $this->glowProperties['color'][$propertyName]; + return $this->glowColor->getColorProperty($propertyName); + } + + public function getGlowColorObject(): ChartColor + { + return $this->glowColor; } /** * Get Glow Size. * - * @return string + * @return ?float */ public function getGlowSize() { - return $this->glowProperties['size']; + return $this->glowSize; } /** * Set Glow Size. * - * @param float $size + * @param ?float $size * * @return $this */ protected function setGlowSize($size) { - $this->glowProperties['size'] = $size; - - return $this; - } - - /** - * Set Glow Color. - * - * @param ?string $color - * @param ?int $alpha - * @param ?string $colorType - * - * @return $this - */ - protected function setGlowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->glowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->glowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->glowProperties['color']['type'] = (string) $colorType; - } + $this->glowSize = $size; return $this; } @@ -562,7 +555,11 @@ abstract class Properties public function setShadowProperty(string $propertyName, $value): self { $this->activateObject(); - $this->shadowProperties[$propertyName] = $value; + if ($propertyName === 'color' && is_array($value)) { + $this->shadowColor->setColorPropertiesArray($value); + } else { + $this->shadowProperties[$propertyName] = $value; + } return $this; } @@ -580,13 +577,22 @@ abstract class Properties */ public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void { - $this->activateObject() - ->setShadowPresetsProperties((int) $presets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, - $colorType ?? $this->shadowProperties['color']['type'] - ) + $this->activateObject()->setShadowPresetsProperties((int) $presets); + if ($presets === 0) { + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + if ($colorValue !== null) { + $this->shadowColor->setValue($colorValue); + } + if ($colorType !== null) { + $this->shadowColor->setType($colorType); + } + if (is_numeric($colorAlpha)) { + $this->shadowColor->setAlpha((int) $colorAlpha); + } + $this ->setShadowBlur($blur) ->setShadowAngle($angle) ->setShadowDistance($distance); @@ -709,48 +715,62 @@ abstract class Properties return $this; } + public function getShadowColorObject(): ChartColor + { + return $this->shadowColor; + } + /** * Get Shadow Property. * * @param string|string[] $elements * - * @return string + * @return array|string */ public function getShadowProperty($elements) { + if ($elements === 'color') { + return [ + 'value' => $this->shadowColor->getValue(), + 'type' => $this->shadowColor->getType(), + 'alpha' => $this->shadowColor->getAlpha(), + ]; + } + return $this->getArrayElementsValue($this->shadowProperties, $elements); } + /** @var ChartColor */ + protected $lineColor; + /** @var array */ - protected $lineProperties = [ - 'color' => [ - 'type' => '', //self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => '', //null, - 'alpha' => null, - ], - 'style' => [ - 'width' => null, //'9525', - 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => '', //self::LINE_STYLE_DASH_SOLID, - 'cap' => '', //self::LINE_STYLE_CAP_FLAT, - 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, - 'w' => '', - 'len' => '', - ], - 'end' => [ - 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, - 'w' => '', - 'len' => '', - ], + protected $lineStyleProperties = [ + 'width' => null, //'9525', + 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => '', //self::LINE_STYLE_DASH_SOLID, + 'cap' => '', //self::LINE_STYLE_CAP_FLAT, + 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, + 'w' => '', + 'len' => '', + ], + 'end' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, + 'w' => '', + 'len' => '', ], ], ]; + public function getLineColor(): ChartColor + { + return $this->lineColor; + } + /** * Set Line Color Properties. * @@ -758,20 +778,16 @@ abstract class Properties * @param ?int $alpha * @param string $colorType */ - public function setLineColorProperties($value, $alpha = null, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void + public function setLineColorProperties($value, $alpha = null, $colorType = ChartColor::EXCEL_COLOR_TYPE_STANDARD): void { - $this->activateObject() - ->lineProperties['color'] = $this->setColorProperties( + $this->activateObject(); + $this->lineColor->setColorPropertiesArray( + $this->setColorProperties( $value, $alpha, $colorType - ); - } - - public function setColorPropertiesArray(array $color): void - { - $this->activateObject() - ->lineProperties['color'] = $color; + ) + ); } /** @@ -783,7 +799,7 @@ abstract class Properties */ public function getLineColorProperty($propertyName) { - return $this->lineProperties['color'][$propertyName]; + return $this->lineColor->getColorProperty($propertyName); } /** @@ -807,47 +823,47 @@ abstract class Properties { $this->activateObject(); if (is_numeric($lineWidth)) { - $this->lineProperties['style']['width'] = $lineWidth; + $this->lineStyleProperties['width'] = $lineWidth; } if ($compoundType !== '') { - $this->lineProperties['style']['compound'] = $compoundType; + $this->lineStyleProperties['compound'] = $compoundType; } if ($dashType !== '') { - $this->lineProperties['style']['dash'] = $dashType; + $this->lineStyleProperties['dash'] = $dashType; } if ($capType !== '') { - $this->lineProperties['style']['cap'] = $capType; + $this->lineStyleProperties['cap'] = $capType; } if ($joinType !== '') { - $this->lineProperties['style']['join'] = $joinType; + $this->lineStyleProperties['join'] = $joinType; } if ($headArrowType !== '') { - $this->lineProperties['style']['arrow']['head']['type'] = $headArrowType; + $this->lineStyleProperties['arrow']['head']['type'] = $headArrowType; } if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { - $this->lineProperties['style']['arrow']['head']['size'] = $headArrowSize; - $this->lineProperties['style']['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; - $this->lineProperties['style']['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; + $this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize; + $this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; + $this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; } if ($endArrowType !== '') { - $this->lineProperties['style']['arrow']['end']['type'] = $endArrowType; + $this->lineStyleProperties['arrow']['end']['type'] = $endArrowType; } if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { - $this->lineProperties['style']['arrow']['end']['size'] = $endArrowSize; - $this->lineProperties['style']['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; - $this->lineProperties['style']['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; + $this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize; + $this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; + $this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; } if ($headArrowWidth !== '') { - $this->lineProperties['style']['arrow']['head']['w'] = $headArrowWidth; + $this->lineStyleProperties['arrow']['head']['w'] = $headArrowWidth; } if ($headArrowLength !== '') { - $this->lineProperties['style']['arrow']['head']['len'] = $headArrowLength; + $this->lineStyleProperties['arrow']['head']['len'] = $headArrowLength; } if ($endArrowWidth !== '') { - $this->lineProperties['style']['arrow']['end']['w'] = $endArrowWidth; + $this->lineStyleProperties['arrow']['end']['w'] = $endArrowWidth; } if ($endArrowLength !== '') { - $this->lineProperties['style']['arrow']['end']['len'] = $endArrowLength; + $this->lineStyleProperties['arrow']['end']['len'] = $endArrowLength; } } @@ -860,7 +876,7 @@ abstract class Properties */ public function getLineStyleProperty($elements) { - return $this->getArrayElementsValue($this->lineProperties['style'], $elements); + return $this->getArrayElementsValue($this->lineStyleProperties, $elements); } protected const ARROW_SIZES = [ @@ -890,7 +906,7 @@ abstract class Properties */ public function getLineStyleArrowParameters($arrowSelector, $propertySelector) { - return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); + return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrowSelector]['size'], $propertySelector); } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index b046bc24..2c060d07 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\GridLines; @@ -756,7 +757,7 @@ class Chart $complexScript = null; $fontSrgbClr = ''; $fontSchemeClr = ''; - $uSchemeClr = null; + $underlineColor = null; if (isset($titleDetailElement->rPr)) { // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { @@ -797,9 +798,8 @@ class Chart /** @var ?string */ $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); - if (isset($titleDetailElement->rPr->uFill->solidFill->schemeClr)) { - /** @var ?string */ - $uSchemeClr = self::getAttribute($titleDetailElement->rPr->uFill->solidFill->schemeClr, 'val', 'string'); + if (isset($titleDetailElement->rPr->uFill->solidFill)) { + $underlineColor = $this->readColor($titleDetailElement->rPr->uFill->solidFill); } /** @var ?string */ @@ -883,8 +883,8 @@ class Chart $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); } $fontFound = true; - if ($uSchemeClr) { - $objText->getFont()->setUSchemeClr($uSchemeClr); + if ($underlineColor) { + $objText->getFont()->setUnderlineColor($underlineColor); } } @@ -1066,7 +1066,7 @@ class Chart 'value' => null, 'alpha' => null, ]; - foreach (Properties::EXCEL_COLOR_TYPES as $type) { + foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { if (isset($colorXml->$type)) { $result['type'] = $type; $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string'); @@ -1078,9 +1078,11 @@ class Chart $prstClr = $result['value']; } if (isset($colorXml->$type->alpha)) { - $alpha = (int) self::getAttribute($colorXml->$type->alpha, 'val', 'string'); - $alpha = 100 - (int) ($alpha / 1000); - $result['alpha'] = $alpha; + /** @var string */ + $alpha = self::getAttribute($colorXml->$type->alpha, 'val', 'string'); + if (is_numeric($alpha)) { + $result['alpha'] = ChartColor::alphaFromXml($alpha); + } } break; @@ -1154,7 +1156,7 @@ class Chart $endArrowLength ); $colorArray = $this->readColor($sppr->ln->solidFill); - $chartObject->setColorPropertiesArray($colorArray); + $chartObject->getLineColor()->setColorPropertiesArray($colorArray); } private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 7b1ced63..4dbe7272 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Style; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; + class Font extends Supervisor { // Underline types @@ -19,7 +21,7 @@ class Font extends Supervisor protected $name = 'Calibri'; /** - * The following 7 are used only for chart titles, I think. + * The following 6 are used only for chart titles, I think. * *@var string */ @@ -37,11 +39,8 @@ class Font extends Supervisor /** @var string */ private $strikeType = ''; - /** @var string */ - private $uSchemeClr = ''; - - /** @var string */ - private $uSrgbClr = ''; + /** @var ?ChartColor */ + private $underlineColor; // end of chart title items /** @@ -582,47 +581,24 @@ class Font extends Supervisor return $this; } - public function getUSchemeClr(): string + public function getUnderlineColor(): ?ChartColor { if ($this->isSupervisor) { - return $this->getSharedComponent()->getUSchemeClr(); + return $this->getSharedComponent()->getUnderlineColor(); } - return $this->uSchemeClr; + return $this->underlineColor; } - public function setUSchemeClr(string $uSchemeClr): self + public function setUnderlineColor(array $colorArray): self { if (!$this->isSupervisor) { - $this->uSchemeClr = $uSchemeClr; + $this->underlineColor = new ChartColor(); + $this->underlineColor->setColorPropertiesArray($colorArray); } else { // should never be true // @codeCoverageIgnoreStart - $styleArray = $this->getStyleArray(['uSchemeClr' => $uSchemeClr]); - $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); - // @codeCoverageIgnoreEnd - } - - return $this; - } - - public function getUSrgbClr(): string - { - if ($this->isSupervisor) { - return $this->getSharedComponent()->getUSrgbClr(); - } - - return $this->uSrgbClr; - } - - public function setUSrgbClr(string $uSrgbClr): self - { - if (!$this->isSupervisor) { - $this->uSrgbClr = $uSrgbClr; - } else { - // should never be true - // @codeCoverageIgnoreStart - $styleArray = $this->getStyleArray(['uSrgbClr' => $uSrgbClr]); + $styleArray = $this->getStyleArray(['underlineColor' => $colorArray]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); // @codeCoverageIgnoreEnd } @@ -747,6 +723,14 @@ class Font extends Supervisor if ($this->isSupervisor) { return $this->getSharedComponent()->getHashCode(); } + if ($this->underlineColor === null) { + $underlineColor = ''; + } else { + $underlineColor = + $this->underlineColor->getValue() + . $this->underlineColor->getType() + . (string) $this->underlineColor->getAlpha(); + } return md5( $this->name . @@ -765,8 +749,7 @@ class Font extends Supervisor $this->eastAsian, $this->complexScript, $this->strikeType, - $this->uSchemeClr, - $this->uSrgbClr, + $underlineColor, (string) $this->baseLine, ] ) . @@ -791,8 +774,7 @@ class Font extends Supervisor $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); - $this->exportArray2($exportedArray, 'uSchemeClr', $this->getUSchemeClr()); - $this->exportArray2($exportedArray, 'uSrgbClr', $this->getUSrgbClr()); + $this->exportArray2($exportedArray, 'underlineColor', $this->getUnderlineColor()); return $exportedArray; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 1bdf4fe1..acc6f3af 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\GridLines; @@ -517,19 +518,8 @@ class Chart extends WriterPart } $objWriter->startElement('c:spPr'); - if (!empty($yAxis->getFillProperty('value'))) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $yAxis->getFillProperty('type')); - $objWriter->writeAttribute('val', $yAxis->getFillProperty('value')); - $alpha = $yAxis->getFillProperty('alpha'); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); - } - $objWriter->endElement(); - $objWriter->endElement(); - } + $this->writeColor($objWriter, $yAxis->getFillColorObject()); + $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $yAxis); $this->writeShadow($objWriter, $yAxis); @@ -723,19 +713,7 @@ class Chart extends WriterPart $objWriter->startElement('c:spPr'); - if (!empty($xAxis->getFillProperty('value'))) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getFillProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('value')); - $alpha = $xAxis->getFillProperty('alpha'); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); - } - $objWriter->endElement(); - $objWriter->endElement(); - } + $this->writeColor($objWriter, $xAxis->getFillColorObject()); $this->writeGridlinesLn($objWriter, $xAxis); @@ -1472,8 +1450,9 @@ class Chart extends WriterPart if (is_numeric($xAxis->getShadowProperty('direction'))) { $objWriter->writeAttribute('dir', Properties::angleToXml((float) $xAxis->getShadowProperty('direction'))); } - if ($xAxis->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn')); + $algn = $xAxis->getShadowProperty('algn'); + if (is_string($algn) && $algn !== '') { + $objWriter->writeAttribute('algn', $algn); } foreach (['sx', 'sy'] as $sizeType) { $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]); @@ -1487,19 +1466,12 @@ class Chart extends WriterPart $objWriter->writeAttribute($sizeType, Properties::angleToXml((float) $sizeValue)); } } - if ($xAxis->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape')); + $rotWithShape = $xAxis->getShadowProperty('rotWithShape'); + if (is_numeric($rotWithShape)) { + $objWriter->writeAttribute('rotWithShape', (string) (int) $rotWithShape); } - $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value'])); - $alpha = $xAxis->getShadowProperty(['color', 'alpha']); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); - } - $objWriter->endElement(); + $this->writeColor($objWriter, $xAxis->getShadowColorObject(), false); $objWriter->endElement(); } @@ -1517,15 +1489,7 @@ class Chart extends WriterPart } $objWriter->startElement('a:glow'); $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $size)); - $objWriter->startElement("a:{$yAxis->getGlowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', (string) $yAxis->getGlowProperty(['color', 'value'])); - $alpha = $yAxis->getGlowProperty(['color', 'alpha']); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); // alpha - } - $objWriter->endElement(); // color + $this->writeColor($objWriter, $yAxis->getGlowColorObject(), false); $objWriter->endElement(); // glow } @@ -1559,19 +1523,7 @@ class Chart extends WriterPart } $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap')); $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound')); - if (!empty($gridlines->getLineColorProperty('value'))) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$gridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', (string) $gridlines->getLineColorProperty('value')); - $alpha = $gridlines->getLineColorProperty('alpha'); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); // alpha - } - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - } + $this->writeColor($objWriter, $gridlines->getLineColor()); $dash = $gridlines->getLineStyleProperty('dash'); if (!empty($dash)) { @@ -1613,4 +1565,27 @@ class Chart extends WriterPart $objWriter->writeAttribute($name, $value); } } + + private function writeColor(XMLWriter $objWriter, ChartColor $chartColor, bool $solidFill = true): void + { + $type = $chartColor->getType(); + $value = $chartColor->getValue(); + if (!empty($type) && !empty($value)) { + if ($solidFill) { + $objWriter->startElement('a:solidFill'); + } + $objWriter->startElement("a:$type"); + $objWriter->writeAttribute('val', $value); + $alpha = $chartColor->getAlpha(); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr + if ($solidFill) { + $objWriter->endElement(); //a:solidFill + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index da7d825b..8a376df4 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -256,14 +256,19 @@ class StringTable extends WriterPart $objWriter->endElement(); // solidFill // Underscore Color - if ($element->getFont()->getUSchemeClr()) { - $objWriter->startElement($prefix . 'uFill'); - $objWriter->startElement($prefix . 'solidFill'); - $objWriter->startElement($prefix . 'schemeClr'); - $objWriter->writeAttribute('val', $element->getFont()->getUSchemeClr()); - $objWriter->endElement(); // schemeClr - $objWriter->endElement(); // solidFill - $objWriter->endElement(); // uFill + $underlineColor = $element->getFont()->getUnderlineColor(); + if ($underlineColor !== null) { + $type = $underlineColor->getType(); + $value = $underlineColor->getValue(); + if (!empty($type) && !empty($value)) { + $objWriter->startElement($prefix . 'uFill'); + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . $type); + $objWriter->writeAttribute('val', $value); + $objWriter->endElement(); // schemeClr + $objWriter->endElement(); // solidFill + $objWriter->endElement(); // uFill + } } // fontName From 177a362f3811282cf975b4f4c3eef530b52bc78c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 19 Jun 2022 19:22:10 -0700 Subject: [PATCH 029/156] Have Phpstan Ignore Chart/Renderer/JpGraph (#2901) This one class consumes a lot of space in Phpstan baseline. The problem is that it is an interface to Jpgraph, which is not maintained in Composer. This means that we have to disable tests involving this module, since we are dealing with very old code in our test suite. This means that we are very unlikely to do any work on this member, so the code error reports are more of a distraction than anything else. Remove them for now, restoring them if we ever solve this problem. --- phpstan-baseline.neon | 280 ------------------------------------------ phpstan.neon.dist | 8 +- 2 files changed, 2 insertions(+), 286 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1491a665..d1f2a03b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1225,286 +1225,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Chart/Properties.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$datasetLabels with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$labelCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$rotation with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatPointMarker\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatPointMarker\\(\\) has parameter \\$markerID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatPointMarker\\(\\) has parameter \\$seriesPlot with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:getCaption\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:getCaption\\(\\) has parameter \\$captionElement with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageAdjustValues\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageAdjustValues\\(\\) has parameter \\$dataValues with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageAdjustValues\\(\\) has parameter \\$sumValues with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageSumCalculation\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageSumCalculation\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageSumCalculation\\(\\) has parameter \\$seriesCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderAreaChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderAreaChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderBarChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderBarChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderBubbleChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCartesianPlotArea\\(\\) has parameter \\$type with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has parameter \\$outputDestination with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderContourChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderContourChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderLineChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderLineChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$doughnut with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$multiplePlots with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotBar\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotBar\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotContour\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$combination with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$filled with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotRadar\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotScatter\\(\\) has parameter \\$bubble with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotScatter\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotStock\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderRadarChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderScatterChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderStockChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$chart has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$colourSet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$graph has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$height has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$markSet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$plotColour has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$plotMark has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$width has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" count: 1 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0979eaed..61672b28 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,6 +9,8 @@ parameters: paths: - src/ - tests/ + excludePaths: + - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php parallel: processTimeout: 300.0 checkMissingIterableValueType: false @@ -19,12 +21,6 @@ parameters: - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' - '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' - # Ignore all JpGraph issues - - '~^Constant (MARK_CIRCLE|MARK_CROSS|MARK_DIAMOND|MARK_DTRIANGLE|MARK_FILLEDCIRCLE|MARK_SQUARE|MARK_STAR|MARK_UTRIANGLE|MARK_X|SIDE_RIGHT) not found\.$~' - - '~^Instantiated class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot) not found\.$~' - - '~^Call to method .*\(\) on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' - - '~^Access to property .* on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' - # Some issues in Xls/Parser between 1.6.3 and 1.7.7 - message: "#^Offset '(left|right|value)' does not exist on (non-empty-array\\|string|array\\|null)\\.$#" From a89572107a655ac3fe8107034c7c1a3acb7b4740 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 25 Jun 2022 22:08:32 -0700 Subject: [PATCH 030/156] Handling of #REF! Errors in Subtotal, and More (#2902) * Handling of #REF! Errors in Subtotal, and More This PR derives from, and supersedes, PR #2870, submitted by @ndench. The problem reported in the original is that SUBTOTAL does not handle #REF! errors in its arguments properly; however, my investigation has enlarged the scope. The main problem is in Calculation, and it has a simple fix. When the calculation engine finds a reference to an uninitialized cell, it uses `null` as the value. This is appropriate when the cell belongs to a defined sheet; however, for an undefined sheet, #REF! is more appropriate. With that fix in place, SUBTOTAL still needs a small fix of its own. It tries to parse its cell reference arguments into an array, but, if the reference does not match the expected format (as #REF! will not), this results in referencing undefined array indexes, with attendant messages. That assignment is changed to be more flexible, eliminating the problem and the messages. Those 2 fixes are sufficient to ensure that the original problem is resolved. It also resolves a similar problem with some other functions (e.g. SUM). However, it does not resolve it for all functions. Or, to be more particular, many functions will return #VALUE! rather than #REF! if this arises, and the same is true for other errors in the function arguments, e.g. #DIV/0!. This PR does not attempt to address all functions; I need to think of a systematic way to pursue that. However, at least for most MathTrig functions, which validate their arguments using a common method, it is relatively easy to get the function to propagate the proper error result. * Arrange Array The Way call_user_func_array Wants Problem with Php8.0+ - array passed to call_user_func_array must have int keys before string keys, otherwise Php thinks we are passing positional parameters after keyword parameters. 7 other functions use flattenArrayIndexed, but Subtotal is the only one which uses that result to subsequently pass arguments to call_user_func_array. So the others should not require a change. A specific test is added for SUM to validate that conclusion. * Change Needed for Hidden Row Filter Same as change made to Formula Args filter. --- .../Calculation/Calculation.php | 2 +- .../Calculation/Information/ExcelError.php | 8 +++ .../Calculation/MathTrig/Helpers.php | 4 +- .../Calculation/MathTrig/Operations.php | 2 +- .../Calculation/MathTrig/Subtotal.php | 27 ++++++++- .../Functions/MathTrig/SubTotalTest.php | 28 +++++++++ .../Calculation/RefErrorTest.php | 58 +++++++++++++++++++ 7 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 516faa86..4e3a7c53 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4834,7 +4834,7 @@ class Calculation $cell->attach($pCellParent); } else { $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef; - $cellValue = null; + $cellValue = ($cellSheet !== null) ? null : Information\ExcelError::REF(); } } else { return $this->raiseFormulaError('Unable to access Cell Reference'); diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 88de7e54..6305e502 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -25,6 +25,14 @@ class ExcelError 'spill' => '#SPILL!', ]; + /** + * @param mixed $value + */ + public static function throwError($value): string + { + return in_array($value, self::$errorCodes, true) ? $value : self::$errorCodes['value']; + } + /** * ERROR_TYPE. * diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php index 348aa5b3..f34f159b 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -38,7 +38,7 @@ class Helpers return 0 + $number; } - throw new Exception(ExcelError::VALUE()); + throw new Exception(ExcelError::throwError($number)); } /** @@ -59,7 +59,7 @@ class Helpers return 0 + $number; } - throw new Exception(ExcelError::VALUE()); + throw new Exception(ExcelError::throwError($number)); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php index f26da389..06258451 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -118,7 +118,7 @@ class Operations if (is_numeric($arg)) { $returnValue *= $arg; } else { - return ExcelError::VALUE(); + return ExcelError::throwError($arg); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php b/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php index 336bc690..6d8f4723 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php @@ -18,7 +18,11 @@ class Subtotal return array_filter( $args, function ($index) use ($cellReference) { - [, $row, ] = explode('.', $index); + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + if (!is_numeric($row)) { + return true; + } return $cellReference->getWorksheet()->getRowDimension($row)->getVisible(); }, @@ -35,7 +39,9 @@ class Subtotal return array_filter( $args, function ($index) use ($cellReference) { - [, $row, $column] = explode('.', $index); + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + $column = $explodeArray[2] ?? ''; $retVal = true; if ($cellReference->getWorksheet()->cellExists($column . $row)) { //take this cell out if it contains the SUBTOTAL or AGGREGATE functions in a formula @@ -87,7 +93,22 @@ class Subtotal public static function evaluate($functionType, ...$args) { $cellReference = array_pop($args); - $aArgs = Functions::flattenArrayIndexed($args); + $bArgs = Functions::flattenArrayIndexed($args); + $aArgs = []; + // int keys must come before string keys for PHP 8.0+ + // Otherwise, PHP thinks positional args follow keyword + // in the subsequent call to call_user_func_array. + // Fortunately, order of args is unimportant to Subtotal. + foreach ($bArgs as $key => $value) { + if (is_int($key)) { + $aArgs[$key] = $value; + } + } + foreach ($bArgs as $key => $value) { + if (!is_int($key)) { + $aArgs[$key] = $value; + } + } try { $subtotal = (int) Helpers::validateNumericNullBool($functionType); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php index cf79ac0b..43156182 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php @@ -127,4 +127,32 @@ class SubTotalTest extends AllSetupTeardown $sheet->getCell('H1')->setValue("=SUBTOTAL(9, A1:$maxCol$maxRow)"); self::assertEquals(362, $sheet->getCell('H1')->getCalculatedValue()); } + + public function testRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, #REF!)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } + + public function testSecondaryRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, B1:B9,#REF!,C1:C9)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } + + public function testNonStringSingleCellRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, 1, C1, Sheet99!A11)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } + + public function testNonStringCellRangeRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, Sheet99!A1)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php b/tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php new file mode 100644 index 00000000..ffdfc2fa --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php @@ -0,0 +1,58 @@ +getActiveSheet(); + $sheet1->setTitle('Sheet1'); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Sheet2'); + $sheet2->getCell('A1')->setValue(5); + $sheet1->getCell('A1')->setValue(9); + $sheet1->getCell('A2')->setValue(2); + $sheet1->getCell('A3')->setValue(4); + $sheet1->getCell('A4')->setValue(6); + $sheet1->getCell('A5')->setValue(7); + $sheet1->getRowDimension(5)->setVisible(false); + $sheet1->getCell('B1')->setValue('=1/0'); + $sheet1->getCell('C1')->setValue('=Sheet99!A1'); + $sheet1->getCell('C2')->setValue('=Sheet2!A1'); + $sheet1->getCell('C3')->setValue('=Sheet2!A2'); + $sheet1->getCell('H1')->setValue($formula); + self::assertSame($expected, $sheet1->getCell('H1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function providerRefError(): array + { + return [ + 'Subtotal9 Ok' => [12, '=SUBTOTAL(A1,A2:A4)'], + 'Subtotal9 REF' => ['#REF!', '=SUBTOTAL(A1,A2:A4,C1)'], + 'Subtotal9 with literal and cells' => [111, '=SUBTOTAL(A1,A2:A4,99)'], + 'Subtotal9 with literal no rows hidden' => [111, '=SUBTOTAL(109,A2:A4,99)'], + 'Subtotal9 with literal ignoring hidden row' => [111, '=SUBTOTAL(109,A2:A5,99)'], + 'Subtotal9 with literal using hidden row' => [118, '=SUBTOTAL(9,A2:A5,99)'], + 'Subtotal9 with Null same sheet' => [12, '=SUBTOTAL(A1,A2:A4,A99)'], + 'Subtotal9 with Null Different sheet' => [12, '=SUBTOTAL(A1,A2:A4,C3)'], + 'Subtotal9 with NonNull Different sheet' => [17, '=SUBTOTAL(A1,A2:A4,C2)'], + 'Product DIV0' => ['#DIV/0!', '=PRODUCT(2, 3, B1)'], + 'Sqrt REF' => ['#REF!', '=SQRT(C1)'], + 'Sum NUM' => ['#NUM!', '=SUM(SQRT(-1), A2:A4)'], + 'Sum with literal and cells' => [111, '=SUM(A2:A4, 99)'], + 'Sum REF' => ['#REF!', '=SUM(A2:A4, C1)'], + 'Tan DIV0' => ['#DIV/0!', '=TAN(B1)'], + ]; + } +} From b5b83abc0e8d516474bac75c83c3cfc041c0497b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 29 Jun 2022 09:20:33 -0700 Subject: [PATCH 031/156] Adjust Both Coordinates for Two-Cell Anchors (#2909) Fix #2908. When support for two-cell anchors was added for drawings, we neglected to adjust the second cell address when rows or columns are added or deleted. It also appears that "twoCell" and "oneCell" were introduced as lower-case literals when support for the editAs attribute was subsequently introduced. --- src/PhpSpreadsheet/ReferenceHelper.php | 6 +++ src/PhpSpreadsheet/Worksheet/BaseDrawing.php | 6 +-- .../Writer/Xlsx/DrawingsInsertRowsTest.php | 45 ++++++++++++++++++ .../Writer/Xlsx/DrawingsTest.php | 8 ++-- tests/data/Writer/XLSX/issue.2908.xlsx | Bin 0 -> 120924 bytes 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php create mode 100644 tests/data/Writer/XLSX/issue.2908.xlsx diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 8caaab18..59247f89 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -525,6 +525,12 @@ class ReferenceHelper if ($objDrawing->getCoordinates() != $newReference) { $objDrawing->setCoordinates($newReference); } + if ($objDrawing->getCoordinates2() !== '') { + $newReference = $this->updateCellReference($objDrawing->getCoordinates2()); + if ($objDrawing->getCoordinates2() != $newReference) { + $objDrawing->setCoordinates2($newReference); + } + } } // Update workbook: define names diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 815536b5..369e4162 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -9,8 +9,8 @@ use PhpOffice\PhpSpreadsheet\IComparable; class BaseDrawing implements IComparable { const EDIT_AS_ABSOLUTE = 'absolute'; - const EDIT_AS_ONECELL = 'onecell'; - const EDIT_AS_TWOCELL = 'twocell'; + const EDIT_AS_ONECELL = 'oneCell'; + const EDIT_AS_TWOCELL = 'twoCell'; private const VALID_EDIT_AS = [ self::EDIT_AS_ABSOLUTE, self::EDIT_AS_ONECELL, @@ -530,6 +530,6 @@ class BaseDrawing implements IComparable public function validEditAs(): bool { - return in_array($this->editAs, self::VALID_EDIT_AS); + return in_array($this->editAs, self::VALID_EDIT_AS, true); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php new file mode 100644 index 00000000..e2cbbff3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php @@ -0,0 +1,45 @@ +load($inputFilename); + $sheet = $spreadsheet->getActiveSheet(); + $drawingCollection = $sheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection); + $drawing = $drawingCollection[0]; + self::assertNotNull($drawing); + self::assertSame('D10', $drawing->getCoordinates()); + self::assertSame('F11', $drawing->getCoordinates2()); + self::assertSame('oneCell', $drawing->getEditAs()); + + $sheet->insertNewRowBefore(5); + $sheet->insertNewRowBefore(6); + + // Save spreadsheet to file and read it back + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $drawingCollection2 = $rsheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection2); + $drawing2 = $drawingCollection2[0]; + self::assertNotNull($drawing2); + self::assertSame('D12', $drawing2->getCoordinates()); + self::assertSame('F13', $drawing2->getCoordinates2()); + self::assertSame('oneCell', $drawing2->getEditAs()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index f089b9ee..14b816d2 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -554,10 +554,10 @@ class DrawingsTest extends AbstractFunctional { return [ 'absolute' => ['absolute'], - 'onecell' => ['onecell'], - 'twocell' => ['twocell'], - 'unset (will be treated as twocell)' => [''], - 'unknown (will be treated as twocell)' => ['unknown', ''], + 'onecell' => ['oneCell'], + 'twocell' => ['twoCell'], + 'unset (will be treated as twoCell)' => [''], + 'unknown (will be treated as twoCell)' => ['unknown', ''], ]; } diff --git a/tests/data/Writer/XLSX/issue.2908.xlsx b/tests/data/Writer/XLSX/issue.2908.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3dcd7923f179933b1304d050116c9defb8dcd28d GIT binary patch literal 120924 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

EuXj^5awS+6XJ`$~_?ng}f9%@w; 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

-HFasC6Fm$;G<1-_;WoZgWn}a5dLyEN#O{X{#WE@&h-AV&_yTFB;mXu>yrW<|c zRHOHtO7xmpgI+V5`L_#w$T$XOR%1+FJI3eRF($K>A&?XIJyD67FE(S&+kVV>%ZJQo zn{n@oVoY6EiOpZs!EVAu0v`=Sd$8`V1LnNeh6xKRFgT5EI=cz~>!CVacn?|5-AC|$ zO)bU$V|p1ztnr|$lhUMJg~_zqyl&jR$&1l>6&O0Bo@}!j=g#cHc?{9|WVB+`3OmNF zZNd=Nz2AJYtQDR7+kv43j-lBEsH_wQkf{t^+(K}4vz{)>aTkgO-fSx$4O!dc=@?N_ z5zP&W(JAPt#3OCMtNVReO~9M-bTjULqz<>Pt;Xo=LzKmFT)(CQ*Q_eV6?c6AZuXIPXF=kL;)0}g{&5}5k)t;Id^O2*ju zxh8`NmF+C+$NK}gd1)=i=GEhzxsCYK%x3&)T01V9*@TPena-s`|7lJuzjNZsMTe08 zZmU@g*U5&75HP}8xJHIxjr(hgyY)ncHdO?t z;o&56m>xA2)B2W9v_!mk>;R22w*q};*W$ui<+x-{3wli@dwH+{=g_dvpVdJo;9v-` z3)wH%Z6% zRpZZ-%5mP*2K?#4dh~nX1cp9%m_SucFsQ{tuhr-Cznq7%MPN~JkbE_DZR4`n#VMwl*?4uJGFKVM+)RUbsWXUF5&8%T~M7BD& z6{8s%ZTz4L%~W(9ffzS4ggdAQZq|b=gzdJcDT+t7IdSK+O&GtLVcU{s-rtSkIjzQ# z4asU`Zo;Mf`-1c)^v)sE%Wgpbc|{nUS%xvWbqp2Rt{DxuET<5+EGfXG$6N65mI$`j zlATa5LSkYHdFfa*GNI8GxlXxMYlbT2n9?Dn>dd8ZrJ$z2P9cQGHZ=W8M`!oUg|_-e zz~AUZwjV}Nr{^s3T5GX~@?ZbSK6lI?JbbB zw&c?e;c-4&;1QrOycC9wR&b!qhI=>jd5f#aavCvV32AnI7sh8hF^Ej)(m4$@+ID*U za!h`~hC;^CEq)OG zF{Lz{E=}vezywS2FY;h4f#=HQ1$goZ!yPG{DkrBv#S626MWNMESnY6Q?gs%3&ZDAGA)1$r_|#Q51qn)Oe(`4r`Ivuslc$Ar!Zhv8~Tw&Jp5fV zbf&GL^ZZYJ59*H7^TZ^Zd`Ya>8K6P8qu2ZZE+XsbmsN#+=>+20E?k`7f%6&K_Rem> zz3Ypyev69+8^ZioZJ4qCBr={X$NoaQ86r<6JSJ5s1|HW=vWLF;ofwwUj6nh{$#Qxv{z%>IHMa(R z)2hurFazlTu3OiEwV(KLyf%u?FpZsh8Kn*i1gu4ibZD}AG9HE7(I^eMd)2rmmxU?c zAcfM-E1TGkrDRCQ$k4v4P2sbXaeQ_xf;aZY@%R^Hv|oquNnrv7oj?uSzMZ<=NvroV zYtD@ZBeb|PK@XQ83rZMe5s<1+O~Any0g%8$fT9)``{3t2*#L)uuU{27^dyB47I07} zn0tUjT0%pP`OO%+mmi(zh+*|R6!y{*jL0ZLpBW|SGqa8jVMjlD-%HXOaREI~uj#Ff znL`+p5y6$Y3Ea272^+pjp_uosp|XVu2%?BV<^UTckf5RyEVPM<7PWP;JUyM5yNiKk zPj}#kDSL4RWBv&X0vNZ^B>_p73&kJSJN#Vg>@uGj&FOzKBDSQ*5P>%9ctTJ)P&gGPc+6c=V=$NTf#P59K^6Ce8%E73|dx)KC9ihXkC)wU>N7DMx|2NZ5=jOtYOgk>k<+Wrb1LoJ@>Lp~Z>+10O z*IqPH@qDxbztJ8tW$3J77Fa+}2d1oD%oT1bOy1j;!c$*HG3Bu?+`OzAS1)hJbq{+n zVQmycmqyWlK^y~?1u=MS7ly9(V#MkIMlJE-@+BVJw;_t9FGcY3*GZH%u|269^Z{gu z6p9&oW>(LeUReZVt|lu|po5T8;QJoXFxUJRz`^2~%=Wi3pJgUXv2^A<2r8wHK@>v} zwbJazTAlR$T??i@+Jq~Y#n=dnyBpbP%sGwd%f{$KZ*$QsQqnnY^q%F$KsMT7HeO#X z(pl1iJ66^*u5HG=7aOtY{bsECvI(oUHDJ-!dZfQwk9!!OU%9FlBl4&KSuQqwke)Zm zcs_{nS@g*CKK&Q8n6qMek0gOp zzC#Bnw|T;NX)BpjUMsyML#Elh$INP6IJFYzOsc@2rk=vNvrkZdNAXsf+st|u2qa1E zQ{i?4yqdmbFrS8Jc`$lOwWq=i%?IG$PP(jjPqpLD^)(ni-^(^?!6gL$-UQP=IUN{0 zzZpa3H(~_CiknyXu(u&XW7dJsbd!{UW@3ucYihGA9LKg3UQArog-f$q@c)_9hX0t| zg#VmgO&}t0%y!@p^!69cJ%lyiIM6|+W0X2hE2QT(6Vh02o%<`_sy-|khf{6?XH`)D3vn(T z$$!jah&i{IOy>y0xl_!gxMVf~E0d0I0UhfyI@o+CE?-!LtLdoj-na{^X$z7;rC~G- zglL$=DcU0c$IsZ1mh(|tQ(nr(c6ANqX`H@3{JV6i@HaH;$vfU4n_AlJp!HQR502QXs45IohdRJ1$-f5(Ub1J$!_2|i_Hlx?%X7rh6 z!{E7{7_-=gVYv)(=2YY2nWWxn^%zJh+h3l4eh2#JIY~!77_iunzKi_mmDh#y*s#4R z@WJWL7)a_rU~wlE%7b&%!N%WRO9t|NjXbUBrB*Mfe(kF+3u?#ydnpBSqA7W@S zgGNcu)St@NKd%!*`Q5-YCvIDP82g%oaI@|j{n6${I?~Z>I}o8F1zB;10a`JvGdA2r zkX>aS%zdN|V`tW5#B4W}GS2#VaB-T82IItqStS^>q6!}$@xVqGt2UJZs&_|Q*=Zvq zv5umSAo4a8x0OWyRa@I}`67m7*=&PZ4d^|cfKR#h&uJm6(J*j5CS=y& zzI9!w_o`!C!8^fT(vwvCgj8gWF6-*OudMXj3GHW&lV}V<{(}Xg3Qj%UJwH@5eDJ z(}VtWX#Fy(FpvP*Ki`JF)UCc`$D$BABU$|}bH106{5zElH-Q#zf{gUrWE z3Gi|Bnd-&`GrYKPrrU&)7fdO^#Sayr|Liid+G<=d{S@2nC@#Fe5Pk2gK>r6SF=T2j zZGf3hW^jHD1}&+hF0-Ap88+w8$xw!U=zNFd*>Ts?5qx{f20PnSp>{~c$r0G zR_J2ARn#E6r#k7qMT%Oj3QJRWP>f38(70#PhhI1dP#JQ8JsQk6)JF^i3XP1)(S72mTgP9;S zoJux~3OhWb14Gi~GShEi`r-QZ(tOj?%+hdA8uG)fI|)++_dgQP?M9ig40Wu;igC&G$02ia*RP#d*`K$n5ISkFvOAwi7p##(&Wi zBk5PyAEBN4BZW`t$VL;h@WwO-Ygc;Kjr9pJyov}A)j!2L`r=VIXgv*VmI@Z7l67(s zyhCJCnopC8Gu+d08N8(q3{;LWq|kyjEupXON@D4!9_kXEMw)iquf%Y&yCGzqz3J>O z&X7#%v23diWsD9KBxv!TM!&?kRbI*B-7j3YgG^%FOfN2<7a;)n>4;kKe?NE(f28~_ znM+5=JdF9O{3hJCybX6QuEyPYr*KDiq4}q14huI;VAE<*(SDF&3cT7W9p(x_Cgb*98)E&p>Z|fL-G~lX5 zWDgl+AZZ24Zn4W9lkis}HXNDzud zsd%hooXV{Zvk!j^=Y{q`^bu|Db_!{iB5# zv?zgI3*0!LA!y$v1ftAF447Vu{`Vcn0J6N9&-$>xJ%Nyk)fMvaMrF&lby4qh=12v9dQ5#^l;=@*3dL;y5<4^RUqWChp;(UbyB%$G zYQ-tMvMYr(TT{q-Gl~gMXor1<&CBc)2Rd06Us;nXmBr6|Er#poDJt*6OK?JkWD_j_}vW=lyJ+52Q zh^bGs;b<$_7-gf=3^kE0#rujCOEy6UP}Vuc{G4H_^y^mz4uOU?3L$P0JcMRe?0q_S z1Bi~+i?D^QvoBdHGjoy>NV@oU&~%c?0qQWCCQ{+Or6IiZSt+s}EysQNhcIq-3C7JS z!>F7_8ZK#CZW{*YwUa8^(2t7SE3E~8rnf$4S|u)4jF?G-$t7K0P>b<2?uiRXO`mY! zoo{M!tjq=*pP_SxO{m9boBkLYpwVmzOqt)A$uX>3ojZZGpR^NjPGHFNlMI`RaMAQy zKC=?N$a;Fuug66Mxl7WHV_1F>ZhWK>GhdA$|NqzCd%ss*rR&50!S|PQea}1ROtTld zf=ZEML1omjV3`3_Y9K%ekPtcs5NRr+VnMoHL=+nWN^j}CCp&eg?d+YM-Ru3{_xdCZ zGuO=fUV1o=Uh)YKyM4+kPr09GJ!`FfF(eb-PT%3d*!4C%^jsAlU0aV)3rnfo2uBI& zwUN|~L`hdA+VMw1{y#iZO~^_lGu25H6{bsY1iOfn+gY@B`pPk3PI?!zteWrPWEo3c zG0b_(k3k7#_&v*a73o@9uWP5*2@j4t9$Go>&J(JLW`NQXYS68V+{ zFn!}W6nFTn#4j>~7W$7zTgb2?ppBBWx+H*6FHxDN`Y<5bK?P4mnp8p8HHX%-0{``t z-L&(9Jq{E)hz2xgC`tsUGE=E5z$zCj7Lt-+d0BiR20`;8ve%wM3fR{`!g~=s@p>oj zSV{g_P=s3-mr%IMaogfLqD5M1_TP{t6#VtIq-u;qLHbfOTzw|*oZ5pw&K6#t|WEfwzNtt-WLRcINcfwb%1>&xI%Ol)7GU#Q(E*O zvk{oKt`s*DVfLO%gh^f+u&^0JiBN{EXvA>6r;H4q@5ckHeAtuaK^+-eyR`TfKIk5S z)V-~VeDWXo$2R>Jp30?E90E78AKk7BJB(i$!IRa}Y(njSX-1dHkbY`Se^BGng2(J~ zRX`oiYt%Nfe43f3l>q|r4*F#p`v$5A{W5HE94v3gCs{VUf2;;OzAeQopXOoBp3_+T z)=|uRDm|5pmtNI0xxR8c;%Brc6;` zQYnyeOvAJ43O~ru=zppegBFeA(1;2j*v0<(vc`>RuQuTx&Hkl}a?8YW^qyQmMU{nX zCS~G=DcQIosnMjVzx;U}YW-A3Y=6zmQD7XgW$FDnooUA{^BuTt0bLI&k?W@y z@ON7B6ru~APfI?K$l~S&P3&Whc6Flf$}$XC#d|N8tJ;pclcRX)lP=V{wFr~i)Ie6# zwnB1RS7i&8*jT#DLx?u|QF&`QKfJH2qmCOo|2N z`)R0C$vLw4(r$+MKCU4<6Hsd9_09b3CV3DJIuY^{oy2LaVr{%o7n5`muhsK)uQ2}$ zQ;_6E8(gn>6>>jfLN`tWVa0q_mYlA$l!i$;|Nl@NRBzBGc?Gn3TfZapdm*2Ka|X92 z+At)!6aA(+&}Rynaz;CP&#>dlNkl(n{A=k8_olma-INLpOsK%%nbo*`dNJ;qUyVo8 z8*%@NdfYa@9Q~BHkXnx$=G*ZH!p`3+zhyQpM`|0^e&s+O@xGI7;g{l&lu(|H?@VeA zh4InJMl&I70I&Nm3iNL$7313RS-5(FMpbHPG3)WC71`L4Sx*MElHwF|QevB>v8~{l z!lXi78m2RfLop7)Z5|H3-Ja>hpXoXcCPKP}XlC#4mTuA$x+$ODZ+23SL4bV z4vb7L#=D2@==8B1N+k}*&Hf`Y1PbRyB5oomz((nO<(v;syjX-mX=N-&4Q`;DdiC@= z{DHpz6;m2ykN{NFsIs<30{!bU-x(VlTzltSl)4!B z1wkWndKU$`tlftL=Nqu^tR2OT^xOPIBs|?HJyTHLjHIdIf=s4j`dy|_CQ+(eLZk1x zuhX-oG=wJfWMet`y*~Hc){yQ;`bVU3@%R7gprgf7IoYf}v&LqwJ&Zj^8?jC7k;DfV4 zJo6k~m)XTsEY;{kckG6wJoH*{5;rE4px=~cJiWFGUseW;t0JqV=)9OwN>vU(l|P6V z_vc~I%o^NC==*=BX5+WBD{y5(EnT-J{E@CtpXud93N{?7=`=&Yyqsu794+O+FWAnq z*h=`~4~BV+;*eD$D%AN^PGIS$ow#dp72T|63`nTMu$e>&$`c{=qLB8b6(2&Y{N&~! zGF!B^hx))rW1+39*v_<MBg;}9iA1aNj=Qp8uat)q-wG&@gSVO`=jrM3Bk>Ul?yJAdg z4$Uz-X=k6mNM!j`3tbftuAJCT;yn^`(05iA(Z*34n(uMVWV)PG`oEjt!tWEB@OvUL zEwwP@SszBO@Zr%HeVDw}gEuqnIN0QdE#N{snL{QHT9wS*K~U;4F1erbr2S&GZf#Tn zWr-QTmyS@dSxZMN^F;jg)9h9+SyKTBqs1VaJ`^USneX**l5!0K6iUql$*lKa@oVL{ zb7~=$y;6>1o0FB|X$qJUHk()jYh$t@S#oQINewp=I@9{a%?w4IV-0s|F_nnip>DEL zaS61|Yj;|%A?tUO>ic63&CCt3QA6w)T4)XbX(YqR*GBeCe!UAP3LEiFMm;`1(}+Fa z)?)jiMtpd>6^BbIklWmf7FycQXa}!tK#-_OL#PeyL40$v16%jDV(s=$tlaLW+ZDp> z%`QydREKdJ$}v9uIG){IgOXNl5JekHMpj%#E0D?3WZHe0x$ZQEPA80>Sb^V7%Egs4 z={qM@;=hP|{_s>Tkw`9{Kj1V1x+C&Z!E| zs6;nqh3cN<5ar(cqS#kLwO*ANXm3tci|PnBCSVWb#acg!XPUMFQBG;lq84zrbxtO;qMK3`(TpoZEsM zm86zXkKw7sbdT!HkgWh=LYS4E{ z2X21Wi$AZ8V8UxLtb4BspJw{e;G{ZW|5`)Usvn&cmc#5kps5?Q5)wPDQl_bzmg4aI zM2bTXFuZ4NOc3IOLxj_6e0?u;p!8);Kk~A}D`_CMeb9VJAK@Krf00d|7W55HnNkR-9PKtc^HV^k@~a_1<{J|Y?~ zHNW=RQf#M_Cu=HQXGGf+@)8X*@;7ZiC}A`skt|mut@ptbE_`w%jNM=Pv1uO}a#tCW z-_QiL42(_r2G1=07GGr8;gFoMK~!=n3N~hUo3T79pc;v8gp`M$)=SIiiE8LuZaLt@ z?MvuF&8@-JN!7SMQCYuda6>{N-`j9aVikTjr4V-}9>vF}oaiDt)F_8sBEezBRHQ7$ z%o)vL3UM9oA=K|bI}g{6JBus+nvLI2ti$h>ur@7+ZdML%csv)w5)1I`=d|V&$~-cA zDceuNAjA}8+10))zmDR;)miABLi97Y4mWFLZCW`75(V{^yF!<5#F|EYn&UJez$gl~ zfmt$9WR_PTUl9$7d~BNzws{?!W`6;d(ux|~HYXqb=eDC4U5x9Vs=yT!s&UOzM01lg z8d!mE${MZMk!e!$alC_`Rgi8=9BnM4oo(%9fBPa4Qkd;a<}gx3LC8la65OTeW5dai0_iEehnv(t?=O zTr!@ilq_Ml&nT=OriZ(=j{uz1Q$^|JkBi2^YX%Gv^W?p=+3G-1%vRt#J3 z!Go{(@Ru!4JiOVDJJ@XlqtE_J&8XCUhkeMUYYj!pUO+HWbl|?s2aU&Q0V?hwtFKeNpav&m z7=E(c>Sp#w0cC6Dr@+I@sxW9l7jB&0jQ=7sx{7oi71JLk*5Hp%m7~|hawNW4jT24H zX73*%-Uvye*hIeg>~t4Wcb6mW<9aOrHi9)LfY zF~x?PSDZ($`A6}Wg&Fv&fY0D_oaC2KyoKeXk=BxmLiy}+R^93&_P<`OFL}W~9G5&e zsu-o7qvsh5rb19yV^k()g>n|yO+gS$&aDYx#Y>s=>yKf`$`0&1OEzW$YrNkVt>Wb+ zTF_Suy=HaiVC;9f@aTp9MrKI6mMW_^+04g1w z*zxUIZ2ahZq`!9*Yd_7whOa8I^rIq7+I$RiHXp*Wx4y%n+*ULPi5A&jl1YmmGR!ngR%wD0koXzXAYzHvj2CLpfYH|h*TT;&$OdgLKAw=sKZTjSjKrxL}@l$l~{`#lj_)x zHf;QwE-m{_n{4XtCXY%Ts^SW<(`ae6KEruEGAsu8hvO0|4ozRzFb)NUpD!p5L;Qvn zH7USh#ntJPl3|oK-PRextDiSglV@W5i)F~Drb{8WMD7aVy|sEYFX#C(zGiP)l94CT zPkdM|ft6t?*GF3ebg_UDlf>4Bv-U8izIY6eyqbj*&Flg)Ko?PythFU>A}JmAl+myv z-S{FhhgCsK;rtsa%0tW}vbxcN+-4@F31GAQhV)*qx+9rHM$N<;_>B}%HU00Lb}zmw?ZE!CF1&X*h?hT(VBRJlCamnjQ)@f% z#EMF6`lu3>otkwTqNNlnqQMEe%;*h)EH<49qlsU5h$gKfN8jXLevAy~vIuiOcjHh?5Y=oG#Y>bOtn`B(@JT4Yo%#1d-p?oIFF>fcP+f5z?+!Qidm2ud9sa4 zZ^gSs7`dbpA7?0NL~mbd&^+H{lInil_X*Ok7wJ0DPk+dj(Bd!zk*s4N97B+Gi)uJ9 z;6gJM$)V~7jD5w9#UD9g^AU-utfo~Y8X}&nSdNs35juX=(@7$J?$IZfgP<>e`s1SF zuslMNe5dl7_zcaI$ON&wsb`^JgDRBHtgKE6wM9|B*RZZJGK$lP57=IQj~kwj4tk{& z51uC*t5m@NnMfl>a*v!O#V{KA*-jFgant_*#y~m0(LK%c;)|T`Rh!( zA8QY`W88*Z+?!U5yHo7ABiW0gvpVTw)ZzCN%5c@BVqBk4gkIAM(1$d5b}8PA$7Zp_btM0GjlO8A(O6IF8VLAn`!=6r?ldl6uLh|3w`D` z;ocSXcxqD(R)0{79pAO!z3-~9^Oz0q<~i_gu?H_6Yr^C=Dlj4?AAO!FMDOu*OC~j8 zcv3Y+&aJ_)q&&JSIT)E-iTjt>uzFVks)&3XY#$H#PkRWN{HAWjp%|iZne{6EV?C_S zC;h{5i4}*2VlCTbrtS7{-UaF|jnOJ(KvUw7nNW5xrv+oz+p+Y0J4)L!8fM{SxlHsy`2)+QwW4Fla>g;?r3x(gf{29RNxRMk zLfW=K0;u37uhGOe36^Fk$~vjeBHqe0lX?5^Qyf+ydPsfO{r&&%p}uA3nO6ss*jkTXLHJq8_eciGAt1(T9#9(_DWPMaR9IGtHR?iQxOpr+&H@s*HMXFJv|H8%&0Ky(Da_= zCYo{Kx~Uzwfk>o(QX_6nZNO~{%F%aj1+JM@j%#Pq{g}<`XEvFs6*tT*!BtapaCJg1 z`ps@Y|JhAM9!(fJm&!7&470X+P~NKKJGGqfi`7oX2&jnSKlL+@i@W?^9hXdT^jIWg zmBe9eC~G-Es{nsbJeOT>!Eg;uBb`$(ombC)beMQ zeaK$X$cpGEK1_r1@mgk0a}SI!!g6RKVuh5VLLcml5E&+y4*!%_YA`nC05*Koj5bdk zoveh1N>JM-YPlSZ_(Y>xv5eMR)sc?)2gM=P`ENOh4lS|@)YE@GHbF8jp0m`yRE1(7 zRxCt4Yms%%#pmgLcQtv2?%91~85VW2NRJii20bV1y$pz$T&@7iWLZbGIXOzYN~G(l ze58;yA4(%u${+C&?YO99I@wnaDte84bQ*!~A&sV3xkgId3Yn3o2s|M_d5-ET8bCV{ zRg>3?l2$Jcg*5#M*Ww~{s$e)wmG>yjxB!vT7zUd@XbUIU9lBBJho{|D0dCVIOxRPhgelvnu4qPsBA$xv=;r;hw5kwPNl&P2*+gs+bH}} zBK(StZIa+L*(&5gq^lXv?2n_*@=nZJdKjM_^}|m5?c$AO7Zr@s68qY>)RGpq2%lkO zNsr-8rDXpdhn}x;SZ_!w#@`YDGt#ZLkh`EdXriO~-MYjaM)9a#JeNXj5#Nhrdw!$P zntOEz(KWgxRjfoY&D%721^mt$kJh>-mOAApLvGZOY&_o@f;WAn+esGGHGI`J7n%&` z^7_&2@WQL_B`hXpMhhI~bAx`$51(ZW6R%O#_-cwzjp@_6u6hlTza!+xm{w=4WAPoP zS|Rdr7vX;wt%s8Gl&_*bjIr(}Hl)Zu?}>N=$!1o!|0FMt4WHE`VUHi<-wNW+^&Z@| zyafZb@>pskM$WTi*AX%Go0ElGk_&KqN(1g$62fC^<9K{+3=4PG zQlU9fWNR_$3ZM%Jcc;}RQmCx2Cdf%o%Ltfs4OLXet0ykQ@1*R^M%m`VzdtUW;`pB& zV!WsmL2B}yUkC8$5+aXh^RVEnc2ssLwY!U*&I-|*cd>WW`I_!-nD@sXrV&Zv2)2^K zmljzoFskj;cz+{)!E+-lt9DD*x)LU-ScyVv^ln#0a8xU9I4A&4&CMa#g&lDeoodFY zO>x||+K+@+4&k%>7KFTHLJzN_%thF@3Kn;=7h8FE3y{m=(UsA?FO-9%-Zq*=g%omgY~DHZ*ufqpCX+~h1v(p7mW4&%}APUM?F zn23R2wvn`lu2O|s=#I1ow9cw^st{2?@i_K_lJbnOTM(1Ytq7iVYq8G%tZmy!irQ%A zla+0)XM6bUq9#9%7Bph__ceIo(+V0<8)j_L3i%C~wX*@!x7&zhTQPmJ2dQtku<^q> ze001RH7#Xu`D>{tSvu8SH`K6}!&t6usjQQEYHV= zZ|YFs(};}+!C)KvPu74UPC@=3bzHY(8TX2J5*u1%Jfn-GkRN1O4W5(vVvnd({*m-E z&u70wn>NYP1PO~ZY^HdR^ph z*f^TJW%`tYFO;Cgcx0eD{4VS~*NL&OwqwE8Ok|YTnvtP+>5IY(@w0Hu|jr6FbGEQ)%dy17zXog~d6G^FGLew4Bg z@up_I&Is*Yr~`TS2;TUz0}rhaVfex(+`Bp-ThDt@ONi{D`-6BLxreOcqZXz0P=ccv zLu3pnNo=5}>goLXZ%k}BkPkHF(ph@_WgJ2marLn&x9T9l<=dVLsw@5#sgXszR@bZ6 z;w$A3(K+F}77C*ag$*HWc&`}`tSQCBk85$ThHeG>Qtpo0Rd?9@Bw0D0OkuxpjBGV<)!Oqmq zUm~T6M`;Nh;RbZY+{kVYBJCABhK_H;kf$qf_uLGu|Gb$9q{~`F&P z5+xUEuvODC6hcx6FiieYi&{~qOLzP{VjQyGA^N78@S{qzEj35N(V%QQC&r{@Vfd14 z%-$PDBdvL}!qT$pO4hu=3(Y?3Ci9uhqF|P?8v`txQkrYK;y94&K-%kP@!hXhgOHQ}UgvZR!6NXi8=@g7C{O@L2%LvHd+8#)Nf1oFbP zz9G7Kt#oIr*skjidvN!nY}}kwk4dkF@Xo0)N;~3I6eRLckcwT;G|%mRoE|GON4L_j z3S%95*K^*FK1@KlhuBjZQYKO~33j}(F=!-fb20X5HyOU6l9WMFlAU&?Yo|L~?ruSa zvk@I3%^a5M6@n+hsS&=9Q|opSS$uoeg@;zw zp_*N6G z#f#b~gNJMpto|q)cP}h7JFWe7wHwRd)sh25hcp73ajfb1F*7R1+p>Xrgp{AU=_fdP zw3SE|7nwR+oN1vZ&0bR6O&h41`Ov1;f>!$MU|ZM;ExyPNVDj>OJURO?-u=dlHu@zB z_G;wB3@K@_OTO48Jbo66Ltcjw1WJSj%N1c(bFu08U62iNxCofGG9M%7=HaQAI`CC~ zJ?bPYKlZrTl89H#tKKM}8Ko!Cvkl!L`Ka-<@gE-{}ZDB^l54Dm)RimpHeZQd9vn!GrC-i{ByslqcaRbb50 zER0IZ#uKY5uyL;)U*^Zq;8yaLvQ`P_sW23*HW{p10ZwaCDEaO}V|DYxPjP7Mwudet zM$0`D^0nM|8UcQPVRLk%62&>RMz40K>!5#iFe`xB>#Hz+Nj>(S3!D9rU978iZHYvk ztQL_*&w7fN@c4Nk4#P6t9Vf0aq()U%B?T^~buMG5q$*u^paWxAmg`w$lQdGfCKRbdN33H~8OxCFCN-{82d9wU?fnq53#qsKK4<1-rhdvXs zaP418NaeU``gx3AT8Rbkp2gR7K6>;@YT{M$jfrgJ3&&Wta7>HFIH{=I{8~#mde{oG zUseX8k1mv#=Xm2<@5IgTEE22zQ+Dh5;a`#jT@NRd%cWwylp!ibsU;7!4w#2#P=8Lk z77l*rWZBJ(QVO~DF%`OyyQucjG5`t?H}ZC$Ww&Aa%V#iT(K+-b`s=;47I(hp#`Z5N zkx|iUP=_YonSdZ)75Y$UTX7+g;WhD@8d;?B{#y>g00k?^7%YSIl}aZZN^`^Z*Z#5U zE7n8-di5#mrU`RlexK(GVI|S&ozE6x?AkmWtZy#Y~YwSMWzNZ zn;`KzYnsR?^fLS@B^%4$p(Q8I*C3XYO#f}Y z$1JxuY_11)B-J8)djrZ^sL-f_0)&gUW;d2@3*hegQQW)8k7fI#D7OC4aHdTX$C&2>_9es+}a?ock$K=q)DE|orJgHpquWX zo9&_%u7hM;JzGPTT}d=qAD%f~&d^KlE^!9mj-@L*aOwtVcxxw0^t z+v3{U!RSdT2_I3LHp8}-D9{K^cNCGN_a=Lj->d8rQt`&W<>=vap&^#02Q!L4t=|-k zwUP;~HEy)*QW&mK6o)e%NLZhTdlzQWB|D4lXB}u1ugNVju`k|)_n=+kHM*f1UUcb> zpNHZw3PaYG9m#sC0Zp5d)ELUfRIo>}b}e+G1y73IQLNfkgZ`5`F?^OCGhh4;-<8n1 zD!3XX^J19g)=$0IC#n6NXf ziDLn}HB?G$Ak74|lTNwfc9}@R2Zkv)?xjY!x%R0Gu-mb0n$+no}7W(rbOb}T}M+j3{b((y5QQ%Q7fOfqT8^|QFDF2>Aij<@U zrWD~ai%)mAF~4Bp_<`s`?p1)?>t^Rvc)RfCK{p;rbz*n|73rj7*!)#1s#sku$Ejek z(%{VMd%UL!kP6ClKkHH+KP$yyhGhA%r=B9&-w^%Lp1n?#YOHDZUDU9e9r8j0H{y;3H5f9d0fUno zaJm|3B|{n)!sld<&_`s))HegmKWoE~gba+FT8t@cYVmo#2Q5M-I-d|C&=SM$_d1X; z_Z$YN6r=Bg5?n_FbMM?rY(EXNgH{_!e4*8$G(sRh+hTmy&!`CtP>Wc6^I5K!SwfN5 z7%Rd$8RSY*TC#1F#CoKn3sqCv+^-uMj>vQQGj7H z3vp*s1_iPZGdAU5-s?G7yZ1Eq<~HDLYdf+#T&QN%YiRW~d%2EnppCm*SWN9Tt9mr? zGr^cadoAoN&Ck$U9QADCN`7~;M%x7BVPg8{7`Nn8B)*i1#8=Dk;KJ`PXz~Fn;cqZx zW(EdLJ&i%r3UGUJ6>d+h!KgS$H}xF~TXp!;Bz=*eF4CME$7h3}fv9THF=I zcx+7-wtm-c7LrjMLjtD0kl+Of^}{g-8@GkfhS!hB>0X?p;-FGUa$xf6LhLy~ z0bqIjgwy^&2%Qw1@AJZV`qdf?Pdkqh%PWvTRB^1L9Uk61!gB&dInqa`+JHGbbI?1j z5!cOiV(79kQnt5Ze>ttZ+ixJajER{UN=O}1Gk48yTjQWl1{yWg8OAu7?tD^2v1 z2cSiSjC*0ADM@IBipScQhJwZVYI_JD&8m}3*yIl*r#6NUz6&G$%|=XIUW|v5^XTFf zePN%Ws_90L+hU}#DfMlUbHnDh!mqnGj8 zB}Ha~6Lse-&2Qe-v{wU5s=AttEUij`66^Tv=9yzGV9`Je|srZ9>PS4gJ!VE#n76rrhzmp zPtr!&QZB}ok^spN_3(_0fXTum$)G4t!X#M>I`P=b zDy;dW4He(s>+Z0ri4#t}kMV+`lY zfy3Fr-XlS5WgEV{ry1!xny_?h9Tsk>!lKu!vGmPaEP2C*#jn?3(Uxk=f2joXUMRxi z*Q&63yB%wGwqVuUjaan37|$Q5!yDhbvFYnpy!K@i_GN}q)=ou6?$#tlC0PYoW>v{F zgSt`>itS0MG1@gAA`+w0(1=KwLaq@gv%Mqx)WnC>G;~R|M^2FEX{M2tUadqqsTk!W zD~3Q~duT*O+l*>NB0!_#o8JE?^GIvoIde}a!C2z%M4DZV{I(@hq7&`JW<)F2b|ife&h!gli9v5FvOz0rV?i*oVs zx^lde5kwP}gO=jg`VLW^6!Y;pL>t;gEYjto0rH@ZpO06!V*1W{TGNyG6J3q_mS*FD zRk?U_OD-0FV#n?yKAb9z!cEu^^0Abm04+iY*>nw3-gDuu^g0Zlm5C?U+HtNZ3J;B% z;)GfuTZ%<)g_jlZ`89IDFQmyxX6koDD$}Ya`N+KLvx)mq(TKlfm8>lwx`UHqZjxDC z*jO+j5xG4U^TahtOZPUvSyo!p{x59x*DWkcbW<~HBuev!wAr?Z@@x@*GfYXcf{@C! zq2TjX2uRe!oO&PRa}*d>&Wg#mVPzBtZL-FyQp?#8U8nw#l?r#u-A~O6bAJhw7Hf7wOYXWi%JikT$PFIpR%F%Gi4Y~ z1emhhjuUM@AhO=A|ua8VvOM!G!P|1|!wwNTC)eXk9XtVF|rO}prvc@~bQ9Sax6L+m_#^|&#?pn}+p>sKBBk)0yJ&LuvT5$D*LiCzij%%lvV(f-`d`3jvB4iP(Cy#50j5oZ9 zdidzVAK&voL*}LX)Ju8%JQT-|Jk%kIBX?18sAEkQpoSmGv7KzO0ynVfU?(0*FUB27 zCvfZ22QXvZDIyE)_$`PkznBcj8__5kp+tbp7gqcv46QvSLC`|HU9?8ZWOdOkI9ch% zv+uj;>pO7!;zIoOttNbF^P*AEmkcExufafBYLNuPy!nwwj6RXsd5iUuC6&TxXIba( zj^Tl2&FDYhfx%1McyM_Yo?P98r_x(6?Uhz6JKK&g8vN*Txey3@NECkFBPL}@`7m(6 zPuI;wtLr1n6H)|dk)uIvx!|HJMrLJA6l3rbmBoW(c}Wp3GuJB(LD|f*m|{}J50uVJ z;nkc@AsC^Qu=W)}3lT+t0&XpbD}|!e=(t(WTa&efGL<5&q$LV2>t{d)pT)--EbDv3vUPFMm?4MkY*2cq70Ka*W#5U ztvE%OX@5SjVz&qPFRw-aq+$$u>Ifc9&cKm#UUUj6646*LkA`p6r$Qw{BSdc`fuA1Y zKa#Wx%R?n^#-Dk;`9VA-(tFlf!|gg(h(Ru<7IivQ8OD=KzQ&DzIft7kGDE?zaxT26rN!NS zrWGTeZNjaK>XG!J9bZrwKC1F!)xlOwc{_kd*0Hulq8Ql@WOujkIxoj_>}LF^jZh9ZOZ->e5XK>!Z8J!Xfc ziB8qUkhq~5BWBoeUrGzI%j?la0rJOdsq7l5!id6X(mUwOf1d%&+2O#0&(&hXw{6IF zx#8A+G{IIxf^>O^AU1v$z0Ff1_#Pd?X%rZ%rx>FT{>am-%dhS5vE#-5W6#?A0h zAu6aWOyy!bpRa7ercX<-{)-Nrs|`}o3QbU9X!SE%Na`varf@#_+*g>msTO{@0OEZD z6S*ncOv_IJkyVZnvWI9<15_A3D(nvSMNP01CmTEQVNM&io#?=>lL35^6~n2D7^>-# zx!BQeB1b>lztQ1E+IunFomzzf^E=RQRt=`CIg69krYi}sDCqGnmXC^xE*un8Rljz4 zvH8mo#w@PKfW%hZIIROoFPG!{N+Krmf&`ttOQC0bk>?aMF+tmSJA0KbB>Tn~9)S$i zORr=Exs8!nfX|bA#Wo{)Q2W{45iH+h!-#~Baodz~^m{rR(_RbVXk#4tN*_C{6c1u&>E5uHe3&RdjB;P}5 zd$uNql{;$imqjNrZ*v8{%nhN*2!zidjcBxku8GA6g6Cmg6Q*_6R(=|K)gB&1nI`yV z_8O&fQ7|$_Yp&J3JybYt>~tF$p@GcSz%u>VL#v&|X;r&Xgg86)(&G?RaurnyQrNg3u49#4kYcU!`ICnwDEp2v&*ETE8bI0NPN0D zj1MyW_^K*~dN!+;3dksrW7DoiEPOW~Co4Ls7-{9H2u!oGIu5?>Jza|@mYm1vR6BMb z_QOfVAg0$+bV0U@QtR{Ce$zIe$0I9RQAweZTOq`)1&@3rufh9NvP2bbmZzS8=iu28 z*1gk(sVw)OmN#KYS`+SA<;Kv(c8sEcKbdaBlS@mH`a(TEI1)i=xv4HH*kNy+$;N}} zRk(G^2~1j4gAWez8GN3Qk^7bhk9$wRcfwSvyPjVBcu8$zM z4#=zu!bWr|cZuD=HfDbq^g&my8-19?x!DiaR*Kp;*+ovWQ;ECbWZhcW7A0iW7vA?` zd};wvh7G-wYB4sw0LKemXrYn_OUVer$?^zki2u6FY;85Fqn6%;q29M9ceY30Z;_h0M zv|3RLH#NM644}~q%_}jSqXaUgYEyzV{;rV<-CL8_w6U;U6%Ub11|?$ghs}6mRT&m- z{0NQYhaY{Y>=dX{mVfQR9NfP23nAq`V=S#ryoyGf^b;r#Vp z%-_Cng_uH)aP!(?TJA58hw$cyjhMAMfT=5@n7G>7!{nW>YmiISlFNIwOPOj_ zGEEuQw&xKquMgosUOgtSEx`jz{CMY}6JDY+v#kN2+rT!fVV`91veccmxQoKFE z7R9mqtPk%KwN-aeQa|XxxHac7e#Q4#v^f*+errc&Wxz}isHTlL z)8NH>Spls0pb}%B%fY>i&*QD%g%Lv5&O z3(C?Ag|(;>8`W3^A`1$p_9^*}1|?-%F78UH#qc@J7(VqJ>(`1LTiC?SLKJKv54k30 z}iZy*od#m+)C?KfKSWySi4auIoJ$% znfF~3Rt5ZwLQ-sme5kC-%$6Abw5tjOpD)C;k0K_*Z+`!ghqeCFF?MV}-a?wwnVzdYNBN7I0j2{GKXxB)L8i=dY1NpZh$mr$K1 z3r6v7Q4=0pSA(G`z~)Z^aJWKdL!xuFPP&3l+?U#nJ7(l#;9rm7rl+d#U$eV##ey(; z&nm_P>ss-3W~FJfvZe@L`^1Y!Hu^ATg$?TtG$WthgfdnFX8wf>ac>Lae)ciVzK7kS z{T+&PE+yE%%JrC}-vp#r*o zyk1IQ>q03PM+jAGzLq+jNMl__EiXmy$wVNDP8`T=LJO5)BwkK~Rc8XNxK0F#k zg)5FW_N2_FP7_U#bvd@hPXDGofgwg&rh{W#YKtll5OgD?7V>l!EST9Ji$dunj3By83l z>0*PZ6}+)Vc*1pXx5V-8t~xxrsRTopp2edZ@^G%W3thCjVboHnTZuS2Fn&=pMo$Ko z?P|jLc83Wk7CXA|;pato{jJk@WqSrTzL|+T7S>>JN*1={6X-YBu-a@3>fL5G#hffZ zM!Z^$I~KQK^2;?iX=|ff#&)2=?MD z9ycwi!{im`@Y08^)&|>b+D1nhufF5Jz4QDSlGuRZ%WQbzXo$*+ogZs5ra;Rlc<3gW zo{K6@7DPSkzUQMHy!JscGHat|qvm!h?*Nf%R7yg-yoViBHp(NRD?~OZb%gN7w_Uhz zNe!->T#YNIxzQ`pj{Y-knEd=%>^s{Ix4(^bY9w-y*7@Zgzgop1O&+Ed)3BRhrviFr z(_J!@kFH2wbu%_@E5_Qb6#pi`yoUV4l?rcD3Sd+q)GC{jQG~=gl8=?}4 zDWE0NtRn>i2{}8-j4qmLv5NFZ4+(V=EbzaJiC@aPXoN>Z%UDJMdM&R76V_JZkwq0) zvb6+dK8bD^rPN~YpXtDsuWWdJZv|GpUx1Y#9>vDbvhX^;`{axb)y_6FxxHqQphwe6 zFgoQtroCmu@oFLj#Vy3de3lj%5-EQ}dQHr&4TXw)A*8+Ej3KkjF>av)I}cKj0%0mg z;HwfFo>`lVJ7>Ev_l@HSJ!>5V`b@8>XGmghMw&V6?)!0nMVQoN7r;5*N zrsU2)?8n{f>TuT*A7*R|Vpnb$Z=P^t>5fXwB5Ihmu^Hpi>+$5udL+KqhNatVNZ($H z-3MxsQ69lQ>Y!KZ{P;ZAfkP!7tWJo^U3#Uz>nA+*ytc!cCwA1y}^Cdm~E5t(R`p4}ZoPE!aUWq2|FEjvap zsKafErRX=M1jA-!VZyp1ytKax-77Iyoyc*9vF%tVCa$l?s03R0iD!_wsuX*^>_Ax^T_BzxHbaC; zVWy8S6r%u{7=!{8ry@~GHRa!sl!b_VMC#Tae@E7L%7D~DM(e{fN_l4~9ZGT2O~cn~N}bg&pJGv}3Q$PfR4V zLk&S+Do%gjOH?IXC85?htJ%7dZbFy08ON$!g#0DwlN`iD&v)QZVYQi=d*1HAqIViF zY#x31)LiU1-e6XsBI4(@5|9Af+FD&AX=eupEpEiPopCB#-j`*Bw+kMB2>S~CnEG-R z?t9*b5z9*O_)ATA?rR@DDh%VSJ%lQ+7j3~HT`?j9wzWwF<6na}hSOz1JhLT^!Sh=1 z*Lh#zokKyho{ECX(U=#0QUmLF%+`VQk8PN@r4V~hw4*t|vaxISzjbE?gpe>~pp?}Ga)H3=68$DhFJDEZo^-_M7eyt2p zLD3kTbZlF^5fs+}A07-~W_l4GOgx3j&)M+W!61&=W2hr@k>%)+M5sVQCQ`1c(Gp~X zBqZS>3Z^Ev@i%Mz1iiODn>i*@3OrRK4sBflA_*@>rBq^kdOkkM@q_5wpq&_DbDX5% zy1$XM5#P{tl9tlilpfaWP+2pcc&iM3);8m@oy~ZU*127X#zgOVjoblhGR2EbJ`2B> zP)U%&yun#2nn%}G;Q9r%cx+1@a#~wVcEgFPRwPlW4V@RI#Vy6w6ZL3kRpf%0ly^34 zFy=Ha-n!3QFlY&pOL`}=TAS!15b3!a**{lkh&*~Pb=y6 zP;eVrCTA#0#X-wWWLL#{9;%5Wm9E!4Ycx@>7=KyYj`vQVM>CafR0}ADT~ydnqdnf zfW#);oKS}miDj7kS_Qty3!}j&bg8s?H~UcYNT_@@1ZDc2ooYz$NtY4FFY~b0!XVxt z$-swb!9)QvmMA1mqW+~o@oNLAl{N3YUgCHP1tf^>&f}c&&7W>S7d~TrLf_((T~mw9Q|b3|!INi5EWW#Qm#WgzUB0 znddf(+$ujHE`L3$#g(<~gobH(LuyMG8i{Ob(9X*4IUb>lLpZ*u3M)VAqyR>hG>T)D z&6xR0Er!hX;_ijHcq^k0?QAnEkjJ(St9{i1ZtOZjrLnXXLzX+S{`1qQ7P_LPkA*ey zQ0`3>S&enr@cv1RTaksk=H}z>WggtK+K&ZOL2E%HkNJ7#IdR{>ZxND-|$lZo1{_0C%mEDRd#?9=45yj zfH%rQ1<`Qu+I$!u3 zp=ZF4YI_Tg=NIF-4|4J7k^>l#RD@wO3UTk;BJ4WaiE?ik4lx@q~nT9R@)KCF!5 z@eM6_U}-s?e?14at^6`X%gVYJwE406qYm7)umyK4IFF4->(Ho}f&O?O&1TmsR!SK8 z_(&ZFr!`_wsvUPEy^jy___cnQd&L?mh9Fs&%+*jqet6oB^pDChcTWWpcG>XQrecgq z&%;B{mLlPWT%^BSjn9gTQ59Cpu+2!y9nl;b)n5xaMSGS%6Qa}lo2+4!m{9xVv1d(S zw2=s=%pJxj=ln?8?7%~5)fhRi0yj;|L7z!yar?Yd+&}Lermic;!Sn4VUJ;A8Q6bdx zS&i%qxg(Z)q>(%`il%P8Ju54BTg@ z64vSmiK9l+HCkd4mE~SgpyNS8TUk?HE3ZHTAfAww4-oPe)6rkOHydM~J&h}8ci=Zu znlNtJmsq~D5Qj^Sy@bXN>glan6sQL%+k+#bi858b#cr5?kRGm*Bp z0c9?aiN6F{YSWa;vx#;gv#b-h6ESF%;qLjT@LmpeF{`AN+CytvU>jH_W5 zIAS%Fo1=J9AlAYvX|I&9<>48`z&;GnY4WkZHwFFl_Q-vEQmE)!v)tIpAEEaCfKtZJw?L;01b+R(*3k{b# zv}bUTeI;PZIx6W<>)C4hAMqH=sf-$uaY1>KL74`~nJ(kYFZ%cuD~=y~Tr9B?XF(Bt zN3PI8j*vl7m_Ul8%N;;XRU1&ai_G0>H-(U2dlHIffv)nSx9yaEmZ6eJT zrt8p7NL5_x!QS%@%->dtfs2c9RZ1!PC7#4xNk=j9xeTm+w-BG7v7x*zXqE(#5r_+B z8WPUb!~r5WlD<)||5}gBisJ_#lpdojL}DFszWL6wAu=omiFT7doA@IcSaUL5z7W1I zaAMWAQaq4m!{F&9xM6BNdL>ljrle9lw5kj%-mS#pJTF=)XqxL`G=%_?UTI}v7WN>S zELN)=vau?S+7Uv2s}Ffi9%R>bqM*)+ zq6QaAeNoi1{EE|P(Pr)Cq3~@T8caEEMKv-di|yi*T&#_i)Gd+i7G0jNWg3ILP%fKRYmf3w z+4{H|%GYHQK_k=~&3w(aMe)LC4vb%wgW+?sam(}!+?QKjsR>;aL^lPXk(RKqDU9P~L41+r!`laaSo}sKo=7`~@k??saYYfPtt`bW z@1Mb!r!!I1*k|(2DBH!4FI6 zNUQNwkt9K+_i{x@^5f&m)*y0-NBG`n=RM@!(T&iy6%hsblzy$~f?^CYxGA-o(P_ml z2=RsE`9T0*6T=h_h?&SFA7*?zJ&zp(#Ps>khL`-oe zFRM~#_hRd3%}CjxUG|RP-r0vSn2O@I&!Wd3=Me%K06i3|=R8yH~_NXUHf1fBJ5}_NR+ydop z2nq?0oAU4>(l^pbnv0?w1_?5Cjp7rh*nC6- z8Xb~wo2mG04gm{I)NAgdJ4S`l8i}FA6+?DY6vrxKc=u!kYd#5M{r)JDx47}xqI!&7 z*pBh(0ZdpK!`wGwnD(+4qvjN2(DZLHZ0=#)xAF+azIX~V)}KJis~OmNpb9y)EI#Eu zB$rLY-pWgn8zdq+(Y_>_)hzeVZ-P|aT1$0ddDU~>RWZuBhxD0elc*58|9-8c2QaUrvew%!{@l{<pU(ijvsvVNc}$}{r1nuvP~NJ70F-MkfZrYoT{j!awqhINE|Ar<}FQ}liX^o zIlnUsgziUF>qKZ>iI~=dkV~QYt`Z=v9l=ZapC)1{fN7`OQSRu(naTzn$*acKd5zd} zyc(pui!smD;?L`CwBijIzQB%Kl4>w0sTwy<$w%*md|Wd*1J_JB zkLwb$aQ%#2^h+*9-=s3!m{@{7v#Zd5PAzVnQH4IwG@#F4h=dXy7@g|HturY6i`((k z%bj>~eFMh5(1fQqmtet%_1IC=gaeIk9CEhcthW>8Zl4(*6&h%x0%!^O;0hCi#X_tv z8$_;;hH4bZQ2<0Bf+x$(&^j4f)Jc;Xdt5oSvzk^)L3-G; zO>Ln~7rTtuYxq}#z`7srO+Q--A_8sjs0md{{GxT`y@aw2!tQ?6CDuZ{6a@_d(ycIS zL$F9@I{{^_D!w5XB}x=SVY3!6rUj)n)>;x8GKmMe5bSJ2MPnzbo82fTsNaH8bs(?Oh0+?YSsxjxsTt*dD_NUyZ zAVHhLxmx*L=UHcJSlwD*+}g58BU4f^auT`4gOAIK28X1QY-U z00;m803iSxm!wU10ssKN1ONaS0001ZY%gSTVRvb6XLB!Pa$$FAZf7wrcx`N)RLgGL zFc7>K=sys=J0fL0EFp5>+DT9#XoEh`Gec1k5s4H?#&%Hj-@BwF8)-X<2kF_Zec4rU( zC|km3g#7T$z_v`EJg={-_HlcJy?s6YUD zf*OWmLRgA~Bql)xb14B)F-V9da0Nt)R|V@}d7u;#u3!PhMpLOGnBWss;32#Pd>0eF zf5Ce@vpf0C?D_VbZ_jtm`DHSbz;;wm_VbXvql4hU<$;D2p#I(2pg-)g<>)O_C0zUI zE0#?poySpU!0V+Q7_~L09|z_yCUMz7E(MLMc5pK(nVHzEDelf*f%mxv3nfw{eDe)wg+14tFN; zDu&0(IX3F7*PKat|Ajwx)@zC^`7kZM+U@a+vgVyb0wagck)}{u&*l>0lywTlyOp(Bp@OV+(@#&@5LUi=M$SjeExr)c*mNbu+>7)^CMYVsUvN7+VW{6m@YbQV zg+!{Gtw2~0<9m(jDrEP~VpBZY5zfx+sjD>8B0~eO!aDggY#uA{A$!F<$y3p zfiO^H^)Of9R=Y#uOP;rJIVYU$>n9v2NRJcdlEx*&?hKu8pZi*oECB#>a}t1PvgUTG zIS^q`l0pfq1W3aos3dREhRn06sUH<;XIJP9Y2ZFll6MoAXk zQs4UADG|f(m6v>BZGFgEMQmq-6KWf@^l3E1X zf07wIa>Cw%krcD5+j_QsSDoR}fNnO4?Ub>M@T%a@aEj_o`iFJCws6FsZLV@HLHQms zv(0O)E`A%Y!#m+_JQb{Y)9&^)-(s?6pEvcQv*f>PvZtHR^?p;Cxsk}y4n~%tBO76A zd2f%eB>0^Q4|Us?D$|nnrT>=9_KKr6EOk^yht=rzB;g6f?rJmb2xQi3-8;;Wr7P#) z8nasCFFB!tF}TvcEmdP9_=JM)9zHIRn`=niu)zj!YR2`L2UnF`fQ#gbzf=y0*49Kw0txQy1865)U_2#S5!!5cNnB46R^St2z8pIg@M! zs0($a5~>S|KPsO{qy1m_OU14<05{*J zdsYl<;p47lV*YURN2#XWqvUMNL|^B*_-2vyRWV#r{o{nhjF7q&5AL($$}Foq_lzfK z)+Df0Uyin1liFbS8f>O_oZc8|tg@hqJ zHW`FGX)|l`cDwnCU z8CkP8x;Op-cHPja-=%Rm`vdJA)b0m> zPyJsjJ*qDrt_yadr>XZ{reA>1x)&a)abBBL;HdRHXcVW@_PZx*EV-e$^>fkV4vg@_ z1H+1m_S?b#ifHJOQ+iqZD(kmxW;b(4iRi!_HN{u=$U6)5;HP?1d~ae;cAHOXV}x1z zGZo^6CbGn_qc=BN2VVdrHAdbRy1RAfShM2}a`-J4MXyMHD=+eOs>cfnehEnusap1LloxDzyun|2%Xo8KTZ_j z`Zg46Nn+RopOj-EsanWKabeqD2gcpfU+fl8d%`sr1-z zDm8Zg*}uf$*$#Y+g)k(b2oywcG-MWVenb#=qK$J!js+Q|LZ_;T@l_}o$O%R?kq$-# zFrk7R#rReHSo87wHISttM52KR!cO@Ggue#j#dy9i7sldW#^YptJ*Nu-YWoQ=4Ea|) zjA#&KGQLx^u=snx^mp+(Jc!Z^h-J}NoVa`ekQoief!KxX0*W|E=O1!iPDAXTF$4g} zg&z(VwMCMc31sk>?5x+j)ENo&wMyf~4Z@P`vX$b&EQPV?0F~D81;wYj6 Date: Wed, 29 Jun 2022 22:09:16 +0200 Subject: [PATCH 032/156] Minor memory improvement --- src/PhpSpreadsheet/ReferenceHelper.php | 2 ++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 8caaab18..a6b82015 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -316,6 +316,7 @@ class ReferenceHelper $objColumnDimension->setColumnIndex($newReference); } } + $worksheet->refreshColumnDimensions(); } } @@ -339,6 +340,7 @@ class ReferenceHelper $objRowDimension->setRowIndex($newRoweference); } } + $worksheet->refreshRowDimensions(); $copyDimension = $worksheet->getRowDimension($beforeRow - 1); diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d63fc10a..be717053 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -658,10 +658,8 @@ class Worksheet implements IComparable */ public function refreshColumnDimensions() { - $currentColumnDimensions = $this->getColumnDimensions(); $newColumnDimensions = []; - - foreach ($currentColumnDimensions as $objColumnDimension) { + foreach ($this->getColumnDimensions() as $objColumnDimension) { $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension; } @@ -677,10 +675,8 @@ class Worksheet implements IComparable */ public function refreshRowDimensions() { - $currentRowDimensions = $this->getRowDimensions(); $newRowDimensions = []; - - foreach ($currentRowDimensions as $objRowDimension) { + foreach ($this->getRowDimensions() as $objRowDimension) { $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension; } From 5d5e550342426d51c85f2f10cb8ee6ca0b1bb84b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 29 Jun 2022 17:52:09 -0700 Subject: [PATCH 033/156] Additional Support for Chart DataSeriesValues (#2906) * Additional Support for Chart DataSeriesValues Fix #2863. DataSeriesValues now extends Properties, allowing it to share code in common with Axis and Gridlines. This causes some minor breakages; in particular line width is now initialized to null instead of Excel's default value, and is specified in points, as the user would expect from Excel, rather than the value stored in Xml. This change: - adds support for 1 or 2 marker colors. - adds support for `smoothLine` to DataSeriesValues. - will determine `catAx` or `valAx` for Axis based on what is read from the Xml when available, rather than guessing based on format. (Another minor break.) - reads `formatCode` and `sourceLinked` for Axis. - correct 2 uses of `$plotSeriesRef` to `$plotSeriesIndex` in Writer/Xlsx/Chart. - pushes coverage over 90% for Chart (88.70% overall). * Update Change Log I had updated previously but forgot to stage the member. * Adopt Some Suggestions Incorporate some changes suggested by @bridgeplayr. * Use ChartColor for DSV Fill And Font Text DataSeriesValues Fill could be a scalar or an array, so I saved it till last. * Some Final Cleanup No code changes. Illustrate even more of the new features in existing sample files. Deprecate *_ARGB in Properties/ChartColors in favor of *_RGB, because it uses only 6 hex digits. The alpha value is stored separately. --- CHANGELOG.md | 2 +- phpstan-baseline.neon | 12 +- .../33_Chart_create_bar_custom_colors.php | 183 +++++++++++++++ samples/Chart/33_Chart_create_line.php | 4 +- samples/Chart/33_Chart_create_scatter2.php | 70 +++++- .../templates/32readwriteScatterChart8.xlsx | Bin 0 -> 12409 bytes src/PhpSpreadsheet/Chart/Axis.php | 23 +- src/PhpSpreadsheet/Chart/ChartColor.php | 52 +++-- src/PhpSpreadsheet/Chart/DataSeries.php | 4 - src/PhpSpreadsheet/Chart/DataSeriesValues.php | 171 ++++++++++---- src/PhpSpreadsheet/Chart/Properties.php | 34 +-- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 178 ++++++++------- src/PhpSpreadsheet/Style/Font.php | 62 +++-- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 213 ++++++++---------- .../Writer/Xlsx/StringTable.php | 53 +++-- .../Chart/AxisGlowTest.php | 1 + .../Chart/BarChartCustomColorsTest.php | 162 +++++++++++++ .../Chart/Charts32ColoredAxisLabelTest.php | 10 +- .../Chart/Charts32ScatterTest.php | 147 ++++++++++-- .../Chart/Charts32XmlTest.php | 34 ++- tests/PhpSpreadsheetTests/Chart/ColorTest.php | 32 +++ .../Chart/DataSeriesValues2Test.php | 172 ++++++++++++++ .../Chart/DataSeriesValuesTest.php | 10 +- .../{Writer/Xlsx => Chart}/Issue589Test.php | 6 +- .../Chart/ShadowPresetsTest.php | 29 +++ 25 files changed, 1279 insertions(+), 385 deletions(-) create mode 100644 samples/Chart/33_Chart_create_bar_custom_colors.php create mode 100644 samples/templates/32readwriteScatterChart8.xlsx create mode 100644 tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/ColorTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php rename tests/PhpSpreadsheetTests/{Writer/Xlsx => Chart}/Issue589Test.php (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9148425d..b7d55bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) -- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) - Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) ## 1.23.0 - 2022-04-24 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d1f2a03b..858502fd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4055,16 +4055,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx.php - - - message: "#^Cannot call method getDataValues\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Cannot call method getFillColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#1 \\$plotSeriesValues of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeBubbles\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|null, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|false given\\.$#" count: 1 @@ -4087,7 +4077,7 @@ parameters: - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 42 + count: 41 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - diff --git a/samples/Chart/33_Chart_create_bar_custom_colors.php b/samples/Chart/33_Chart_create_bar_custom_colors.php new file mode 100644 index 00000000..75f2306b --- /dev/null +++ b/samples/Chart/33_Chart_create_bar_custom_colors.php @@ -0,0 +1,183 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Custom colors for dataSeries (gray, blue, red, orange) +$colors = [ + 'cccccc', '00abb8', 'b8292f', 'eb8500', +]; + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Custom colors +$dataSeriesValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4, [], null, $colors), +]; + +// Build the dataseries +$series1 = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues +); + +// Set up a layout object for the Pie chart +$layout1 = new Layout(); +$layout1->setShowVal(true); +$layout1->setShowPercent(true); + +// Set the series in the plot area +$plotArea1 = new PlotArea($layout1, [$series1]); +// Set the chart legend +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title1 = new Title('Test Bar Chart'); + +// Create the chart +$chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Pie charts don't have a Y-Axis +); + +// Set the position where the chart should appear in the worksheet +$chart1->setTopLeftPosition('A7'); +$chart1->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart1); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels2 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues2 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Custom colors +$dataSeriesValues2 = [ + $dataSeriesValues2Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), +]; +$dataSeriesValues2Element->setFillColor($colors); + +// Build the dataseries +$series2 = new DataSeries( + DataSeries::TYPE_DONUTCHART, // plotType + null, // plotGrouping (Donut charts don't have any grouping) + range(0, count($dataSeriesValues2) - 1), // plotOrder + $dataSeriesLabels2, // plotLabel + $xAxisTickValues2, // plotCategory + $dataSeriesValues2 // plotValues +); + +// Set up a layout object for the Pie chart +$layout2 = new Layout(); +$layout2->setShowVal(true); +$layout2->setShowCatName(true); + +// Set the series in the plot area +$plotArea2 = new PlotArea($layout2, [$series2]); + +$title2 = new Title('Test Donut Chart'); + +// Create the chart +$chart2 = new Chart( + 'chart2', // name + $title2, // title + null, // legend + $plotArea2, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Like Pie charts, Donut charts don't have a Y-Axis +); + +// Set the position where the chart should appear in the worksheet +$chart2->setTopLeftPosition('I7'); +$chart2->setBottomRightPosition('P20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart2); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_line.php b/samples/Chart/33_Chart_create_line.php index fee2a284..8cb87e30 100644 --- a/samples/Chart/33_Chart_create_line.php +++ b/samples/Chart/33_Chart_create_line.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -35,6 +36,7 @@ $dataSeriesLabels = [ new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 ]; +$dataSeriesLabels[0]->setFillColor('FF0000'); // Set the X-Axis Labels // Datatype // Cell reference for data @@ -57,7 +59,7 @@ $dataSeriesValues = [ new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), ]; -$dataSeriesValues[2]->setLineWidth(60000); +$dataSeriesValues[2]->setLineWidth(60000 / Properties::POINTS_WIDTH_MULTIPLIER); // Build the dataseries $series = new DataSeries( diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index 1d6e331c..ef6353bb 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -2,6 +2,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; @@ -64,11 +65,76 @@ $dataSeriesValues = [ new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), ]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$yAxis = new Axis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + // Build the dataseries $series = new DataSeries( DataSeries::TYPE_SCATTERCHART, // plotType @@ -79,8 +145,7 @@ $series = new DataSeries( $dataSeriesValues, // plotValues null, // plotDirection false, // smooth line - //DataSeries::STYLE_LINEMARKER // plotStyle - DataSeries::STYLE_MARKER // plotStyle + DataSeries::STYLE_SMOOTHMARKER // plotStyle ); // Set the series in the plot area @@ -103,6 +168,7 @@ $chart = new Chart( $yAxisLabel, // yAxisLabel // added xAxis for correct date display $xAxis, // xAxis + $yAxis, // yAxis ); // Set the position where the chart should appear in the worksheet diff --git a/samples/templates/32readwriteScatterChart8.xlsx b/samples/templates/32readwriteScatterChart8.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fdd85b0ed3bf9fbac400a46a350da93f74e5d9a4 GIT binary patch literal 12409 zcmeHt1y>yD)-|q;1P$&GBuMZ8!L@-v(BSUwuEE_scyPDi5Znpw?(QD=I+?jQGt7Ly z;Jv+8b^l313=k3u0}KWX4h#&81Z;f9o+$(z4D1FP3=AC%4nkGX(!x&P z!cJ4(*-GD5o!-gZj5rGl;#CG11nB$!d;K3?f#TQ!i*Jmm!q;(kC{c9^Iy+f~G!P;0 z;$Nd(KuU6>hIH0)ckCZ?F@Y;!QV5g>#mv}_q*d_vey=T!W`$xI-pWe96oRO=nwt@( zZ^PC@j9AnbVo|Nkw~$TlsTRqYUlZNO`0N zCa?zVc4nWa@7qTRAT=wl4u_L(y^2C?ljZI8Qs1C*RMXKkYrfj6V_LaaI@vbnmqcQF zTefmu4qb%?y8w5v+zP0ShJH6jNdP+WTBl5J%pe%128?-CKgS!~jxi^3~6r9S>0JKNX>wjY-p zYC2QosdKa6t8s9lIR&pedRL4?(gYf>ſT|-C@Q%5J6zH=;CdiOT&i>jH(7VU)a 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 z zAv*;Pj!a0CA}E{FfbYSRlLzxHgf^Q?q0d)h3wTDPR2%zpSe+jXhC5Du9&FjCxrHFA zW|fN$dnw)~R#g2_s_$Iqvqlto2BU=hR9mbWN#sXo54Naq;Qs1&RaipSPvmct+HzB( zS+mw-Ve0)=KFP`_iqV&pL5u7tO~BW`pv~aa)m4E+ppY3&)82Ctt*n!;tg}mRF+qyW zk>N*zfgBRS;W2sPM(}86l!UHdV!f~P<;T~&B?$+;vPa|wY&I!olVFEX4o?wHqNDV= z`~tQ{b#dZNA@Oir+E-K;PN8`xw~ydxjW#HxBs_4{Eo3gJ(@oyyy3GhtI2kk9#A0Ox z-97tvFn;-^LTv#z$v~t%gL%$}QtDayFL+?`XxV2LOt4$IUz=42#gnSI-_LUB&-#RM zHg0rv&C@Sscw#rxPTYq)ttMiIq@I$l-(Ci-w`Z7nrtm3$Y(*gnFgigW0QKbl#Zf0i z^sd7|j!FgU^`QRMQMIkC{^zHlCHwQ08r}Z0cHn;ud4=qM6qZ{0%AZeBHs`gA@^>)t zMrNw<3Ym{@$fv}vE^^H$?B~)P$ZpYF-$ltQ1$65ZbnE#tblOCP0Gi=*FYg7o508dD zg6Uq|psAfG2a-`*=eByvrMy%L)nn2(Ta_a*$#S63f5XZaiACqCScr;8EWNJbUswra z>EmoZ8pAzu4h(`7(A06BlJ?iJQ?tKl{i4svqv^6lpb9g!*I=H;%J%^Lp(O>NmXBQ| z1iYo#&eCakojh%{n{P|8T`-4h_0>RE-5Z#{A91a^H;olZuorc>$mAHs73mx zIQh>I*mL8*PtW`|1p`}#|7HBgKmNxo&2yaRBNe}q&;bAa5&sylc#iVCulpNi98@3y z-OBSW?{k3XMfcwTj#$3{ewW~%n?A2i{x%K4`NQ;irSdt#^BUl91by6J2>(?=f7L+G zQJ&X)exo$v|3djk1?V}#bA9?7L6-a%!q46Ob1(i?tv&~QF6w>*np6D!#{NGN?>XRe zq4XQjgz^{QGnw>UM*Wga&q4pLcYZ^HCjUX8|4={A&Ho;S{%W2{{TK5;W0DLI8bq$2 QbCxJzKW7y5=zf0se<{9 literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 69a25d92..72438891 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -27,6 +27,9 @@ class Axis extends Properties 'numeric' => null, ]; + /** @var string */ + private $axisType = ''; + /** * Axis Options. * @@ -62,11 +65,11 @@ class Axis extends Properties * * @param mixed $format_code */ - public function setAxisNumberProperties($format_code, ?bool $numeric = null): void + public function setAxisNumberProperties($format_code, ?bool $numeric = null, int $sourceLinked = 0): void { $format = (string) $format_code; $this->axisNumber['format'] = $format; - $this->axisNumber['source_linked'] = 0; + $this->axisNumber['source_linked'] = $sourceLinked; if (is_bool($numeric)) { $this->axisNumber['numeric'] = $numeric; } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { @@ -156,6 +159,22 @@ class Axis extends Properties $this->axisOptions['orientation'] = (string) $orientation; } + public function getAxisType(): string + { + return $this->axisType; + } + + public function setAxisType(string $type): self + { + if ($type === 'catAx' || $type === 'valAx') { + $this->axisType = $type; + } else { + $this->axisType = ''; + } + + return $this; + } + /** * Set Fill Property. * diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php index 05c1bb9a..7f87e391 100644 --- a/src/PhpSpreadsheet/Chart/ChartColor.php +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -6,6 +6,8 @@ class ChartColor { const EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; + const EXCEL_COLOR_TYPE_RGB = 'srgbClr'; + /** @deprecated 1.24 use EXCEL_COLOR_TYPE_RGB instead */ const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; const EXCEL_COLOR_TYPES = [ self::EXCEL_COLOR_TYPE_ARGB, @@ -22,6 +24,18 @@ class ChartColor /** @var ?int */ private $alpha; + /** + * @param string|string[] $value + */ + public function __construct($value = '', ?int $alpha = null, ?string $type = null) + { + if (is_array($value)) { + $this->setColorPropertiesArray($value); + } else { + $this->setColorProperties($value, $alpha, $type); + } + } + public function getValue(): string { return $this->value; @@ -61,10 +75,21 @@ class ChartColor /** * @param null|float|int|string $alpha */ - public function setColorProperties(?string $color, $alpha, ?string $type): self + public function setColorProperties(?string $color, $alpha = null, ?string $type = null): self { + if (empty($type) && !empty($color)) { + if (substr($color, 0, 1) === '*') { + $type = 'schemeClr'; + $color = substr($color, 1); + } elseif (substr($color, 0, 1) === '/') { + $type = 'prstClr'; + $color = substr($color, 1); + } elseif (preg_match('/^[0-9A-Fa-f]{6}$/', $color) === 1) { + $type = 'srgbClr'; + } + } if ($color !== null) { - $this->setValue($color); + $this->setValue("$color"); } if ($type !== null) { $this->setType($type); @@ -80,21 +105,16 @@ class ChartColor public function setColorPropertiesArray(array $color): self { - if (array_key_exists('value', $color) && is_string($color['value'])) { - $this->setValue($color['value']); - } - if (array_key_exists('type', $color) && is_string($color['type'])) { - $this->setType($color['type']); - } - if (array_key_exists('alpha', $color)) { - if ($color['alpha'] === null) { - $this->setAlpha(null); - } elseif (is_numeric($color['alpha'])) { - $this->setAlpha((int) $color['alpha']); - } - } + return $this->setColorProperties( + $color['value'] ?? '', + $color['alpha'] ?? null, + $color['type'] ?? null + ); + } - return $this; + public function isUsable(): bool + { + return $this->type !== '' && $this->value !== ''; } /** diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index dca1186e..d27db33e 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -257,8 +257,6 @@ class DataSeries $keys = array_keys($this->plotLabel); if (in_array($index, $keys)) { return $this->plotLabel[$index]; - } elseif (isset($keys[$index])) { - return $this->plotLabel[$keys[$index]]; } return false; @@ -339,8 +337,6 @@ class DataSeries $keys = array_keys($this->plotValues); if (in_array($index, $keys)) { return $this->plotValues[$index]; - } elseif (isset($keys[$index])) { - return $this->plotValues[$keys[$index]]; } return false; diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 0a2f5a85..d6a8dcca 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -7,7 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class DataSeriesValues +class DataSeriesValues extends Properties { const DATASERIES_TYPE_STRING = 'String'; const DATASERIES_TYPE_NUMBER = 'Number'; @@ -45,6 +45,12 @@ class DataSeriesValues */ private $pointMarker; + /** @var ChartColor */ + private $markerFillColor; + + /** @var ChartColor */ + private $markerBorderColor; + /** * Series Point Size. * @@ -69,23 +75,10 @@ class DataSeriesValues /** * Fill color (can be array with colors if dataseries have custom colors). * - * @var null|string|string[] + * @var null|ChartColor|ChartColor[] */ private $fillColor; - /** @var string */ - private $schemeClr = ''; - - /** @var string */ - private $prstClr = ''; - - /** - * Line Width. - * - * @var int - */ - private $lineWidth = 12700; - /** @var bool */ private $scatterLines = true; @@ -101,18 +94,23 @@ class DataSeriesValues * @param int $pointCount * @param mixed $dataValues * @param null|mixed $marker - * @param null|string|string[] $fillColor + * @param null|ChartColor|ChartColor[]|string|string[] $fillColor * @param string $pointSize */ public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { + parent::__construct(); + $this->markerFillColor = new ChartColor(); + $this->markerBorderColor = new ChartColor(); $this->setDataType($dataType); $this->dataSource = $dataSource; $this->formatCode = $formatCode; $this->pointCount = $pointCount; $this->dataValues = $dataValues; $this->pointMarker = $marker; - $this->fillColor = $fillColor; + if ($fillColor !== null) { + $this->setFillColor($fillColor); + } if (is_numeric($pointSize)) { $this->pointSize = (int) $pointSize; } @@ -198,6 +196,16 @@ class DataSeriesValues return $this; } + public function getMarkerFillColor(): ChartColor + { + return $this->markerFillColor; + } + + public function getMarkerBorderColor(): ChartColor + { + return $this->markerBorderColor; + } + /** * Get Point Size. */ @@ -252,37 +260,96 @@ class DataSeriesValues return $this->pointCount; } + /** + * Get fill color object. + * + * @return null|ChartColor|ChartColor[] + */ + public function getFillColorObject() + { + return $this->fillColor; + } + + private function stringToChartColor(string $fillString): ChartColor + { + $value = $type = ''; + if (substr($fillString, 0, 1) === '*') { + $type = 'schemeClr'; + $value = substr($fillString, 1); + } elseif (substr($fillString, 0, 1) === '/') { + $type = 'prstClr'; + $value = substr($fillString, 1); + } elseif ($fillString !== '') { + $type = 'srgbClr'; + $value = $fillString; + $this->validateColor($value); + } + + return new ChartColor($value, null, $type); + } + + private function chartColorToString(ChartColor $chartColor): string + { + $type = (string) $chartColor->getColorProperty('type'); + $value = (string) $chartColor->getColorProperty('value'); + if ($type === '' || $value === '') { + return ''; + } + if ($type === 'schemeClr') { + return "*$value"; + } + if ($type === 'prstClr') { + return "/$value"; + } + + return $value; + } + /** * Get fill color. * - * @return null|string|string[] HEX color or array with HEX colors + * @return string|string[] HEX color or array with HEX colors */ public function getFillColor() { - return $this->fillColor; + if ($this->fillColor === null) { + return ''; + } + if (is_array($this->fillColor)) { + $array = []; + foreach ($this->fillColor as $chartColor) { + $array[] = self::chartColorToString($chartColor); + } + + return $array; + } + + return self::chartColorToString($this->fillColor); } /** * Set fill color for series. * - * @param string|string[] $color HEX color or array with HEX colors + * @param ChartColor|ChartColor[]|string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ public function setFillColor($color) { if (is_array($color)) { - foreach ($color as $colorValue) { - if (substr($colorValue, 0, 1) !== '*' && substr($colorValue, 0, 1) !== '/') { - $this->validateColor($colorValue); + $this->fillColor = []; + foreach ($color as $fillString) { + if ($fillString instanceof ChartColor) { + $this->fillColor[] = $fillString; + } else { + $this->fillColor[] = self::stringToChartColor($fillString); } } - } else { - if (substr($color, 0, 1) !== '*' && substr($color, 0, 1) !== '/') { - $this->validateColor("$color"); - } + } elseif ($color instanceof ChartColor) { + $this->fillColor = $color; + } elseif (is_string($color)) { + $this->fillColor = self::stringToChartColor($color); } - $this->fillColor = $color; return $this; } @@ -306,24 +373,23 @@ class DataSeriesValues /** * Get line width for series. * - * @return int + * @return null|float|int */ public function getLineWidth() { - return $this->lineWidth; + return $this->lineStyleProperties['width']; } /** * Set line width for the series. * - * @param int $width + * @param null|float|int $width * * @return $this */ public function setLineWidth($width) { - $minWidth = 12700; - $this->lineWidth = max($minWidth, $width); + $this->lineStyleProperties['width'] = $width; return $this; } @@ -466,26 +532,33 @@ class DataSeriesValues return $this; } - public function getSchemeClr(): string + /** + * Smooth Line. + * + * @var bool + */ + private $smoothLine; + + /** + * Get Smooth Line. + * + * @return bool + */ + public function getSmoothLine() { - return $this->schemeClr; + return $this->smoothLine; } - public function setSchemeClr(string $schemeClr): self + /** + * Set Smooth Line. + * + * @param bool $smoothLine + * + * @return $this + */ + public function setSmoothLine($smoothLine) { - $this->schemeClr = $schemeClr; - - return $this; - } - - public function getPrstClr(): string - { - return $this->prstClr; - } - - public function setPrstClr(string $prstClr): self - { - $this->prstClr = $prstClr; + $this->smoothLine = $smoothLine; return $this; } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index a64a826f..fdc3c12b 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -59,6 +59,8 @@ abstract class Properties const LINE_STYLE_COMPOUND_TRIPLE = 'tri'; const LINE_STYLE_DASH_SOLID = 'solid'; const LINE_STYLE_DASH_ROUND_DOT = 'sysDot'; + const LINE_STYLE_DASH_SQUARE_DOT = 'sysDash'; + /** @deprecated 1.24 use LINE_STYLE_DASH_SQUARE_DOT instead */ const LINE_STYLE_DASH_SQUERE_DOT = 'sysDash'; const LINE_STYPE_DASH_DASH = 'dash'; const LINE_STYLE_DASH_DASH_DOT = 'dashDot'; @@ -68,7 +70,7 @@ abstract class Properties const LINE_STYLE_CAP_SQUARE = 'sq'; const LINE_STYLE_CAP_ROUND = 'rnd'; const LINE_STYLE_CAP_FLAT = 'flat'; - const LINE_STYLE_JOIN_ROUND = 'bevel'; + const LINE_STYLE_JOIN_ROUND = 'round'; const LINE_STYLE_JOIN_MITER = 'miter'; const LINE_STYLE_JOIN_BEVEL = 'bevel'; const LINE_STYLE_ARROW_TYPE_NOARROW = null; @@ -643,30 +645,6 @@ abstract class Properties return $this; } - /** - * Set Shadow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - protected function setShadowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->shadowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->shadowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->shadowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - /** * Set Shadow Blur. * @@ -766,6 +744,12 @@ abstract class Properties ], ]; + public function copyLineStyles(self $otherProperties): void + { + $this->lineStyleProperties = $otherProperties->lineStyleProperties; + $this->lineColor = $otherProperties->lineColor; + } + public function getLineColor(): ChartColor { return $this->lineColor; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 2c060d07..df25dac7 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -14,7 +14,6 @@ use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; @@ -90,6 +89,7 @@ class Chart case 'plotArea': $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; + $catAxRead = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { case 'layout': @@ -97,9 +97,11 @@ class Chart break; case 'catAx': + $catAxRead = true; if (isset($chartDetail->title)) { $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } + $xAxis->setAxisType('catAx'); $this->readEffects($chartDetail, $xAxis); if (isset($chartDetail->spPr)) { $sppr = $chartDetail->spPr->children($this->aNamespace); @@ -122,16 +124,22 @@ class Chart $axPos = null; if (isset($chartDetail->axPos)) { $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); - + } + if ($catAxRead) { + $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); + } elseif (!empty($axPos)) { switch ($axPos) { case 't': case 'b': $whichAxis = $xAxis; + $xAxis->setAxisType($chartDetailKey); break; case 'r': case 'l': $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); break; } @@ -373,14 +381,14 @@ class Chart case 'ser': $marker = null; $seriesIndex = ''; - $srgbClr = null; - $lineWidth = null; + $fillColor = null; $pointSize = null; $noFill = false; - $schemeClr = ''; - $prstClr = ''; $bubble3D = false; $dPtColors = []; + $markerFillColor = null; + $markerBorderColor = null; + $lineStyle = null; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -399,12 +407,16 @@ class Chart case 'spPr': $children = $seriesDetail->children($this->aNamespace); $ln = $children->ln; - $lineWidth = self::getAttribute($ln, 'w', 'string'); - if (is_countable($ln->noFill) && count($ln->noFill) === 1) { - $noFill = true; + if (isset($children->ln)) { + $ln = $children->ln; + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + $lineStyle = new GridLines(); + $this->readLineStyle($seriesDetails, $lineStyle); } if (isset($children->solidFill)) { - $this->readColor($children->solidFill, $srgbClr, $schemeClr, $prstClr); + $fillColor = new ChartColor($this->readColor($children->solidFill)); } break; @@ -414,13 +426,7 @@ class Chart $children = $seriesDetail->spPr->children($this->aNamespace); if (isset($children->solidFill)) { $arrayColors = $this->readColor($children->solidFill); - if ($arrayColors['type'] === 'srgbClr') { - $dptColors[$dptIdx] = $arrayColors['value']; - } elseif ($arrayColors['type'] === 'prstClr') { - $dptColors[$dptIdx] = '/' . $arrayColors['value']; - } else { - $dptColors[$dptIdx] = '*' . $arrayColors['value']; - } + $dptColors[$dptIdx] = new ChartColor($arrayColors); } } @@ -429,10 +435,13 @@ class Chart $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; - if (count($seriesDetail->spPr) === 1) { - $ln = $seriesDetail->spPr->children($this->aNamespace); - if (isset($ln->solidFill)) { - $this->readColor($ln->solidFill, $srgbClr, $schemeClr, $prstClr); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $markerFillColor = $this->readColor($children->solidFill); + } + if (isset($children->ln->solidFill)) { + $markerBorderColor = $this->readColor($children->ln->solidFill); } } @@ -446,19 +455,19 @@ class Chart break; case 'val': - $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'bubbleSize': - $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'bubble3D': @@ -478,36 +487,15 @@ class Chart $seriesValues[$seriesIndex]->setScatterLines(false); } } - if (is_numeric($lineWidth)) { + if ($lineStyle !== null) { if (isset($seriesLabel[$seriesIndex])) { - $seriesLabel[$seriesIndex]->setLineWidth((int) $lineWidth); + $seriesLabel[$seriesIndex]->copyLineStyles($lineStyle); } if (isset($seriesCategory[$seriesIndex])) { - $seriesCategory[$seriesIndex]->setLineWidth((int) $lineWidth); + $seriesCategory[$seriesIndex]->copyLineStyles($lineStyle); } if (isset($seriesValues[$seriesIndex])) { - $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); - } - } - if ($schemeClr) { - if (isset($seriesLabel[$seriesIndex])) { - $seriesLabel[$seriesIndex]->setSchemeClr($schemeClr); - } - if (isset($seriesCategory[$seriesIndex])) { - $seriesCategory[$seriesIndex]->setSchemeClr($schemeClr); - } - if (isset($seriesValues[$seriesIndex])) { - $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); - } - } elseif ($prstClr) { - if (isset($seriesLabel[$seriesIndex])) { - $seriesLabel[$seriesIndex]->setPrstClr($prstClr); - } - if (isset($seriesCategory[$seriesIndex])) { - $seriesCategory[$seriesIndex]->setPrstClr($prstClr); - } - if (isset($seriesValues[$seriesIndex])) { - $seriesValues[$seriesIndex]->setPrstClr($prstClr); + $seriesValues[$seriesIndex]->copyLineStyles($lineStyle); } } if ($bubble3D) { @@ -532,6 +520,39 @@ class Chart $seriesValues[$seriesIndex]->setFillColor($dptColors); } } + if ($markerFillColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + } + if ($markerBorderColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + } + if ($smoothLine) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSmoothLine(true); + } + } } } /** @phpstan-ignore-next-line */ @@ -544,11 +565,11 @@ class Chart /** * @return mixed */ - private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) + private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?ChartColor $fillColor = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's'); @@ -560,7 +581,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace)); $seriesValues @@ -571,7 +592,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's'); @@ -583,7 +604,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's'); @@ -698,8 +719,7 @@ class Chart $defaultLatin = null; $defaultEastAsian = null; $defaultComplexScript = null; - $defaultSrgbColor = ''; - $defaultSchemeColor = ''; + $defaultFontColor = null; if (isset($titleDetailPart->pPr->defRPr)) { /** @var ?int */ $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer'); @@ -729,7 +749,7 @@ class Chart $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string'); } if (isset($titleDetailPart->pPr->defRPr->solidFill)) { - $this->readColor($titleDetailPart->pPr->defRPr->solidFill, $defaultSrgbColor, $defaultSchemeClr); + $defaultFontColor = $this->readColor($titleDetailPart->pPr->defRPr->solidFill); } } foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { @@ -755,8 +775,7 @@ class Chart $latinName = null; $eastAsian = null; $complexScript = null; - $fontSrgbClr = ''; - $fontSchemeClr = ''; + $fontColor = null; $underlineColor = null; if (isset($titleDetailElement->rPr)) { // not used now, not sure it ever was, grandfathering @@ -781,10 +800,8 @@ class Chart $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); // not used now, not sure it ever was, grandfathering - /** @var ?string */ - $fontSrgbClr = self::getAttribute($titleDetailElement->rPr, 'color', 'string'); if (isset($titleDetailElement->rPr->solidFill)) { - $this->readColor($titleDetailElement->rPr->solidFill, $fontSrgbClr, $fontSchemeClr); + $fontColor = $this->readColor($titleDetailElement->rPr->solidFill); } /** @var ?bool */ @@ -834,19 +851,15 @@ class Chart if (is_int($fontSize)) { $objText->getFont()->setSize(floor($fontSize / 100)); $fontFound = true; + } else { + $objText->getFont()->setSize(null, true); } - $fontSrgbClr = $fontSrgbClr ?? $defaultSrgbColor; - if (!empty($fontSrgbClr)) { - $objText->getFont()->setColor(new Color($fontSrgbClr)); + $fontColor = $fontColor ?? $defaultFontColor; + if (!empty($fontColor)) { + $objText->getFont()->setChartColor($fontColor); $fontFound = true; } - // need to think about what to do here - //$fontSchemeClr = $fontSchemeClr ?? $defaultSchemeColor; - //if (!empty($fontSchemeClr)) { - // $objText->getFont()->setColor(new Color($fontSrgbClr)); - // $fontFound = true; - //} $bold = $bold ?? $defaultBold; if ($bold !== null) { @@ -1059,7 +1072,7 @@ class Chart 'innerShdw', ]; - private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null, ?string &$prstClr = null): array + private function readColor(SimpleXMLElement $colorXml): array { $result = [ 'type' => null, @@ -1070,13 +1083,6 @@ class Chart if (isset($colorXml->$type)) { $result['type'] = $type; $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string'); - if ($type === Properties::EXCEL_COLOR_TYPE_ARGB) { - $srgbClr = $result['value']; - } elseif ($type === Properties::EXCEL_COLOR_TYPE_SCHEME) { - $schemeClr = $result['value']; - } elseif ($type === Properties::EXCEL_COLOR_TYPE_STANDARD) { - $prstClr = $result['value']; - } if (isset($colorXml->$type->alpha)) { /** @var string */ $alpha = self::getAttribute($colorXml->$type->alpha, 'val', 'string'); @@ -1092,10 +1098,7 @@ class Chart return $result; } - /** - * @param null|GridLines $chartObject may be extended to include other types - */ - private function readLineStyle(SimpleXMLElement $chartDetail, $chartObject): void + private function readLineStyle(SimpleXMLElement $chartDetail, ?Properties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; @@ -1164,6 +1167,13 @@ class Chart if (!isset($whichAxis)) { return; } + if (isset($chartDetail->numFmt)) { + $whichAxis->setAxisNumberProperties( + (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), + null, + (int) self::getAttribute($chartDetail->numFmt, 'sourceLinked', 'int') + ); + } if (isset($chartDetail->crossBetween)) { $whichAxis->setCrossBetween((string) self::getAttribute($chartDetail->crossBetween, 'val', 'string')); } diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 4dbe7272..19d67563 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -21,7 +21,7 @@ class Font extends Supervisor protected $name = 'Calibri'; /** - * The following 6 are used only for chart titles, I think. + * The following 7 are used only for chart titles, I think. * *@var string */ @@ -41,6 +41,9 @@ class Font extends Supervisor /** @var ?ChartColor */ private $underlineColor; + + /** @var ?ChartColor */ + private $chartColor; // end of chart title items /** @@ -371,7 +374,7 @@ class Font extends Supervisor * * @return $this */ - public function setSize($sizeInPoints) + public function setSize($sizeInPoints, bool $nullOk = false) { if (is_string($sizeInPoints) || is_int($sizeInPoints)) { $sizeInPoints = (float) $sizeInPoints; // $pValue = 0 if given string is not numeric @@ -380,7 +383,9 @@ class Font extends Supervisor // Size must be a positive floating point number // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 if (!is_float($sizeInPoints) || !($sizeInPoints > 0)) { - $sizeInPoints = 10.0; + if (!$nullOk || $sizeInPoints !== null) { + $sizeInPoints = 10.0; + } } if ($this->isSupervisor) { @@ -593,8 +598,7 @@ class Font extends Supervisor public function setUnderlineColor(array $colorArray): self { if (!$this->isSupervisor) { - $this->underlineColor = new ChartColor(); - $this->underlineColor->setColorPropertiesArray($colorArray); + $this->underlineColor = new ChartColor($colorArray); } else { // should never be true // @codeCoverageIgnoreStart @@ -606,6 +610,30 @@ class Font extends Supervisor return $this; } + public function getChartColor(): ?ChartColor + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getChartColor(); + } + + return $this->chartColor; + } + + public function setChartColor(array $colorArray): self + { + if (!$this->isSupervisor) { + $this->chartColor = new ChartColor($colorArray); + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['chartColor' => $colorArray]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + /** * Get Underline. * @@ -713,6 +741,18 @@ class Font extends Supervisor return $this; } + private function hashChartColor(?ChartColor $underlineColor): string + { + if ($this->underlineColor === null) { + return ''; + } + + return + $this->underlineColor->getValue() + . $this->underlineColor->getType() + . (string) $this->underlineColor->getAlpha(); + } + /** * Get hash code. * @@ -723,14 +763,6 @@ class Font extends Supervisor if ($this->isSupervisor) { return $this->getSharedComponent()->getHashCode(); } - if ($this->underlineColor === null) { - $underlineColor = ''; - } else { - $underlineColor = - $this->underlineColor->getValue() - . $this->underlineColor->getType() - . (string) $this->underlineColor->getAlpha(); - } return md5( $this->name . @@ -749,7 +781,8 @@ class Font extends Supervisor $this->eastAsian, $this->complexScript, $this->strikeType, - $underlineColor, + $this->hashChartColor($this->chartColor), + $this->hashChartColor($this->underlineColor), (string) $this->baseLine, ] ) . @@ -762,6 +795,7 @@ class Font extends Supervisor $exportedArray = []; $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); $this->exportArray2($exportedArray, 'color', $this->getColor()); $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index acc6f3af..d242e602 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -419,7 +419,9 @@ class Chart extends WriterPart { // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc // In that case, xAxis is NOT a category. - if ($yAxis->getAxisIsNumericFormat()) { + if ($yAxis->getAxisType() !== '') { + $objWriter->startElement('c:' . $yAxis->getAxisType()); + } elseif ($yAxis->getAxisIsNumericFormat()) { $objWriter->startElement('c:valAx'); } else { $objWriter->startElement('c:catAx'); @@ -469,10 +471,6 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:pPr'); - $objWriter->startElement('a:defRPr'); - $objWriter->endElement(); - $objWriter->endElement(); $caption = $xAxisLabel->getCaption(); if (is_array($caption)) { @@ -622,7 +620,7 @@ class Chart extends WriterPart $objWriter->startElement('c:majorGridlines'); $objWriter->startElement('c:spPr'); - $this->writeGridlinesLn($objWriter, $majorGridlines); + $this->writeLineStyles($objWriter, $majorGridlines); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $majorGridlines); @@ -637,7 +635,7 @@ class Chart extends WriterPart $objWriter->startElement('c:minorGridlines'); $objWriter->startElement('c:spPr'); - $this->writeGridlinesLn($objWriter, $minorGridlines); + $this->writeLineStyles($objWriter, $minorGridlines); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $minorGridlines); @@ -661,10 +659,6 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:pPr'); - $objWriter->startElement('a:defRPr'); - $objWriter->endElement(); - $objWriter->endElement(); $caption = $yAxisLabel->getCaption(); if (is_array($caption)) { @@ -715,7 +709,7 @@ class Chart extends WriterPart $this->writeColor($objWriter, $xAxis->getFillColorObject()); - $this->writeGridlinesLn($objWriter, $xAxis); + $this->writeLineStyles($objWriter, $xAxis); $objWriter->startElement('a:effectLst'); $this->writeGlow($objWriter, $xAxis); @@ -849,40 +843,27 @@ class Chart extends WriterPart /** * Method writing plot series values. - * - * @param int $val value for idx (default: 3) - * @param string $fillColor hex color (default: FF9900) */ - private function writePlotSeriesValuesElement(XMLWriter $objWriter, $val = 3, $fillColor = 'FF9900'): void + private function writePlotSeriesValuesElement(XMLWriter $objWriter, int $val, ?ChartColor $fillColor): void { - if ($fillColor === '') { + if ($fillColor === null || !$fillColor->isUsable()) { return; } $objWriter->startElement('c:dPt'); + $objWriter->startElement('c:idx'); $objWriter->writeAttribute('val', $val); - $objWriter->endElement(); + $objWriter->endElement(); // c:idx $objWriter->startElement('c:bubble3D'); $objWriter->writeAttribute('val', 0); - $objWriter->endElement(); + $objWriter->endElement(); // c:bubble3D $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - if (substr($fillColor, 0, 1) === '*') { - $objWriter->startElement('a:schemeClr'); - $objWriter->writeAttribute('val', substr($fillColor, 1)); - } elseif (substr($fillColor, 0, 1) === '/') { - $objWriter->startElement('a:prstClr'); - $objWriter->writeAttribute('val', substr($fillColor, 1)); - } else { - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - } - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); + $this->writeColor($objWriter, $fillColor); + $objWriter->endElement(); // c:spPr + + $objWriter->endElement(); // c:dPt } /** @@ -934,20 +915,6 @@ class Chart extends WriterPart foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) { $objWriter->startElement('c:ser'); - $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); - if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) { - $fillColor = $plotLabel->getFillColor(); - if ($fillColor !== null && !is_array($fillColor)) { - $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - } - $objWriter->startElement('c:idx'); $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesIdx); $objWriter->endElement(); @@ -956,22 +923,35 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesRef); $objWriter->endElement(); - // Values - $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesRef); + $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); + $labelFill = null; + if ($plotLabel && $groupType === DataSeries::TYPE_LINECHART) { + $labelFill = $plotLabel->getFillColorObject(); + $labelFill = ($labelFill instanceof ChartColor) ? $labelFill : null; + } + if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) { + $fillColor = $plotLabel->getFillColorObject(); + if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) { + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $fillColor); + $objWriter->endElement(); // c:spPr + } + } - if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { - $fillColorValues = $plotSeriesValues->getFillColor(); + // Values + $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesIdx); + + if ($plotSeriesValues !== false && in_array($groupType, self::CUSTOM_COLOR_TYPES, true)) { + $fillColorValues = $plotSeriesValues->getFillColorObject(); if ($fillColorValues !== null && is_array($fillColorValues)) { foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { - $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? ''); + $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? null); } - } else { - $this->writePlotSeriesValuesElement($objWriter); } } // Labels - $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesRef); + $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) { $objWriter->startElement('c:tx'); $objWriter->startElement('c:strRef'); @@ -982,77 +962,54 @@ class Chart extends WriterPart // Formatting for the points if ( - $groupType == DataSeries::TYPE_LINECHART - || $groupType == DataSeries::TYPE_STOCKCHART - || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) - || ($plotSeriesValues !== false && ($plotSeriesValues->getSchemeClr() || $plotSeriesValues->getPrstClr())) + $plotSeriesValues !== false ) { - $plotLineWidth = 12700; - if ($plotSeriesValues) { - $plotLineWidth = $plotSeriesValues->getLineWidth(); - } - $objWriter->startElement('c:spPr'); - $schemeClr = $typeClr = ''; - if ($plotLabel) { - $schemeClr = $plotLabel->getSchemeClr(); - if ($schemeClr) { - $typeClr = 'schemeClr'; - } else { - $schemeClr = $plotLabel->getPrstClr(); - if ($schemeClr) { - $typeClr = 'prstClr'; - } + $fillObject = $labelFill ?? $plotSeriesValues->getFillColorObject(); + $callLineStyles = true; + if ($fillObject instanceof ChartColor && $fillObject->isUsable()) { + if ($groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('a:ln'); + $callLineStyles = false; + } + $this->writeColor($objWriter, $fillObject); + if (!$callLineStyles) { + $objWriter->endElement(); // a:ln } } - if ($schemeClr) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:$typeClr"); - $objWriter->writeAttribute('val', $schemeClr); - $objWriter->endElement(); - $objWriter->endElement(); + $nofill = $groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && !$plotSeriesValues->getScatterLines()); + if ($callLineStyles) { + $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill); } - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $plotLineWidth); - if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { - $objWriter->startElement('a:noFill'); - $objWriter->endElement(); - } elseif ($plotLabel) { - $fillColor = $plotLabel->getFillColor(); - if (is_string($fillColor)) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); - } - } - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:spPr } if ($plotSeriesValues) { $plotSeriesMarker = $plotSeriesValues->getPointMarker(); - if ($plotSeriesMarker) { + $markerFillColor = $plotSeriesValues->getMarkerFillColor(); + $fillUsed = $markerFillColor->IsUsable(); + $markerBorderColor = $plotSeriesValues->getMarkerBorderColor(); + $borderUsed = $markerBorderColor->isUsable(); + if ($plotSeriesMarker || $fillUsed || $borderUsed) { $objWriter->startElement('c:marker'); $objWriter->startElement('c:symbol'); - $objWriter->writeAttribute('val', $plotSeriesMarker); + if ($plotSeriesMarker) { + $objWriter->writeAttribute('val', $plotSeriesMarker); + } $objWriter->endElement(); if ($plotSeriesMarker !== 'none') { $objWriter->startElement('c:size'); $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize()); - $objWriter->endElement(); - $fillColor = $plotSeriesValues->getFillColor(); - if (is_string($fillColor) && $fillColor !== '') { - $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); // srgbClr - $objWriter->endElement(); // solidFill - $objWriter->endElement(); // spPr + $objWriter->endElement(); // c:size + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $markerFillColor); + if ($borderUsed) { + $objWriter->startElement('a:ln'); + $this->writeColor($objWriter, $markerBorderColor); + $objWriter->endElement(); // a:ln } + $objWriter->endElement(); // spPr } $objWriter->endElement(); @@ -1066,7 +1023,7 @@ class Chart extends WriterPart } // Category Labels - $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesRef); + $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx); if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) { $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries(); @@ -1112,7 +1069,7 @@ class Chart extends WriterPart $objWriter->endElement(); if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') { $objWriter->startElement('c:smooth'); - $objWriter->writeAttribute('val', '1'); + $objWriter->writeAttribute('val', $plotSeriesValues->getSmoothLine() ? '1' : '0'); $objWriter->endElement(); } } @@ -1268,6 +1225,14 @@ class Chart extends WriterPart } } + private const CUSTOM_COLOR_TYPES = [ + DataSeries::TYPE_BARCHART, + DataSeries::TYPE_BARCHART_3D, + DataSeries::TYPE_PIECHART, + DataSeries::TYPE_PIECHART_3D, + DataSeries::TYPE_DONUTCHART, + ]; + /** * Write Bubble Chart Details. */ @@ -1384,8 +1349,8 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); $objWriter->startElement('mc:Choice'); - $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); $objWriter->writeAttribute('Requires', 'c14'); + $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); $objWriter->startElement('c14:style'); $objWriter->writeAttribute('val', '102'); @@ -1509,12 +1474,7 @@ class Chart extends WriterPart $objWriter->endElement(); //end softEdge } - /** - * Write Line Style for Gridlines. - * - * @param Axis|GridLines $gridlines - */ - private function writeGridlinesLn(XMLWriter $objWriter, $gridlines): void + private function writeLineStyles(XMLWriter $objWriter, Properties $gridlines, bool $noFill = false): void { $objWriter->startElement('a:ln'); $widthTemp = $gridlines->getLineStyleProperty('width'); @@ -1523,7 +1483,12 @@ class Chart extends WriterPart } $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap')); $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound')); - $this->writeColor($objWriter, $gridlines->getLineColor()); + if ($noFill) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); + } else { + $this->writeColor($objWriter, $gridlines->getLineColor()); + } $dash = $gridlines->getLineStyleProperty('dash'); if (!empty($dash)) { @@ -1544,16 +1509,16 @@ class Chart extends WriterPart if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) { $objWriter->startElement('a:headEnd'); $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowParameters('head', 'w')); - $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowParameters('head', 'len')); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('head')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('head')); $objWriter->endElement(); } if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) { $objWriter->startElement('a:tailEnd'); $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowParameters('end', 'w')); - $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowParameters('end', 'len')); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('end')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('end')); $objWriter->endElement(); } $objWriter->endElement(); //end ln diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index 8a376df4..8b293bc1 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -198,7 +199,7 @@ class StringTable extends WriterPart * @param RichText|string $richText text string or Rich text * @param string $prefix Optional Namespace prefix */ - public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = null): void + public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = ''): void { if (!$richText instanceof RichText) { $textRun = $richText; @@ -207,7 +208,7 @@ class StringTable extends WriterPart $run->setFont(null); } - if ($prefix !== null) { + if ($prefix !== '') { $prefix .= ':'; } @@ -249,27 +250,10 @@ class StringTable extends WriterPart } // Color - $objWriter->startElement($prefix . 'solidFill'); - $objWriter->startElement($prefix . 'srgbClr'); - $objWriter->writeAttribute('val', $element->getFont()->getColor()->getRGB()); - $objWriter->endElement(); // srgbClr - $objWriter->endElement(); // solidFill + $this->writeChartTextColor($objWriter, $element->getFont()->getChartColor(), $prefix); // Underscore Color - $underlineColor = $element->getFont()->getUnderlineColor(); - if ($underlineColor !== null) { - $type = $underlineColor->getType(); - $value = $underlineColor->getValue(); - if (!empty($type) && !empty($value)) { - $objWriter->startElement($prefix . 'uFill'); - $objWriter->startElement($prefix . 'solidFill'); - $objWriter->startElement($prefix . $type); - $objWriter->writeAttribute('val', $value); - $objWriter->endElement(); // schemeClr - $objWriter->endElement(); // solidFill - $objWriter->endElement(); // uFill - } - } + $this->writeChartTextColor($objWriter, $element->getFont()->getUnderlineColor(), $prefix, 'uFill'); // fontName if ($element->getFont()->getLatin()) { @@ -300,6 +284,33 @@ class StringTable extends WriterPart } } + private function writeChartTextColor(XMLWriter $objWriter, ?ChartColor $underlineColor, string $prefix, ?string $openTag = ''): void + { + if ($underlineColor !== null) { + $type = $underlineColor->getType(); + $value = $underlineColor->getValue(); + if (!empty($type) && !empty($value)) { + if ($openTag !== '') { + $objWriter->startElement($prefix . $openTag); + } + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . $type); + $objWriter->writeAttribute('val', $value); + $alpha = $underlineColor->getAlpha(); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); // srgbClr/schemeClr/prstClr + $objWriter->endElement(); // solidFill + if ($openTag !== '') { + $objWriter->endElement(); // uFill + } + } + } + } + /** * Flip string table (for index searching). * diff --git a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php index ad7fc776..0ed4a1b4 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php @@ -119,6 +119,7 @@ class AxisGlowTest extends AbstractFunctional $xAxis->setSoftEdges($softEdgesX); self::assertEquals($yGlowSize, $yAxis->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $yAxis->getGlowProperty('color')); + self::assertSame($expectedGlowColor['value'], $yAxis->getGlowProperty(['color', 'value'])); self::assertEquals($softEdgesY, $yAxis->getSoftEdgesSize()); self::assertEquals($softEdgesX, $xAxis->getSoftEdgesSize()); diff --git a/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php b/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php new file mode 100644 index 00000000..824e9600 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php @@ -0,0 +1,162 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testBarchartColor(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + // Custom colors for dataSeries (gray, blue, red, orange) + $colors = [ + 'cccccc', + '*accent1', // use schemeClr, was '00abb8', + '/green', // use prstClr, was 'b8292f', + 'eb8500', + ]; + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels1 = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + 'Worksheet!$C$1', + null, + 1 + ), // 2011 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + // Custom Colors + $dataSeriesValues1Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4); + $dataSeriesValues1Element->setFillColor($colors); + $dataSeriesValues1 = [$dataSeriesValues1Element]; + + // Build the dataseries + $series1 = new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ); + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + $layout1->setShowPercent(true); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, [$series1]); + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // no Y-Axis for Pie Chart + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + $dataSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $dataSeries2); + $plotValues = $dataSeries2[0]->getPlotValues(); + self::assertCount(1, $plotValues); + $fillColors = $plotValues[0]->getFillColor(); + self::assertSame($colors, $fillColors); + + $writer = new XlsxWriter($reloadedSpreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart2); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(4, substr_count($data, '')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php index 71cda504..b8041623 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php @@ -58,7 +58,10 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('00B050', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('00B050', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $yAxisLabel = $chart->getYAxisLabel(); $captionArray = $yAxisLabel->getCaption(); @@ -73,7 +76,10 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('FF0000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('FF0000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index d79cba6f..38884eaf 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; @@ -65,7 +66,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); $plotSeries = $plotArea->getPlotGroup(); @@ -75,19 +79,22 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(7, $values->getPointSize()); - self::assertSame('FFFF00', $values->getFillColor()); + // Had been testing for Fill Color, but we actually + // meant to test for marker color, which is now distinct. + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } @@ -135,7 +142,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $run = $elements[1]; self::assertInstanceOf(Run::class, $run); @@ -149,7 +159,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('single', $font->getUnderline()); - self::assertSame('00B0F0', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('00B0F0', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $run = $elements[2]; self::assertInstanceOf(Run::class, $run); @@ -163,7 +176,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); $plotSeries = $plotArea->getPlotGroup(); @@ -173,19 +189,22 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(7, $values->getPointSize()); - self::assertSame('FFFF00', $values->getFillColor()); + // Had been testing for Fill Color, but we actually + // meant to test for marker color, which is now distinct. + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } @@ -232,7 +251,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); $plotSeries = $plotArea->getPlotGroup(); @@ -242,17 +264,19 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertTrue($values->getScatterLines()); - self::assertSame(12700, $values->getLineWidth()); + // the default value of 1 point is no longer written out + // when not explicitly specified. + self::assertNull($values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertTrue($values->getScatterLines()); - self::assertSame(12700, $values->getLineWidth()); + self::assertNull($values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertTrue($values->getScatterLines()); - self::assertSame(12700, $values->getLineWidth()); + self::assertNull($values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); @@ -303,7 +327,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); } $plotArea = $chart->getPlotArea(); @@ -314,19 +341,97 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(7, $values->getPointSize()); - self::assertSame('FFFF00', $values->getFillColor()); + // Had been testing for Fill Color, but we actually + // meant to test for marker color, which is now distinct. + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter8(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart8.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $plotArea = $chart->getPlotArea(); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertSame(31750 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); + + self::assertSame('sq', $values->getLineStyleProperty('cap')); + self::assertSame('tri', $values->getLineStyleProperty('compound')); + self::assertSame('sysDash', $values->getLineStyleProperty('dash')); + self::assertSame('miter', $values->getLineStyleProperty('join')); + self::assertSame('arrow', $values->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertSame('med', $values->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertSame('sm', $values->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertSame('triangle', $values->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertSame('med', $values->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertSame('lg', $values->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertSame('accent1', $values->getLineColorProperty('value')); + self::assertSame('schemeClr', $values->getLineColorProperty('type')); + self::assertSame(40, $values->getLineColorProperty('alpha')); + self::assertSame('', $values->getFillColor()); + + self::assertSame(7, $values->getPointSize()); + self::assertSame('diamond', $values->getPointMarker()); + self::assertSame('0070C0', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); + self::assertSame('002060', $values->getMarkerBorderColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerBorderColor()->getType()); + + $values = $plotValues[1]; + self::assertSame(7, $values->getPointSize()); + self::assertSame('square', $values->getPointMarker()); + self::assertSame('accent6', $values->getMarkerFillColor()->getValue()); + self::assertSame('schemeClr', $values->getMarkerFillColor()->getType()); + self::assertSame(3, $values->getMarkerFillColor()->getAlpha()); + self::assertSame('0FF000', $values->getMarkerBorderColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerBorderColor()->getType()); + self::assertNull($values->getMarkerBorderColor()->getAlpha()); + + $values = $plotValues[2]; + self::assertSame(7, $values->getPointSize()); + self::assertSame('triangle', $values->getPointMarker()); + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); + self::assertNull($values->getMarkerFillColor()->getAlpha()); + self::assertSame('accent4', $values->getMarkerBorderColor()->getValue()); + self::assertSame('schemeClr', $values->getMarkerBorderColor()->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php index 3123278f..6a4673fd 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php @@ -84,12 +84,16 @@ class Charts32XmlTest extends TestCase $chart = $charts[0]; self::assertNotNull($chart); $xAxis = $chart->getChartAxisX(); + $yAxis = $chart->getChartAxisY(); self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); if (is_bool($numeric)) { $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, true); } - $yAxis = $chart->getChartAxisY(); + self::assertSame('valAx', $yAxis->getAxisType()); + self::assertSame('valAx', $xAxis->getAxisType()); self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + $xAxis->setAxisType(''); + $yAxis->setAxisType(''); if (is_bool($numeric)) { $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); $yAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); @@ -119,6 +123,34 @@ class Charts32XmlTest extends TestCase ]; } + public function testCatAxValAxFromRead(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + $yAxis = $chart->getChartAxisY(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); + self::assertSame('valAx', $yAxis->getAxisType()); + self::assertSame('valAx', $xAxis->getAxisType()); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame(0, substr_count($data, '')); + self::assertSame(2, substr_count($data, '')); + } + public function testAreaPrstClr(): void { $file = self::DIRECTORY . '32readwriteAreaChart4.xlsx'; diff --git a/tests/PhpSpreadsheetTests/Chart/ColorTest.php b/tests/PhpSpreadsheetTests/Chart/ColorTest.php new file mode 100644 index 00000000..e8326e52 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ColorTest.php @@ -0,0 +1,32 @@ +getType()); + self::assertSame('800000', $color->getValue()); + $color->setColorProperties('*accent1'); + self::assertSame('schemeClr', $color->getType()); + self::assertSame('accent1', $color->getValue()); + $color->setColorProperties('/red'); + self::assertSame('prstClr', $color->getType()); + self::assertSame('red', $color->getValue()); + } + + public function testDataSeriesValues(): void + { + $dsv = new DataSeriesValues(); + $dsv->setFillColor([new ChartColor(), new ChartColor()]); + self::assertSame(['', ''], $dsv->getFillColor()); + $dsv->setFillColor('cccccc'); + self::assertSame('cccccc', $dsv->getFillColor()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php b/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php new file mode 100644 index 00000000..a27397f9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php @@ -0,0 +1,172 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testDataSeriesValues(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + null, // plotType + null, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + self::assertEmpty($series->getPlotType()); + self::assertEmpty($series->getPlotGrouping()); + self::assertFalse($series->getSmoothLine()); + $series->setPlotType(DataSeries::TYPE_AREACHART); + $series->setPlotGrouping(DataSeries::GROUPING_PERCENT_STACKED); + $series->setSmoothLine(true); + self::assertSame(DataSeries::TYPE_AREACHART, $series->getPlotType()); + self::assertSame(DataSeries::GROUPING_PERCENT_STACKED, $series->getPlotGrouping()); + self::assertTrue($series->getSmoothLine()); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + self::assertSame(1, $chart->getPlotArea()->getPlotGroupCount()); + $plotValues = $chart->getPlotArea()->getPlotGroup()[0]->getPlotValues(); + self::assertCount(3, $plotValues); + self::assertSame([], $plotValues[1]->getDataValues()); + self::assertNull($plotValues[1]->getDataValue()); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotValues2 = $chart2->getPlotArea()->getPlotGroup()[0]->getPlotValues(); + self::assertCount(3, $plotValues2); + self::assertSame([15.0, 73.0, 61.0, 32.0], $plotValues2[1]->getDataValues()); + self::assertSame([15.0, 73.0, 61.0, 32.0], $plotValues2[1]->getDataValue()); + $labels2 = $chart->getPlotArea()->getPlotGroup()[0]->getPlotLabels(); + self::assertCount(3, $labels2); + self::assertSame(2010, $labels2[0]->getDataValue()); + $dataSeries = $chart->getPlotArea()->getPlotGroup()[0]; + self::assertFalse($dataSeries->getPlotValuesByIndex(99)); + self::assertNotFalse($dataSeries->getPlotValuesByIndex(0)); + self::assertSame([12, 56, 52, 30], $dataSeries->getPlotValuesByIndex(0)->getDataValues()); + self::assertSame(DataSeries::TYPE_AREACHART, $dataSeries->getPlotType()); + self::assertSame(DataSeries::GROUPING_PERCENT_STACKED, $dataSeries->getPlotGrouping()); + self::assertTrue($dataSeries->getSmoothLine()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testSomeProperties(): void + { + $dataSeriesValues = new DataSeriesValues(); + self::assertNull($dataSeriesValues->getDataSource()); + self::assertEmpty($dataSeriesValues->getPointMarker()); + self::assertSame(3, $dataSeriesValues->getPointSize()); + $dataSeriesValues->setDataSource('Worksheet!$B$1') + ->setPointMarker('square') + ->setPointSize(6); + self::assertSame('Worksheet!$B$1', $dataSeriesValues->getDataSource()); + self::assertSame('square', $dataSeriesValues->getPointMarker()); + self::assertSame(6, $dataSeriesValues->getPointSize()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php b/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php index c34ca697..47c2fb89 100644 --- a/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php +++ b/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Exception; use PHPUnit\Framework\TestCase; @@ -51,13 +52,14 @@ class DataSeriesValuesTest extends TestCase public function testGetLineWidth(): void { $testInstance = new DataSeriesValues(); - self::assertEquals(12700, $testInstance->getLineWidth(), 'should have default'); + // default has changed to null from 1 point (12700) + self::assertNull($testInstance->getLineWidth(), 'should have default'); - $testInstance->setLineWidth(40000); - self::assertEquals(40000, $testInstance->getLineWidth()); + $testInstance->setLineWidth(40000 / Properties::POINTS_WIDTH_MULTIPLIER); + self::assertEquals(40000 / Properties::POINTS_WIDTH_MULTIPLIER, $testInstance->getLineWidth()); $testInstance->setLineWidth(1); - self::assertEquals(12700, $testInstance->getLineWidth(), 'should enforce minimum width'); + self::assertEquals(12700 / Properties::POINTS_WIDTH_MULTIPLIER, $testInstance->getLineWidth(), 'should enforce minimum width'); } public function testFillColorCorrectInput(): void diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue589Test.php b/tests/PhpSpreadsheetTests/Chart/Issue589Test.php similarity index 96% rename from tests/PhpSpreadsheetTests/Writer/Xlsx/Issue589Test.php rename to tests/PhpSpreadsheetTests/Chart/Issue589Test.php index 82478fb9..747cba74 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue589Test.php +++ b/tests/PhpSpreadsheetTests/Chart/Issue589Test.php @@ -1,6 +1,6 @@ ', $actualXml); + self::assertXmlStringEqualsXmlString('', $actualXml); } } } @@ -153,7 +153,7 @@ class Issue589Test extends TestCase if ($actualXml === false) { self::fail('Failure saving the spPr element as xml string!'); } else { - self::assertXmlStringEqualsXmlString('', $actualXml); + self::assertXmlStringEqualsXmlString('', $actualXml); } } } diff --git a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php index 58c024c1..e96d6c14 100644 --- a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php @@ -123,6 +123,35 @@ class ShadowPresetsTest extends TestCase } } + public function testPreset0(): void + { + $axis = new Axis(); + $axis->setShadowProperties(0); + $expectedShadow = [ + 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + 'color' => [ + 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $axis->getShadowProperty($key), $key); + } + } + public function testOutOfRangePresets(): void { $axis = new Axis(); From 0719c7cb8756d54ab0ff24c511b62a86aa7b3b6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 05:21:24 -0700 Subject: [PATCH 034/156] Bump dompdf/dompdf from 1.2.2 to 2.0.0 (#2917) Bumps [dompdf/dompdf](https://github.com/dompdf/dompdf) from 1.2.2 to 2.0.0. - [Release notes](https://github.com/dompdf/dompdf/releases) - [Commits](https://github.com/dompdf/dompdf/compare/v1.2.2...v2.0.0) --- updated-dependencies: - dependency-name: dompdf/dompdf dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.json | 2 +- composer.lock | 316 ++++++++++++++++++-------------------------------- 2 files changed, 114 insertions(+), 204 deletions(-) diff --git a/composer.json b/composer.json index f0c40cf8..16991514 100644 --- a/composer.json +++ b/composer.json @@ -79,7 +79,7 @@ }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "dompdf/dompdf": "^1.0", + "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", "jpgraph/jpgraph": "^4.0", "mpdf/mpdf": "8.1.1", diff --git a/composer.lock b/composer.lock index 9945b9a2..648bf630 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fa9fa4814df8320d600551ca8ec11883", + "content-hash": "6cbb20f8d8f2daae0aeb72431cda0980", "packages": [ { "name": "ezyang/htmlpurifier", @@ -25,12 +25,12 @@ }, "type": "library", "autoload": { - "psr-0": { - "HTMLPurifier": "library/" - }, "files": [ "library/HTMLPurifier.composer.php" ], + "psr-0": { + "HTMLPurifier": "library/" + }, "exclude-from-classmap": [ "/library/HTMLPurifier/Language/" ] @@ -51,10 +51,6 @@ "keywords": [ "html" ], - "support": { - "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" - }, "time": "2021-12-25T01:21:49+00:00" }, { @@ -116,10 +112,6 @@ "stream", "zip" ], - "support": { - "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/master" - }, "funding": [ { "url": "https://opencollective.com/zipstream", @@ -173,10 +165,6 @@ "complex", "mathematics" ], - "support": { - "issues": "https://github.com/MarkBaker/PHPComplex/issues", - "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.1" - }, "time": "2021-06-29T15:32:53+00:00" }, { @@ -229,10 +217,6 @@ "matrix", "vector" ], - "support": { - "issues": "https://github.com/MarkBaker/PHPMatrix/issues", - "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" - }, "time": "2021-07-01T19:01:15+00:00" }, { @@ -338,9 +322,6 @@ "psr", "psr-18" ], - "support": { - "source": "https://github.com/php-fig/http-client/tree/master" - }, "time": "2020-06-29T06:28:15+00:00" }, { @@ -393,9 +374,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-factory/tree/master" - }, "time": "2019-04-30T12:38:16+00:00" }, { @@ -446,9 +424,6 @@ "request", "response" ], - "support": { - "source": "https://github.com/php-fig/http-message/tree/master" - }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -497,9 +472,6 @@ "psr-16", "simple-cache" ], - "support": { - "source": "https://github.com/php-fig/simple-cache/tree/master" - }, "time": "2017-10-23T01:57:42+00:00" }, { @@ -533,12 +505,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -563,9 +535,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -635,10 +604,6 @@ "regex", "regular expression" ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.0" - }, "funding": [ { "url": "https://packagist.com", @@ -715,11 +680,6 @@ "validation", "versioning" ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.6" - }, "funding": [ { "url": "https://packagist.com", @@ -781,11 +741,6 @@ "Xdebug", "performance" ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.3" - }, "funding": [ { "url": "https://packagist.com", @@ -808,23 +763,26 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7" + "reference": "04f4e8f6716241cb9200774ff73cb99fbb81e09a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", - "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/04f4e8f6716241cb9200774ff73cb99fbb81e09a", + "reference": "04f4e8f6716241cb9200774ff73cb99fbb81e09a", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "type": "composer-plugin", "extra": { @@ -845,6 +803,10 @@ "email": "franck.nijhof@dealerdirect.com", "homepage": "http://www.frenck.nl", "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -856,6 +818,7 @@ "codesniffer", "composer", "installer", + "phpcbf", "phpcs", "plugin", "qa", @@ -866,11 +829,7 @@ "stylecheck", "tests" ], - "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" - }, - "time": "2021-08-16T14:43:41+00:00" + "time": "2022-06-26T10:27:07+00:00" }, { "name": "doctrine/annotations", @@ -938,10 +897,6 @@ "docblock", "parser" ], - "support": { - "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.2" - }, "time": "2021-08-05T19:00:23+00:00" }, { @@ -1070,10 +1025,6 @@ "parser", "php" ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1092,21 +1043,22 @@ }, { "name": "dompdf/dompdf", - "version": "v1.2.2", + "version": "v2.0.0", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "5031045d9640b38cfc14aac9667470df09c9e090" + "reference": "79573d8b8a141ec8a17312515de8740eed014fa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/5031045d9640b38cfc14aac9667470df09c9e090", - "reference": "5031045d9640b38cfc14aac9667470df09c9e090", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/79573d8b8a141ec8a17312515de8740eed014fa9", + "reference": "79573d8b8a141ec8a17312515de8740eed014fa9", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", + "masterminds/html5": "^2.0", "phenx/php-font-lib": "^0.5.4", "phenx/php-svg-lib": "^0.3.3 || ^0.4.0", "php": "^7.1 || ^8.0" @@ -1153,7 +1105,7 @@ ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", - "time": "2022-04-27T13:50:54+00:00" + "time": "2022-06-21T21:14:57+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -1232,10 +1184,6 @@ } ], "description": "A tool to automatically fix PHP code style", - "support": { - "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.4.0" - }, "funding": [ { "url": "https://github.com/keradus", @@ -1282,13 +1230,74 @@ "jpgraph", "pie" ], - "support": { - "issues": "https://github.com/ztec/JpGraph/issues", - "source": "https://github.com/ztec/JpGraph/tree/4.x" - }, "abandoned": true, "time": "2017-02-23T09:44:15+00:00" }, + { + "name": "masterminds/html5", + "version": "2.7.5", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f640ac1bdddff06ea333a920c95bbad8872429ab", + "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "time": "2021-07-01T14:25:37+00:00" + }, { "name": "mpdf/mpdf", "version": "v8.1.1", @@ -1511,11 +1520,6 @@ "pseudorandom", "random" ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, "time": "2020-10-15T08:29:30+00:00" }, { @@ -1572,10 +1576,6 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" - }, "time": "2021-07-20T11:28:43+00:00" }, { @@ -1753,10 +1753,6 @@ "keywords": [ "diff" ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" - }, "time": "2020-10-14T08:32:19+00:00" }, { @@ -1865,10 +1861,6 @@ "phpcs", "standards" ], - "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" - }, "time": "2019-12-27T09:44:58+00:00" }, { @@ -1918,10 +1910,6 @@ "reflection", "static analysis" ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, "time": "2020-06-27T09:03:43+00:00" }, { @@ -1975,10 +1963,6 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, "time": "2021-10-19T17:43:47+00:00" }, { @@ -2634,9 +2618,6 @@ "psr", "psr-6" ], - "support": { - "source": "https://github.com/php-fig/cache/tree/master" - }, "time": "2016-08-06T20:24:11+00:00" }, { @@ -2681,10 +2662,6 @@ "container-interop", "psr" ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" - }, "time": "2021-03-05T17:36:06+00:00" }, { @@ -2731,10 +2708,6 @@ "psr", "psr-14" ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, "time": "2019-01-08T18:20:26+00:00" }, { @@ -2782,9 +2755,6 @@ "psr", "psr-3" ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -3796,10 +3766,6 @@ "fpdi", "pdf" ], - "support": { - "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.3.6" - }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", @@ -3939,9 +3905,6 @@ "console", "terminal" ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.2" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4006,9 +3969,6 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4091,9 +4051,6 @@ ], "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4170,9 +4127,6 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4234,9 +4188,6 @@ ], "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4297,9 +4248,6 @@ ], "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.2" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4366,9 +4314,6 @@ "configuration", "options" ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4495,12 +4440,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4526,9 +4471,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4576,12 +4518,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4610,9 +4552,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4657,12 +4596,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4689,9 +4628,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4736,12 +4672,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4772,9 +4708,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4819,12 +4752,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4851,9 +4784,6 @@ "portable", "shim" ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4913,9 +4843,6 @@ ], "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.2" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4996,9 +4923,6 @@ "interoperability", "standards" ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5058,9 +4982,6 @@ ], "description": "Provides a way to profile code", "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.0" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5110,12 +5031,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -5144,9 +5065,6 @@ "utf-8", "utf8" ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.4.2" - }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5269,10 +5187,6 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" - }, "funding": [ { "url": "https://github.com/theseer", @@ -5333,10 +5247,6 @@ "check", "validate" ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, "time": "2021-03-09T10:59:23+00:00" } ], From ec01a71c0b59c46b6e5d558b6360fc216421ab90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Jul 2022 05:36:47 -0700 Subject: [PATCH 035/156] Bump phpunit/phpunit from 9.5.20 to 9.5.21 (#2918) Bumps [phpunit/phpunit](https://github.com/sebastianbergmann/phpunit) from 9.5.20 to 9.5.21. - [Release notes](https://github.com/sebastianbergmann/phpunit/releases) - [Changelog](https://github.com/sebastianbergmann/phpunit/blob/main/ChangeLog-9.5.md) - [Commits](https://github.com/sebastianbergmann/phpunit/compare/9.5.20...9.5.21) --- updated-dependencies: - dependency-name: phpunit/phpunit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/composer.lock b/composer.lock index 648bf630..06116d85 100644 --- a/composer.lock +++ b/composer.lock @@ -1427,16 +1427,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.14.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", + "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", "shasum": "" }, "require": { @@ -1475,7 +1475,7 @@ "parser", "php" ], - "time": "2021-11-30T19:35:32+00:00" + "time": "2022-05-31T20:59:12+00:00" }, { "name": "paragonie/random_compat", @@ -2477,16 +2477,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.20", + "version": "9.5.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", + "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", "shasum": "" }, "require": { @@ -2520,7 +2520,6 @@ "sebastian/version": "^3.0.2" }, "require-dev": { - "ext-pdo": "*", "phpspec/prophecy-phpunit": "^2.0.1" }, "suggest": { @@ -2572,7 +2571,7 @@ "type": "github" } ], - "time": "2022-04-01T12:37:26+00:00" + "time": "2022-06-19T12:14:25+00:00" }, { "name": "psr/cache", @@ -4332,16 +4331,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -4356,7 +4355,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4407,7 +4406,7 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -5197,21 +5196,21 @@ }, { "name": "webmozart/assert", - "version": "1.10.0", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" }, "conflict": { "phpstan/phpstan": "<0.12.20", @@ -5247,7 +5246,7 @@ "check", "validate" ], - "time": "2021-03-09T10:59:23+00:00" + "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], From cbc8ed0845525aff8d8b0381d72f2f2657f94b1b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 2 Jul 2022 01:16:11 -0700 Subject: [PATCH 036/156] Changes for Phpstan 1.8.0 (#2920) Dependabot pushed changes. As usual for Phpstan, some changes to code are required. --- phpstan-baseline.neon | 5 ----- src/PhpSpreadsheet/Helper/Dimension.php | 2 ++ src/PhpSpreadsheet/Worksheet/MemoryDrawing.php | 8 ++++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 858502fd..14160edf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1250,11 +1250,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/DefinedName.php - - - message: "#^Cannot use array destructuring on array\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Dimension.php - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Helper/Dimension.php b/src/PhpSpreadsheet/Helper/Dimension.php index 4e3e6680..425c9a61 100644 --- a/src/PhpSpreadsheet/Helper/Dimension.php +++ b/src/PhpSpreadsheet/Helper/Dimension.php @@ -57,8 +57,10 @@ class Dimension public function __construct(string $dimension) { + // @phpstan-ignore-next-line [$size, $unit] = sscanf($dimension, '%[1234567890.]%s'); $unit = strtolower(trim($unit ?? '')); + $size = (float) $size; // If a UoM is specified, then convert the size to pixels for internal storage if (isset(self::ABSOLUTE_UNITS[$unit])) { diff --git a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index e65541dc..b2d1aa69 100644 --- a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -86,8 +86,8 @@ class MemoryDrawing extends BaseDrawing return; } - $width = imagesx($this->imageResource); - $height = imagesy($this->imageResource); + $width = (int) imagesx($this->imageResource); + $height = (int) imagesy($this->imageResource); if (imageistruecolor($this->imageResource)) { $clone = imagecreatetruecolor($width, $height); @@ -150,8 +150,8 @@ class MemoryDrawing extends BaseDrawing if ($this->imageResource !== null) { // Get width/height - $this->width = imagesx($this->imageResource); - $this->height = imagesy($this->imageResource); + $this->width = (int) imagesx($this->imageResource); + $this->height = (int) imagesy($this->imageResource); } return $this; From f90adcf28eda3fad2e95ba299d91e985facf58a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Jul 2022 01:29:11 -0700 Subject: [PATCH 037/156] Bump phpstan/phpstan from 1.7.7 to 1.8.0 (#2919) Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan) from 1.7.7 to 1.8.0. - [Release notes](https://github.com/phpstan/phpstan/releases) - [Changelog](https://github.com/phpstan/phpstan/blob/1.8.x/CHANGELOG.md) - [Commits](https://github.com/phpstan/phpstan/compare/1.7.7...1.8.0) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: oleibman <10341515+oleibman@users.noreply.github.com> --- composer.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 06116d85..4ac9f631 100644 --- a/composer.lock +++ b/composer.lock @@ -2076,16 +2076,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.7.7", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "cadad14ac63d8a432e01ae89ac5d0ff6fc3b16ab" + "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cadad14ac63d8a432e01ae89ac5d0ff6fc3b16ab", - "reference": "cadad14ac63d8a432e01ae89ac5d0ff6fc3b16ab", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b7648d4ee9321665acaf112e49da9fd93df8fbd5", + "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5", "shasum": "" }, "require": { @@ -2127,7 +2127,7 @@ "type": "tidelift" } ], - "time": "2022-05-31T13:58:21+00:00" + "time": "2022-06-29T08:53:31+00:00" }, { "name": "phpstan/phpstan-phpunit", From c3f53854b61f71f0d80ea449cbc3245514fa200d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 2 Jul 2022 08:53:39 -0700 Subject: [PATCH 038/156] Php/iconv Should Not Treat FFFE/FFFF as Valid (#2910) Fix #2897. We have been relying on iconv/mb_convert_encoding to detect invalid UTF-8, but all techniques designed to validate UTF-8 seem to accept FFFE and FFFF. This PR explicitly converts those characters to FFFD (Unicode substitution character) before validating the rest of the string. It also substitutes one or more FFFD when it detects invalid UTF-8 character sequences. A comment in the code being change stated that it doesn't handle surrogates. It is right not to do so. The only case where we should see surrogates is reading UTF-16. Additional tests are added to an existing test reading a UTF-16 Csv to demonstrate that surrogates are handled correctly, and that FFFE/FFFF are handled reasonably. --- phpstan-baseline.neon | 30 ------------ src/PhpSpreadsheet/Shared/StringHelper.php | 44 ++++++++++++------ .../Reader/Csv/CsvEncodingTest.php | 19 ++++++++ .../Shared/StringHelperInvalidCharTest.php | 44 ++++++++++++++++++ .../Shared/StringHelperTest.php | 29 ++++++++++++ tests/data/Reader/CSV/premiere.utf16le.csv | Bin 112 -> 128 bytes 6 files changed, 121 insertions(+), 45 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 14160edf..04bdf273 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2785,36 +2785,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLERead.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:formatNumber\\(\\) should return string but returns array\\|string\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:sanitizeUTF8\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Parameter \\#1 \\$string of function strlen expects string, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:\\$decimalSeparator \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:\\$thousandsSeparator \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - message: "#^Static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:validateTimeZone\\(\\) is unused\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 2ccb2424..c16de9ce 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -3,13 +3,14 @@ namespace PhpOffice\PhpSpreadsheet\Shared; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use UConverter; class StringHelper { /** Constants */ /** Regular Expressions */ // Fraction - const STRING_REGEXP_FRACTION = '(-?)(\d+)\s+(\d+\/\d+)'; + const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~'; /** * Control characters array. @@ -28,14 +29,14 @@ class StringHelper /** * Decimal separator. * - * @var string + * @var ?string */ private static $decimalSeparator; /** * Thousands separator. * - * @var string + * @var ?string */ private static $thousandsSeparator; @@ -328,19 +329,31 @@ class StringHelper } /** - * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. + * Try to sanitize UTF8, replacing invalid sequences with Unicode substitution characters. */ public static function sanitizeUTF8(string $textValue): string { + $textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue); + if (class_exists(UConverter::class)) { + $returnValue = UConverter::transcode($textValue, 'UTF-8', 'UTF-8'); + if ($returnValue !== false) { + return $returnValue; + } + } + // @codeCoverageIgnoreStart + // I don't think any of the code below should ever be executed. if (self::getIsIconvEnabled()) { - $textValue = @iconv('UTF-8', 'UTF-8', $textValue); - - return $textValue; + $returnValue = @iconv('UTF-8', 'UTF-8', $textValue); + if ($returnValue !== false) { + return $returnValue; + } } - $textValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); + // Phpstan does not think this can return false. + $returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); - return $textValue; + return $returnValue; + // @codeCoverageIgnoreEnd } /** @@ -348,19 +361,19 @@ class StringHelper */ public static function isUTF8(string $textValue): bool { - return $textValue === '' || preg_match('/^./su', $textValue) === 1; + return $textValue === self::sanitizeUTF8($textValue); } /** * Formats a numeric value as a string for output in various output writers forcing * point as decimal separator in case locale is other than English. * - * @param mixed $numericValue + * @param float|int|string $numericValue */ public static function formatNumber($numericValue): string { if (is_float($numericValue)) { - return str_replace(',', '.', $numericValue); + return str_replace(',', '.', (string) $numericValue); } return (string) $numericValue; @@ -537,9 +550,10 @@ class StringHelper */ public static function convertToNumberIfFraction(string &$operand): bool { - if (preg_match('/^' . self::STRING_REGEXP_FRACTION . '$/i', $operand, $match)) { + if (preg_match(self::STRING_REGEXP_FRACTION, $operand, $match)) { $sign = ($match[1] == '-') ? '-' : '+'; - $fractionFormula = '=' . $sign . $match[2] . $sign . $match[3]; + $wholePart = ($match[3] === '') ? '' : ($sign . $match[3]); + $fractionFormula = '=' . $wholePart . $sign . $match[4]; $operand = Calculation::getInstance()->_calculateFormulaValue($fractionFormula); return true; @@ -686,6 +700,6 @@ class StringHelper } $v = (float) $textValue; - return (is_numeric(substr($textValue, 0, strlen($v)))) ? $v : $textValue; + return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue; } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php index 448d3d1e..0cd79853 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php @@ -66,6 +66,25 @@ class CsvEncodingTest extends TestCase self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); } + public function testSurrogate(): void + { + // Surrogates should occur only in UTF-16, and should + // be properly converted to UTF8 when read. + // FFFE/FFFF are illegal, and should be converted to + // substitution character when read. + // Excel does not handle any of the cells in row 3 well. + // LibreOffice handles A3 fine, and discards B3/C3, + // which is a reasonable action. + $filename = 'tests/data/Reader/CSV/premiere.utf16le.csv'; + $reader = new Csv(); + $reader->setInputEncoding(Csv::guessEncoding($filename)); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('𐐀', $sheet->getCell('A3')->getValue()); + self::assertEquals('�', $sheet->getCell('B3')->getValue()); + self::assertEquals('�', $sheet->getCell('C3')->getValue()); + } + /** * @dataProvider providerGuessEncoding */ diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php new file mode 100644 index 00000000..54679499 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php @@ -0,0 +1,44 @@ +getActiveSheet(); + $substitution = '�'; + $array = [ + ['Normal string', 'Hello', 'Hello'], + ['integer', 2, 2], + ['float', 2.1, 2.1], + ['boolean true', true, true], + ['illegal FFFE/FFFF', "H\xef\xbf\xbe\xef\xbf\xbfello", "H{$substitution}{$substitution}ello"], + ['illegal character', "H\xef\x00\x00ello", "H{$substitution}\x00\x00ello"], + ['overlong character', "H\xc0\xa0ello", "H{$substitution}{$substitution}ello"], + ['Osmanya as single character', "H\xf0\x90\x90\x80ello", 'H𐐀ello'], + ['Osmanya as surrogate pair (x)', "\xed\xa0\x81\xed\xb0\x80", "{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}"], + ['Osmanya as surrogate pair (u)', "\u{d801}\u{dc00}", "{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}"], + ['Half surrogate pair (u)', "\u{d801}", "{$substitution}{$substitution}{$substitution}"], + ['Control character', "\u{7}", "\u{7}"], + ]; + + $sheet->fromArray($array); + $row = 0; + foreach ($array as $value) { + self::assertSame($value[1] === $value[2], StringHelper::isUTF8((string) $value[1])); + ++$row; + $expected = $value[2]; + self::assertSame( + $expected, + $sheet->getCell("B$row")->getValue(), + $sheet->getCell("A$row")->getValue() + ); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php index bcbcd5d2..85a92613 100644 --- a/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php @@ -119,4 +119,33 @@ class StringHelperTest extends TestCase self::assertEquals($expectedResult, $result); } + + /** + * @dataProvider providerFractions + */ + public function testFraction(string $expected, string $value): void + { + $originalValue = $value; + $result = StringHelper::convertToNumberIfFraction($value); + if ($result === false) { + self::assertSame($expected, $originalValue); + self::assertSame($expected, $value); + } else { + self::assertSame($expected, (string) $value); + self::assertNotEquals($value, $originalValue); + } + } + + public function providerFractions(): array + { + return [ + 'non-fraction' => ['1', '1'], + 'common fraction' => ['1.5', '1 1/2'], + 'fraction between -1 and 0' => ['-0.5', '-1/2'], + 'fraction between -1 and 0 with space' => ['-0.5', ' - 1/2'], + 'fraction between 0 and 1' => ['0.75', '3/4 '], + 'fraction between 0 and 1 with space' => ['0.75', ' 3/4'], + 'improper fraction' => ['1.75', '7/4'], + ]; + } } diff --git a/tests/data/Reader/CSV/premiere.utf16le.csv b/tests/data/Reader/CSV/premiere.utf16le.csv index a5bb1ff12e771e8628bf3c52930311bffbb3ca94..4ca3c3e4d36cc53c72741b69e4b31a94dfd18b91 100644 GIT binary patch delta 22 ecmXSDV4P4Oz<7h Date: Sun, 3 Jul 2022 18:32:42 +0200 Subject: [PATCH 039/156] Update to ReadMe about PDF and Chart Export installation and configuration --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 40b025e7..57560702 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,38 @@ to ensure that the correct dependencies are retrieved to match your deployment e See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. +### Additional Installation Options + +If you want to write to PDF, or to include Charts when you write to HTML or PDF, then you will need to install additional libraries: + +#### PDF + +For PDF Generation, you can install any of the following, and then configure PhpSpreadsheet to indicate which library you are going to use: + - mpdf/mpdf + - dompdf/dompdf + - tecnickcom/tcpdf + +and configure PhpSpreadsheet using: + +```php +// Dompdf, Mpdf or Tcpdf (as appropriate) +$className = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class; +IOFactory::registerWriter('Pdf', $className); +``` +or the appropriate PDF Writer wrapper for the library that you have chosen to install. + +#### Chart Export + +For Chart export, we support, which you will also need to install yourself + - jpgraph/jpgraph + +and then configure PhpSpreadsheet using: +```php +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +``` + +You can `composer/require` the github version of jpgraph, but this was abandoned at version 4.0; or manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/) + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). From faf6d819c65a0d1188a4d4d17997941759cd2219 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 4 Jul 2022 08:30:46 -0700 Subject: [PATCH 040/156] Keep Calculated String Results Below 32K (#2921) * Keep Calculated String Results Below 32K This is the result of an investigation into issue #2884 (see also PR #2913). It is, unfortunately, not a fix for the original problem; see the discussion in that PR for why I don't think there is a practical fix for that specific problem at this time. Excel limits strings to 32,767 characters. We already truncate strings to that length when added to the spreadsheet. However, we have been able to exceed that length as a result of the concatenation operator (Excel truncates); as a result of the CONCATENATE or TEXTJOIN functions (Excel returns #CALC!); or as a result of the REPLACE, REPT, SUBSTITUTE functions (Excel returns #VALUE!). This PR changes PhpSpreadsheet to return the same value as Excel in these cases. Note that Excel2003 truncates in all those cases; I don't think there is a way to differentiate that behavior in PhpSpreadsheet. However, LibreOffice and Gnumeric do not have that limit; if they have a limit at all, it is much higher. It would be fairly easy to use existing settings to differentiate between Excel and LibreOffice/Gnumeric in this respect. I have not done so in this PR because I am not sure how useful that is, and I can easily see it leading to problems (read in a LibreOffice spreadsheet with a 33K cell and then output to an Excel spreadsheet). Perhaps it should be handled with an additional opt-in setting. I changed the maximum size from a literal to a constant in the one place where it was already being enforced (Cell/DataType). I am not sure that is the best place for it to be defined; I am open to suggestions. * Implement Some Suggestions ... from @MarkBaker. --- .../Calculation/Calculation.php | 9 ++++ .../Calculation/Information/ErrorValue.php | 2 +- .../Calculation/Information/ExcelError.php | 29 ++++++------ .../Calculation/TextData/Concatenate.php | 39 ++++++++++++++-- .../Calculation/TextData/Helpers.php | 6 ++- .../Calculation/TextData/Replace.php | 45 ++++++++++++------- src/PhpSpreadsheet/Cell/DataType.php | 5 ++- src/PhpSpreadsheet/Shared/StringHelper.php | 4 +- .../Calculation/StringLengthTest.php | 34 ++++++++++++++ .../data/Calculation/TextData/CONCATENATE.php | 15 +++++++ tests/data/Calculation/TextData/REPLACE.php | 16 +++++++ tests/data/Calculation/TextData/REPT.php | 4 ++ .../data/Calculation/TextData/SUBSTITUTE.php | 28 ++++++++++++ tests/data/Calculation/TextData/TEXTJOIN.php | 22 +++++++++ 14 files changed, 219 insertions(+), 39 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 4e3a7c53..5b1c5520 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -11,6 +11,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\Value; use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Shared; @@ -4711,11 +4712,19 @@ class Calculation // Perform the required operation against the operand 1 matrix, passing in operand 2 $matrixResult = $matrix->concat($operand2); $result = $matrixResult->getArray(); + if (isset($result[0][0])) { + $result[0][0] = Shared\StringHelper::substring($result[0][0], 0, DataType::MAX_STRING_LENGTH); + } } catch (\Exception $ex) { $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); $result = '#VALUE!'; } } else { + // In theory, we should truncate here. + // But I can't figure out a formula + // using the concatenation operator + // with literals that fits in 32K, + // so I don't think we can overflow here. $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; } $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index 869350ed..dda2c705 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes, true) || $value === ExcelError::CALC(); + return in_array($value, ExcelError::$errorCodes, true); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 6305e502..5ca74a3e 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -14,15 +14,20 @@ class ExcelError * @var array */ public static $errorCodes = [ - 'null' => '#NULL!', - 'divisionbyzero' => '#DIV/0!', - 'value' => '#VALUE!', - 'reference' => '#REF!', - 'name' => '#NAME?', - 'num' => '#NUM!', - 'na' => '#N/A', - 'gettingdata' => '#GETTING_DATA', - 'spill' => '#SPILL!', + 'null' => '#NULL!', // 1 + 'divisionbyzero' => '#DIV/0!', // 2 + 'value' => '#VALUE!', // 3 + 'reference' => '#REF!', // 4 + 'name' => '#NAME?', // 5 + 'num' => '#NUM!', // 6 + 'na' => '#N/A', // 7 + 'gettingdata' => '#GETTING_DATA', // 8 + 'spill' => '#SPILL!', // 9 + 'connect' => '#CONNECT!', //10 + 'blocked' => '#BLOCKED!', //11 + 'unknown' => '#UNKNOWN!', //12 + 'field' => '#FIELD!', //13 + 'calculation' => '#CALC!', //14 ]; /** @@ -54,10 +59,6 @@ class ExcelError ++$i; } - if ($value === self::CALC()) { - return 14; - } - return self::NA(); } @@ -154,6 +155,6 @@ class ExcelError */ public static function CALC(): string { - return '#CALC!'; + return self::$errorCodes['calculation']; } } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php index 4413b4a4..7bd60e90 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Concatenate { @@ -23,7 +26,18 @@ class Concatenate $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value)) { + $returnValue = $value; + + break; + } $returnValue .= Helpers::extractString($arg); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + + break; + } } return $returnValue; @@ -56,7 +70,14 @@ class Concatenate // Loop through arguments $aArgs = Functions::flattenArray($args); + $returnValue = ''; foreach ($aArgs as $key => &$arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value)) { + $returnValue = $value; + + break; + } if ($ignoreEmpty === true && is_string($arg) && trim($arg) === '') { unset($aArgs[$key]); } elseif (is_bool($arg)) { @@ -64,7 +85,12 @@ class Concatenate } } - return implode($delimiter, $aArgs); + $returnValue = ($returnValue !== '') ? $returnValue : implode($delimiter, $aArgs); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + } + + return $returnValue; } /** @@ -90,9 +116,16 @@ class Concatenate $stringValue = Helpers::extractString($stringValue); if (!is_numeric($repeatCount) || $repeatCount < 0) { - return ExcelError::VALUE(); + $returnValue = ExcelError::VALUE(); + } elseif (ErrorValue::isError($stringValue)) { + $returnValue = $stringValue; + } else { + $returnValue = str_repeat($stringValue, (int) $repeatCount); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); // note VALUE not CALC + } } - return str_repeat($stringValue, (int) $repeatCount); + return $returnValue; } } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php index 0fdf6af8..e7b67a34 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Helpers @@ -21,11 +22,14 @@ class Helpers /** * @param mixed $value String value from which to extract characters */ - public static function extractString($value): string + public static function extractString($value, bool $throwIfError = false): string { if (is_bool($value)) { return self::convertBooleanValue($value); } + if ($throwIfError && is_string($value) && ErrorValue::isError($value)) { + throw new CalcExp($value); + } return (string) $value; } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/src/PhpSpreadsheet/Calculation/TextData/Replace.php index a07ea104..03b66321 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Replace.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Replace { @@ -36,16 +38,20 @@ class Replace try { $start = Helpers::extractInt($start, 1, 0, true); $chars = Helpers::extractInt($chars, 0, 0, true); - $oldText = Helpers::extractString($oldText); - $newText = Helpers::extractString($newText); - $left = mb_substr($oldText, 0, $start - 1, 'UTF-8'); + $oldText = Helpers::extractString($oldText, true); + $newText = Helpers::extractString($newText, true); + $left = StringHelper::substring($oldText, 0, $start - 1); - $right = mb_substr($oldText, $start + $chars - 1, null, 'UTF-8'); + $right = StringHelper::substring($oldText, $start + $chars - 1, null); } catch (CalcExp $e) { return $e->getMessage(); } + $returnValue = $left . $newText . $right; + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } - return $left . $newText . $right; + return $returnValue; } /** @@ -71,24 +77,29 @@ class Replace } try { - $text = Helpers::extractString($text); - $fromText = Helpers::extractString($fromText); - $toText = Helpers::extractString($toText); + $text = Helpers::extractString($text, true); + $fromText = Helpers::extractString($fromText, true); + $toText = Helpers::extractString($toText, true); if ($instance === null) { - return str_replace($fromText, $toText, $text); - } - if (is_bool($instance)) { - if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { - return ExcelError::Value(); + $returnValue = str_replace($fromText, $toText, $text); + } else { + if (is_bool($instance)) { + if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { + return ExcelError::Value(); + } + $instance = 1; } - $instance = 1; + $instance = Helpers::extractInt($instance, 1, 0, true); + $returnValue = self::executeSubstitution($text, $fromText, $toText, $instance); } - $instance = Helpers::extractInt($instance, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); } + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } - return self::executeSubstitution($text, $fromText, $toText, $instance); + return $returnValue; } /** @@ -106,7 +117,7 @@ class Replace } if ($pos !== false) { - return Functions::scalar(self::REPLACE($text, ++$pos, mb_strlen($fromText, 'UTF-8'), $toText)); + return Functions::scalar(self::REPLACE($text, ++$pos, StringHelper::countCharacters($fromText), $toText)); } return $text; diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 16de2a00..f19984db 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -31,8 +31,11 @@ class DataType '#NAME?' => 4, '#NUM!' => 5, '#N/A' => 6, + '#CALC!' => 7, ]; + public const MAX_STRING_LENGTH = 32767; + /** * Get list of error codes. * @@ -58,7 +61,7 @@ class DataType } // string must never be longer than 32,767 characters, truncate if necessary - $textValue = StringHelper::substring((string) $textValue, 0, 32767); + $textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH); // we require that newline is represented as "\n" in core, not as "\r\n" or "\r" $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index c16de9ce..030df66d 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -467,9 +467,9 @@ class StringHelper * * @param string $textValue UTF-8 encoded string * @param int $offset Start offset - * @param int $length Maximum number of characters in substring + * @param ?int $length Maximum number of characters in substring */ - public static function substring(string $textValue, int $offset, int $length = 0): string + public static function substring(string $textValue, int $offset, ?int $length = 0): string { return mb_substr($textValue, $offset, $length, 'UTF-8'); } diff --git a/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php b/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php new file mode 100644 index 00000000..b1152f3f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php @@ -0,0 +1,34 @@ +getActiveSheet(); + // Note use Armenian character below to make sure chars, not bytes + $longstring = str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5); + $sheet->getCell('C1')->setValue($longstring); + self::assertSame($longstring, $sheet->getCell('C1')->getValue()); + $sheet->getCell('C2')->setValue($longstring . 'abcdef'); + self::assertSame($longstring . 'abcde', $sheet->getCell('C2')->getValue()); + $sheet->getCell('C3')->setValue('abcdef'); + $sheet->getCell('C4')->setValue('=C1 & C3'); + self::assertSame($longstring . 'abcde', $sheet->getCell('C4')->getCalculatedValue(), 'truncate cell concat with cell'); + $sheet->getCell('C5')->setValue('=C1 & "A"'); + self::assertSame($longstring . 'A', $sheet->getCell('C5')->getCalculatedValue(), 'okay cell concat with literal'); + $sheet->getCell('C6')->setValue('=C1 & "ABCDEF"'); + self::assertSame($longstring . 'ABCDE', $sheet->getCell('C6')->getCalculatedValue(), 'truncate cell concat with literal'); + $sheet->getCell('C7')->setValue('="ABCDEF" & C1'); + self::assertSame('ABCDEF' . str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 6), $sheet->getCell('C7')->getCalculatedValue(), 'truncate literal concat with cell'); + $sheet->getCell('C8')->setValue('="ABCDE" & C1'); + self::assertSame('ABCDE' . $longstring, $sheet->getCell('C8')->getCalculatedValue(), 'okay literal concat with cell'); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Calculation/TextData/CONCATENATE.php b/tests/data/Calculation/TextData/CONCATENATE.php index c6b583eb..b29843f7 100644 --- a/tests/data/Calculation/TextData/CONCATENATE.php +++ b/tests/data/Calculation/TextData/CONCATENATE.php @@ -1,5 +1,7 @@ ['exception'], + 'result just fits' => [ + // Note use Armenian character below to make sure chars, not bytes + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5) . 'ABCDE', + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'ABCDE', + ], + 'result too long' => [ + '#CALC!', + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'abc', + '=A2', + ], + 'propagate DIV0' => ['#DIV/0!', '1', '=2/0', '3'], ]; diff --git a/tests/data/Calculation/TextData/REPLACE.php b/tests/data/Calculation/TextData/REPLACE.php index 8f02e2e3..2268bf48 100644 --- a/tests/data/Calculation/TextData/REPLACE.php +++ b/tests/data/Calculation/TextData/REPLACE.php @@ -65,4 +65,20 @@ return [ 'negative length' => ['#VALUE!', 'hello', 3, -1, 'xyz'], 'boolean 1st parm' => ['TRDFGE', true, 3, 1, 'DFG'], 'boolean 4th parm' => ['heFALSElo', 'hello', 3, 1, false], + 'propagate REF' => ['#REF!', '=sheet99!A1', 3, 1, 'x'], + 'propagate DIV0' => ['#DIV/0!', '=1/0', 3, 1, 'x'], + 'string which just sneaks in' => [ + str_repeat('A', 32766) . 'C', + str_repeat('A', 32766) . 'B', + 32767, + '1', + 'C', + ], + 'string which overflows' => [ + '#VALUE!', + str_repeat('A', 32766) . 'B', + 32767, + '1', + 'CC', + ], ]; diff --git a/tests/data/Calculation/TextData/REPT.php b/tests/data/Calculation/TextData/REPT.php index f8aef72c..172d8fae 100644 --- a/tests/data/Calculation/TextData/REPT.php +++ b/tests/data/Calculation/TextData/REPT.php @@ -11,4 +11,8 @@ return [ ['111', 1, 3], ['δύο δύο ', 'δύο ', 2], ['#VALUE!', 'ABC', -1], + 'result too long' => ['#VALUE!', 'A', 32768], + 'result just fits' => [str_repeat('A', 32767), 'A', 32767], + 'propagate NUM' => ['#NUM!', '=SQRT(-1)', 5], + 'propagate REF' => ['#REF!', '=sheet99!A1', 5], ]; diff --git a/tests/data/Calculation/TextData/SUBSTITUTE.php b/tests/data/Calculation/TextData/SUBSTITUTE.php index 9b695a71..baf1c87b 100644 --- a/tests/data/Calculation/TextData/SUBSTITUTE.php +++ b/tests/data/Calculation/TextData/SUBSTITUTE.php @@ -85,4 +85,32 @@ return [ 'bool false instance' => ['#VALUE!', 'abcdefg', 'def', '123', false], 'bool true instance' => ['#VALUE!', 'abcdefg', 'def', '123', true], 'bool text' => ['FA-SE', false, 'L', '-'], + 'propagate REF' => ['#REF!', '=sheet99!A1', 'A', 'x'], + 'propagate DIV0' => ['#DIV/0!', 'hello', '=1/0', 1, 'x'], + 'string which just sneaks in' => [ + str_repeat('A', 32766) . 'C', + str_repeat('A', 32766) . 'B', + 'B', + 'C', + ], + 'string which overflows' => [ + '#VALUE!', + str_repeat('A', 32766) . 'B', + 'B', + 'CC', + ], + 'okay long string instance' => [ + 'AAAAB' . str_repeat('A', 32762), + str_repeat('A', 32767), + 'A', + 'B', + 5, + ], + 'overflow long string instance' => [ + '#VALUE!', + str_repeat('A', 32767), + 'A', + 'BB', + 5, + ], ]; diff --git a/tests/data/Calculation/TextData/TEXTJOIN.php b/tests/data/Calculation/TextData/TEXTJOIN.php index e345f7c1..565358a7 100644 --- a/tests/data/Calculation/TextData/TEXTJOIN.php +++ b/tests/data/Calculation/TextData/TEXTJOIN.php @@ -1,5 +1,7 @@ ['exception', ['-', true]], 'three arguments' => ['a', ['-', true, 'a']], 'boolean as string' => ['TRUE-FALSE-TRUE', ['-', true, true, false, true]], + 'result too long' => [ + '#CALC!', + [ + ',', + true, + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'abcde', + ], + ], + 'result just fits' => [ + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5) . ',abcd', + [ + ',', + true, + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'abcd', + ], + ], + 'propagate REF' => ['#REF!', [',', true, '1', '=sheet99!A1', '3']], + 'propagate NUM' => ['#NUM!', [',', true, '1', '=SQRT(-1)', '3']], ]; From c22c6df5b526c6d794cfd552a31df2bdd4615702 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 4 Jul 2022 08:43:54 -0700 Subject: [PATCH 041/156] Charts Additional Support for Layout and DataSeriesValues (#2922) * Charts Additional Support for Layout and DataSeriesValues The dLbls tag in more or less the Xml equivalent of the Layout class. It is currently read and written only for the Chart as a whole. It can, however, also be applied to DataSeriesValues. Further it has properties which are currently ignored, namely label fill, border, and font colors. All of these omissions are handled by this PR. There are other properties which can be applied to the labels, but, for now, only the 3 colors are added. DataSeriesValues can have effects (like glow). Since DSV now descends from Properties, these are already supported, but support needs to be added to the Reader and Writer to handle them. This PR adds the support. * Add Unit Tests Based on new samples. * Minor Improvements Slight increase to coverage. --- CHANGELOG.md | 2 +- phpstan-baseline.neon | 95 ------ samples/Chart/33_Chart_create_bubble.php | 3 +- samples/templates/32readwriteBarChart4.xlsx | Bin 0 -> 11688 bytes samples/templates/32readwriteLineChart4.xlsx | Bin 0 -> 13474 bytes src/PhpSpreadsheet/Chart/DataSeriesValues.php | 15 + src/PhpSpreadsheet/Chart/Layout.php | 182 +++++------ src/PhpSpreadsheet/Chart/Properties.php | 32 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 42 ++- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 283 +++++++++--------- .../Chart/Charts32DsvGlowTest.php | 58 ++++ .../Chart/Charts32DsvLabelsTest.php | 73 +++++ .../Chart/GridlinesShadowGlowTest.php | 5 + .../PhpSpreadsheetTests/Chart/LayoutTest.php | 34 +++ 14 files changed, 493 insertions(+), 331 deletions(-) create mode 100644 samples/templates/32readwriteBarChart4.xlsx create mode 100644 samples/templates/32readwriteLineChart4.xlsx create mode 100644 tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d55bbe..6e3a4db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) -- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) - Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) ## 1.23.0 - 2022-04-24 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 04bdf273..108836bc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1185,46 +1185,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Chart/PlotArea.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getArrayElementsValue\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getArrayElementsValue\\(\\) has parameter \\$elements with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getArrayElementsValue\\(\\) has parameter \\$properties with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getLineStyleArrowSize\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getLineStyleArrowSize\\(\\) has parameter \\$arrayKaySelector with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getLineStyleArrowSize\\(\\) has parameter \\$arraySelector with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getShadowPresetsMap\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getShadowPresetsMap\\(\\) has parameter \\$presetsOption with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" count: 1 @@ -4020,61 +3980,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx.php - - - message: "#^Parameter \\#1 \\$plotSeriesValues of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeBubbles\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|null, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#1 \\$rawTextData of method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\XMLWriter\\:\\:writeRawData\\(\\) expects array\\\\|string\\|null, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float given\\.$#" - count: 6 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float\\|int given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 41 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#6 \\$yAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeCategoryAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#7 \\$xAxis of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#8 \\$majorGridlines of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Parameter \\#9 \\$minorGridlines of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:writeValueAxis\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\|null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Chart\\:\\:\\$calculateCellValues has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php - - message: "#^Parameter \\#1 \\$string of function substr expects string, int given\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_bubble.php b/samples/Chart/33_Chart_create_bubble.php index 33feea62..5efcda71 100644 --- a/samples/Chart/33_Chart_create_bubble.php +++ b/samples/Chart/33_Chart_create_bubble.php @@ -89,7 +89,8 @@ $series = new DataSeries( $series->setPlotBubbleSizes($dataSeriesBubbles); // Set the series in the plot area -$plotArea = new PlotArea(null, [$series]); +$plotArea = new PlotArea(); +$plotArea->setPlotSeries([$series]); // Set the chart legend $legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); diff --git a/samples/templates/32readwriteBarChart4.xlsx b/samples/templates/32readwriteBarChart4.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c78572957769be806273bd25daa4150f418ae7b5 GIT binary patch literal 11688 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 Ie*E?S09k(oaR2}S literal 0 HcmV?d00001 diff --git a/samples/templates/32readwriteLineChart4.xlsx b/samples/templates/32readwriteLineChart4.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..eb09c784e280fb27da5705b25a21f0deef57b981 GIT binary patch literal 13474 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 + } + + public function setLabelLayout(?Layout $labelLayout): self + { + $this->labelLayout = $labelLayout; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Layout.php b/src/PhpSpreadsheet/Chart/Layout.php index cea96557..03a0ca3e 100644 --- a/src/PhpSpreadsheet/Chart/Layout.php +++ b/src/PhpSpreadsheet/Chart/Layout.php @@ -57,7 +57,7 @@ class Layout * show legend key * Specifies that legend keys should be shown in data labels. * - * @var bool + * @var ?bool */ private $showLegendKey; @@ -65,7 +65,7 @@ class Layout * show value * Specifies that the value should be shown in a data label. * - * @var bool + * @var ?bool */ private $showVal; @@ -73,7 +73,7 @@ class Layout * show category name * Specifies that the category name should be shown in the data label. * - * @var bool + * @var ?bool */ private $showCatName; @@ -81,7 +81,7 @@ class Layout * show data series name * Specifies that the series name should be shown in the data label. * - * @var bool + * @var ?bool */ private $showSerName; @@ -89,14 +89,14 @@ class Layout * show percentage * Specifies that the percentage should be shown in the data label. * - * @var bool + * @var ?bool */ private $showPercent; /** * show bubble size. * - * @var bool + * @var ?bool */ private $showBubbleSize; @@ -104,10 +104,19 @@ class Layout * show leader lines * Specifies that leader lines should be shown for the data label. * - * @var bool + * @var ?bool */ private $showLeaderLines; + /** @var ?ChartColor */ + private $labelFillColor; + + /** @var ?ChartColor */ + private $labelBorderColor; + + /** @var ?ChartColor */ + private $labelFontColor; + /** * Create a new Layout. */ @@ -134,6 +143,30 @@ class Layout if (isset($layout['h'])) { $this->height = (float) $layout['h']; } + $this->initBoolean($layout, 'showLegendKey'); + $this->initBoolean($layout, 'showVal'); + $this->initBoolean($layout, 'showCatName'); + $this->initBoolean($layout, 'showSerName'); + $this->initBoolean($layout, 'showPercent'); + $this->initBoolean($layout, 'showBubbleSize'); + $this->initBoolean($layout, 'showLeaderLines'); + $this->initColor($layout, 'labelFillColor'); + $this->initColor($layout, 'labelBorderColor'); + $this->initColor($layout, 'labelFontColor'); + } + + private function initBoolean(array $layout, string $name): void + { + if (isset($layout[$name])) { + $this->$name = (bool) $layout[$name]; + } + } + + private function initColor(array $layout, string $name): void + { + if (isset($layout[$name]) && $layout[$name] instanceof ChartColor) { + $this->$name = $layout[$name]; + } } /** @@ -304,12 +337,7 @@ class Layout return $this; } - /** - * Get show legend key. - * - * @return bool - */ - public function getShowLegendKey() + public function getShowLegendKey(): ?bool { return $this->showLegendKey; } @@ -317,24 +345,15 @@ class Layout /** * Set show legend key * Specifies that legend keys should be shown in data labels. - * - * @param bool $showLegendKey Show legend key - * - * @return $this */ - public function setShowLegendKey($showLegendKey) + public function setShowLegendKey(?bool $showLegendKey): self { $this->showLegendKey = $showLegendKey; return $this; } - /** - * Get show value. - * - * @return bool - */ - public function getShowVal() + public function getShowVal(): ?bool { return $this->showVal; } @@ -342,24 +361,15 @@ class Layout /** * Set show val * Specifies that the value should be shown in data labels. - * - * @param bool $showDataLabelValues Show val - * - * @return $this */ - public function setShowVal($showDataLabelValues) + public function setShowVal(?bool $showDataLabelValues): self { $this->showVal = $showDataLabelValues; return $this; } - /** - * Get show category name. - * - * @return bool - */ - public function getShowCatName() + public function getShowCatName(): ?bool { return $this->showCatName; } @@ -367,115 +377,111 @@ class Layout /** * Set show cat name * Specifies that the category name should be shown in data labels. - * - * @param bool $showCategoryName Show cat name - * - * @return $this */ - public function setShowCatName($showCategoryName) + public function setShowCatName(?bool $showCategoryName): self { $this->showCatName = $showCategoryName; return $this; } - /** - * Get show data series name. - * - * @return bool - */ - public function getShowSerName() + public function getShowSerName(): ?bool { return $this->showSerName; } /** - * Set show ser name + * Set show data series name. * Specifies that the series name should be shown in data labels. - * - * @param bool $showSeriesName Show series name - * - * @return $this */ - public function setShowSerName($showSeriesName) + public function setShowSerName(?bool $showSeriesName): self { $this->showSerName = $showSeriesName; return $this; } - /** - * Get show percentage. - * - * @return bool - */ - public function getShowPercent() + public function getShowPercent(): ?bool { return $this->showPercent; } /** - * Set show percentage + * Set show percentage. * Specifies that the percentage should be shown in data labels. - * - * @param bool $showPercentage Show percentage - * - * @return $this */ - public function setShowPercent($showPercentage) + public function setShowPercent(?bool $showPercentage): self { $this->showPercent = $showPercentage; return $this; } - /** - * Get show bubble size. - * - * @return bool - */ - public function getShowBubbleSize() + public function getShowBubbleSize(): ?bool { return $this->showBubbleSize; } /** - * Set show bubble size + * Set show bubble size. * Specifies that the bubble size should be shown in data labels. - * - * @param bool $showBubbleSize Show bubble size - * - * @return $this */ - public function setShowBubbleSize($showBubbleSize) + public function setShowBubbleSize(?bool $showBubbleSize): self { $this->showBubbleSize = $showBubbleSize; return $this; } - /** - * Get show leader lines. - * - * @return bool - */ - public function getShowLeaderLines() + public function getShowLeaderLines(): ?bool { return $this->showLeaderLines; } /** - * Set show leader lines + * Set show leader lines. * Specifies that leader lines should be shown in data labels. - * - * @param bool $showLeaderLines Show leader lines - * - * @return $this */ - public function setShowLeaderLines($showLeaderLines) + public function setShowLeaderLines(?bool $showLeaderLines): self { $this->showLeaderLines = $showLeaderLines; return $this; } + + public function getLabelFillColor(): ?ChartColor + { + return $this->labelFillColor; + } + + public function setLabelFillColor(?ChartColor $chartColor): self + { + $this->labelFillColor = $chartColor; + + return $this; + } + + public function getLabelBorderColor(): ?ChartColor + { + return $this->labelBorderColor; + } + + public function setLabelBorderColor(?ChartColor $chartColor): self + { + $this->labelBorderColor = $chartColor; + + return $this; + } + + public function getLabelFontColor(): ?ChartColor + { + return $this->labelFontColor; + } + + public function setLabelFontColor(?ChartColor $chartColor): self + { + $this->labelFontColor = $chartColor; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index fdc3c12b..2ee6572a 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -421,11 +421,19 @@ abstract class Properties ], ]; - protected function getShadowPresetsMap($presetsOption) + protected function getShadowPresetsMap(int $presetsOption): array { return self::PRESETS_OPTIONS[$presetsOption] ?? self::PRESETS_OPTIONS[0]; } + /** + * Get value of array element. + * + * @param mixed $properties + * @param mixed $elements + * + * @return mixed + */ protected function getArrayElementsValue($properties, $elements) { $reference = &$properties; @@ -718,6 +726,16 @@ abstract class Properties return $this->getArrayElementsValue($this->shadowProperties, $elements); } + public function getShadowArray(): array + { + $array = $this->shadowProperties; + if ($this->getShadowColorObject()->isUsable()) { + $array['color'] = $this->getShadowProperty('color'); + } + + return $array; + } + /** @var ChartColor */ protected $lineColor; @@ -748,6 +766,10 @@ abstract class Properties { $this->lineStyleProperties = $otherProperties->lineStyleProperties; $this->lineColor = $otherProperties->lineColor; + $this->glowSize = $otherProperties->glowSize; + $this->glowColor = $otherProperties->glowColor; + $this->softEdges = $otherProperties->softEdges; + $this->shadowProperties = $otherProperties->shadowProperties; } public function getLineColor(): ChartColor @@ -875,6 +897,14 @@ abstract class Properties 9 => ['w' => 'lg', 'len' => 'lg'], ]; + /** + * Get Line Style Arrow Size. + * + * @param int $arraySelector + * @param string $arrayKaySelector + * + * @return string + */ protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) { return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? ''; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index df25dac7..fd156f13 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -389,6 +389,7 @@ class Chart $markerFillColor = null; $markerBorderColor = null; $lineStyle = null; + $labelLayout = null; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -415,6 +416,12 @@ class Chart $lineStyle = new GridLines(); $this->readLineStyle($seriesDetails, $lineStyle); } + if (isset($children->effectLst)) { + if ($lineStyle === null) { + $lineStyle = new GridLines(); + } + $this->readEffects($seriesDetails, $lineStyle); + } if (isset($children->solidFill)) { $fillColor = new ChartColor($this->readColor($children->solidFill)); } @@ -474,6 +481,21 @@ class Chart $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); break; + case 'dLbls': + $labelLayout = new Layout($this->readChartAttributes($seriesDetails)); + + break; + } + } + if ($labelLayout) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLabelLayout($labelLayout); } } if ($noFill) { @@ -947,6 +969,21 @@ class Chart if (isset($chartDetail->dLbls->showLeaderLines)) { $plotAttributes['showLeaderLines'] = self::getAttribute($chartDetail->dLbls->showLeaderLines, 'val', 'string'); } + if (isset($chartDetail->dLbls->spPr)) { + $sppr = $chartDetail->dLbls->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $plotAttributes['labelFillColor'] = new ChartColor($this->readColor($sppr->solidFill)); + } + if (isset($sppr->ln->solidFill)) { + $plotAttributes['labelBorderColor'] = new ChartColor($this->readColor($sppr->ln->solidFill)); + } + } + if (isset($chartDetail->dLbls->txPr)) { + $txpr = $chartDetail->dLbls->txPr->children($this->aNamespace); + if (isset($txpr->p->pPr->defRPr->solidFill)) { + $plotAttributes['labelFontColor'] = new ChartColor($this->readColor($txpr->p->pPr->defRPr->solidFill)); + } + } } return $plotAttributes; @@ -991,10 +1028,7 @@ class Chart } } - /** - * @param null|Axis|GridLines $chartObject may be extended to include other types - */ - private function readEffects(SimpleXMLElement $chartDetail, $chartObject): void + private function readEffects(SimpleXMLElement $chartDetail, ?Properties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index d242e602..876e5f06 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -17,8 +17,6 @@ use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; class Chart extends WriterPart { - protected $calculateCellValues; - /** * @var int */ @@ -33,8 +31,6 @@ class Chart extends WriterPart */ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $calculateCellValues = true) { - $this->calculateCellValues = $calculateCellValues; - // Create XML writer $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { @@ -43,7 +39,7 @@ class Chart extends WriterPart $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); } // Ensure that data series values are up-to-date before we save - if ($this->calculateCellValues) { + if ($calculateCellValues) { $chart->refresh(); } @@ -57,13 +53,13 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); $objWriter->startElement('c:date1904'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->startElement('c:lang'); $objWriter->writeAttribute('val', 'en-GB'); $objWriter->endElement(); $objWriter->startElement('c:roundedCorners'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $this->writeAlternateContent($objWriter); @@ -73,7 +69,7 @@ class Chart extends WriterPart $this->writeTitle($objWriter, $chart->getTitle()); $objWriter->startElement('c:autoTitleDeleted'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->startElement('c:view3D'); @@ -108,7 +104,7 @@ class Chart extends WriterPart $this->writeLegend($objWriter, $chart->getLegend()); $objWriter->startElement('c:plotVisOnly'); - $objWriter->writeAttribute('val', (int) $chart->getPlotVisibleOnly()); + $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly()); $objWriter->endElement(); $objWriter->startElement('c:dispBlanksAs'); @@ -116,7 +112,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:showDLblsOverMax'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -167,7 +163,7 @@ class Chart extends WriterPart $this->writeLayout($objWriter, $title->getLayout()); $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -203,7 +199,7 @@ class Chart extends WriterPart $objWriter->startElement('a:p'); $objWriter->startElement('a:pPr'); - $objWriter->writeAttribute('rtl', 0); + $objWriter->writeAttribute('rtl', '0'); $objWriter->startElement('a:defRPr'); $objWriter->endElement(); @@ -222,7 +218,7 @@ class Chart extends WriterPart /** * Write Chart Plot Area. */ - private function writePlotArea(XMLWriter $objWriter, PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null): void + private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null): void { if ($plotArea === null) { return; @@ -273,16 +269,16 @@ class Chart extends WriterPart if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) { // Line only, Line3D can't be smoothed $objWriter->startElement('c:smooth'); - $objWriter->writeAttribute('val', (int) $plotGroup->getSmoothLine()); + $objWriter->writeAttribute('val', (string) (int) $plotGroup->getSmoothLine()); $objWriter->endElement(); } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) { $objWriter->startElement('c:gapWidth'); - $objWriter->writeAttribute('val', 150); + $objWriter->writeAttribute('val', '150'); $objWriter->endElement(); if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') { $objWriter->startElement('c:overlap'); - $objWriter->writeAttribute('val', 100); + $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); } } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) { @@ -294,7 +290,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:showNegBubbles'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } elseif ($chartType === DataSeries::TYPE_STOCKCHART) { $objWriter->startElement('c:hiLowLines'); @@ -303,7 +299,7 @@ class Chart extends WriterPart $objWriter->startElement('c:upDownBars'); $objWriter->startElement('c:gapWidth'); - $objWriter->writeAttribute('val', 300); + $objWriter->writeAttribute('val', '300'); $objWriter->endElement(); $objWriter->startElement('c:upBars'); @@ -334,12 +330,12 @@ class Chart extends WriterPart } } else { $objWriter->startElement('c:firstSliceAng'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); if ($chartType === DataSeries::TYPE_DONUTCHART) { $objWriter->startElement('c:holeSize'); - $objWriter->writeAttribute('val', 50); + $objWriter->writeAttribute('val', '50'); $objWriter->endElement(); } } @@ -349,12 +345,12 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis(), $majorGridlines, $minorGridlines); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } - $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis(), $majorGridlines, $minorGridlines); if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { $this->writeSerAxis($objWriter, $id2, $id3); } @@ -363,49 +359,75 @@ class Chart extends WriterPart $objWriter->endElement(); } + private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void + { + if ($value !== null) { + $objWriter->startElement("c:$name"); + $objWriter->writeAttribute('val', $value ? '1' : '0'); + $objWriter->endElement(); + } + } + /** * Write Data Labels. */ private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = null): void { + if (!isset($chartLayout)) { + return; + } $objWriter->startElement('c:dLbls'); - $objWriter->startElement('c:showLegendKey'); - $showLegendKey = (empty($chartLayout)) ? 0 : $chartLayout->getShowLegendKey(); - $objWriter->writeAttribute('val', ((empty($showLegendKey)) ? 0 : 1)); - $objWriter->endElement(); + $fillColor = $chartLayout->getLabelFillColor(); + $borderColor = $chartLayout->getLabelBorderColor(); + if ($fillColor && $fillColor->isUsable()) { + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $fillColor); + if ($borderColor && $borderColor->isUsable()) { + $objWriter->startElement('a:ln'); + $this->writeColor($objWriter, $borderColor); + $objWriter->endElement(); // a:ln + } + $objWriter->endElement(); // c:spPr + } + $fontColor = $chartLayout->getLabelFontColor(); + if ($fontColor && $fontColor->isUsable()) { + $objWriter->startElement('c:txPr'); - $objWriter->startElement('c:showVal'); - $showVal = (empty($chartLayout)) ? 0 : $chartLayout->getShowVal(); - $objWriter->writeAttribute('val', ((empty($showVal)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('wrap', 'square'); + $objWriter->writeAttribute('lIns', '38100'); + $objWriter->writeAttribute('tIns', '19050'); + $objWriter->writeAttribute('rIns', '38100'); + $objWriter->writeAttribute('bIns', '19050'); + $objWriter->writeAttribute('anchor', 'ctr'); + $objWriter->startElement('a:spAutoFit'); + $objWriter->endElement(); // a:spAutoFit + $objWriter->endElement(); // a:bodyPr - $objWriter->startElement('c:showCatName'); - $showCatName = (empty($chartLayout)) ? 0 : $chartLayout->getShowCatName(); - $objWriter->writeAttribute('val', ((empty($showCatName)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle - $objWriter->startElement('c:showSerName'); - $showSerName = (empty($chartLayout)) ? 0 : $chartLayout->getShowSerName(); - $objWriter->writeAttribute('val', ((empty($showSerName)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $this->writeColor($objWriter, $fontColor); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p - $objWriter->startElement('c:showPercent'); - $showPercent = (empty($chartLayout)) ? 0 : $chartLayout->getShowPercent(); - $objWriter->writeAttribute('val', ((empty($showPercent)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->endElement(); // c:txPr + } - $objWriter->startElement('c:showBubbleSize'); - $showBubbleSize = (empty($chartLayout)) ? 0 : $chartLayout->getShowBubbleSize(); - $objWriter->writeAttribute('val', ((empty($showBubbleSize)) ? 0 : 1)); - $objWriter->endElement(); + $this->writeDataLabelsBool($objWriter, 'showLegendKey', $chartLayout->getShowLegendKey()); + $this->writeDataLabelsBool($objWriter, 'showVal', $chartLayout->getShowVal()); + $this->writeDataLabelsBool($objWriter, 'showCatName', $chartLayout->getShowCatName()); + $this->writeDataLabelsBool($objWriter, 'showSerName', $chartLayout->getShowSerName()); + $this->writeDataLabelsBool($objWriter, 'showPercent', $chartLayout->getShowPercent()); + $this->writeDataLabelsBool($objWriter, 'showBubbleSize', $chartLayout->getShowBubbleSize()); + $this->writeDataLabelsBool($objWriter, 'showLeaderLines', $chartLayout->getShowLeaderLines()); - $objWriter->startElement('c:showLeaderLines'); - $showLeaderLines = (empty($chartLayout)) ? 1 : $chartLayout->getShowLeaderLines(); - $objWriter->writeAttribute('val', ((empty($showLeaderLines)) ? 0 : 1)); - $objWriter->endElement(); - - $objWriter->endElement(); + $objWriter->endElement(); // c:dLbls } /** @@ -452,7 +474,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -486,7 +508,7 @@ class Chart extends WriterPart $this->writeLayout($objWriter, $layout); $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -517,12 +539,7 @@ class Chart extends WriterPart $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $yAxis->getFillColorObject()); - - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $yAxis); - $this->writeShadow($objWriter, $yAxis); - $this->writeSoftEdge($objWriter, $yAxis); - $objWriter->endElement(); // effectLst + $this->writeEffects($objWriter, $yAxis); $objWriter->endElement(); // spPr if ($yAxis->getAxisOptionsProperty('major_unit') !== null) { @@ -550,7 +567,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:auto'); - $objWriter->writeAttribute('val', 1); + $objWriter->writeAttribute('val', '1'); $objWriter->endElement(); $objWriter->startElement('c:lblAlgn'); @@ -558,12 +575,12 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:lblOffset'); - $objWriter->writeAttribute('val', 100); + $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); if ($isMultiLevelSeries) { $objWriter->startElement('c:noMultiLvlLbl'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } $objWriter->endElement(); @@ -577,7 +594,7 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, GridLines $majorGridlines, GridLines $minorGridlines): void + private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, ?GridLines $majorGridlines, ?GridLines $minorGridlines): void { $objWriter->startElement('c:valAx'); @@ -610,39 +627,27 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); $objWriter->writeAttribute('val', 'l'); $objWriter->endElement(); - $objWriter->startElement('c:majorGridlines'); - $objWriter->startElement('c:spPr'); + if ($majorGridlines !== null) { + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $majorGridlines); + $this->writeEffects($objWriter, $majorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines + } - $this->writeLineStyles($objWriter, $majorGridlines); - - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $majorGridlines); - $this->writeShadow($objWriter, $majorGridlines); - $this->writeSoftEdge($objWriter, $majorGridlines); - - $objWriter->endElement(); //end effectLst - $objWriter->endElement(); //end spPr - $objWriter->endElement(); //end majorGridLines - - if ($minorGridlines->getObjectState()) { + if ($minorGridlines !== null && $minorGridlines->getObjectState()) { $objWriter->startElement('c:minorGridlines'); $objWriter->startElement('c:spPr'); - $this->writeLineStyles($objWriter, $minorGridlines); - - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $minorGridlines); - $this->writeShadow($objWriter, $minorGridlines); - $this->writeSoftEdge($objWriter, $minorGridlines); - $objWriter->endElement(); //end effectLst - + $this->writeEffects($objWriter, $minorGridlines); $objWriter->endElement(); //end spPr $objWriter->endElement(); //end minorGridLines } @@ -676,7 +681,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -706,17 +711,9 @@ class Chart extends WriterPart } $objWriter->startElement('c:spPr'); - $this->writeColor($objWriter, $xAxis->getFillColorObject()); - $this->writeLineStyles($objWriter, $xAxis); - - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $xAxis); - $this->writeShadow($objWriter, $xAxis); - $this->writeSoftEdge($objWriter, $xAxis); - $objWriter->endElement(); //effectList - + $this->writeEffects($objWriter, $xAxis); $objWriter->endElement(); //end spPr if ($id1 !== '0') { @@ -760,7 +757,7 @@ class Chart extends WriterPart if ($isMultiLevelSeries) { if ($groupType !== DataSeries::TYPE_BUBBLECHART) { $objWriter->startElement('c:noMultiLvlLbl'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } } @@ -852,11 +849,11 @@ class Chart extends WriterPart $objWriter->startElement('c:dPt'); $objWriter->startElement('c:idx'); - $objWriter->writeAttribute('val', $val); + $objWriter->writeAttribute('val', "$val"); $objWriter->endElement(); // c:idx $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); // c:bubble3D $objWriter->startElement('c:spPr'); @@ -901,11 +898,11 @@ class Chart extends WriterPart if ($groupType !== DataSeries::TYPE_LINECHART) { if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) { $objWriter->startElement('c:varyColors'); - $objWriter->writeAttribute('val', 1); + $objWriter->writeAttribute('val', '1'); $objWriter->endElement(); } else { $objWriter->startElement('c:varyColors'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } } @@ -916,11 +913,11 @@ class Chart extends WriterPart $objWriter->startElement('c:ser'); $objWriter->startElement('c:idx'); - $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesIdx); + $objWriter->writeAttribute('val', (string) ($this->seriesIndex + $plotSeriesIdx)); $objWriter->endElement(); $objWriter->startElement('c:order'); - $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesRef); + $objWriter->writeAttribute('val', (string) ($this->seriesIndex + $plotSeriesRef)); $objWriter->endElement(); $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); @@ -949,6 +946,9 @@ class Chart extends WriterPart } } } + if ($plotSeriesValues !== false && $plotSeriesValues->getLabelLayout()) { + $this->writeDataLabels($objWriter, $plotSeriesValues->getLabelLayout()); + } // Labels $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); @@ -980,6 +980,7 @@ class Chart extends WriterPart $nofill = $groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && !$plotSeriesValues->getScatterLines()); if ($callLineStyles) { $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill); + $this->writeEffects($objWriter, $plotSeriesValues); } $objWriter->endElement(); // c:spPr } @@ -1018,7 +1019,7 @@ class Chart extends WriterPart if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) { $objWriter->startElement('c:invertIfNegative'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } @@ -1032,7 +1033,7 @@ class Chart extends WriterPart $plotStyle = $plotGroup->getPlotStyle(); if ($plotStyle) { $objWriter->startElement('c:explosion'); - $objWriter->writeAttribute('val', 25); + $objWriter->writeAttribute('val', '25'); $objWriter->endElement(); } } @@ -1089,7 +1090,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); $objWriter->endElement(); } - } else { + } elseif ($plotSeriesValues !== false) { $this->writeBubbles($plotSeriesValues, $objWriter); } } @@ -1115,7 +1116,7 @@ class Chart extends WriterPart $objWriter->startElement('c:strCache'); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesLabel->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesLabel->getPointCount()); $objWriter->endElement(); foreach ($plotSeriesLabel->getDataValues() as $plotLabelKey => $plotLabelValue) { @@ -1154,7 +1155,7 @@ class Chart extends WriterPart $objWriter->startElement('c:multiLvlStrCache'); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); for ($level = 0; $level < $levelCount; ++$level) { @@ -1200,7 +1201,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); $dataValues = $plotSeriesValues->getDataValues(); @@ -1250,7 +1251,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); $dataValues = $plotSeriesValues->getDataValues(); @@ -1260,7 +1261,7 @@ class Chart extends WriterPart $objWriter->startElement('c:pt'); $objWriter->writeAttribute('idx', $plotSeriesKey); $objWriter->startElement('c:v'); - $objWriter->writeRawData(1); + $objWriter->writeRawData('1'); $objWriter->endElement(); $objWriter->endElement(); } @@ -1309,28 +1310,28 @@ class Chart extends WriterPart $x = $layout->getXPosition(); if ($x !== null) { $objWriter->startElement('c:x'); - $objWriter->writeAttribute('val', $x); + $objWriter->writeAttribute('val', "$x"); $objWriter->endElement(); } $y = $layout->getYPosition(); if ($y !== null) { $objWriter->startElement('c:y'); - $objWriter->writeAttribute('val', $y); + $objWriter->writeAttribute('val', "$y"); $objWriter->endElement(); } $w = $layout->getWidth(); if ($w !== null) { $objWriter->startElement('c:w'); - $objWriter->writeAttribute('val', $w); + $objWriter->writeAttribute('val', "$w"); $objWriter->endElement(); } $h = $layout->getHeight(); if ($h !== null) { $objWriter->startElement('c:h'); - $objWriter->writeAttribute('val', $h); + $objWriter->writeAttribute('val', "$h"); $objWriter->endElement(); } @@ -1377,12 +1378,12 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:pageMargins'); - $objWriter->writeAttribute('footer', 0.3); - $objWriter->writeAttribute('header', 0.3); - $objWriter->writeAttribute('r', 0.7); - $objWriter->writeAttribute('l', 0.7); - $objWriter->writeAttribute('t', 0.75); - $objWriter->writeAttribute('b', 0.75); + $objWriter->writeAttribute('footer', '0.3'); + $objWriter->writeAttribute('header', '0.3'); + $objWriter->writeAttribute('r', '0.7'); + $objWriter->writeAttribute('l', '0.7'); + $objWriter->writeAttribute('t', '0.75'); + $objWriter->writeAttribute('b', '0.75'); $objWriter->endElement(); $objWriter->startElement('c:pageSetup'); @@ -1392,12 +1393,22 @@ class Chart extends WriterPart $objWriter->endElement(); } - /** - * Write shadow properties. - * - * @param Axis|GridLines $xAxis - */ - private function writeShadow(XMLWriter $objWriter, $xAxis): void + private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void + { + if ( + !empty($yAxis->getSoftEdgesSize()) + || !empty($yAxis->getShadowProperty('effect')) + || !empty($yAxis->getGlowProperty('size')) + ) { + $objWriter->startElement('a:effectLst'); + $this->writeGlow($objWriter, $yAxis); + $this->writeShadow($objWriter, $yAxis); + $this->writeSoftEdge($objWriter, $yAxis); + $objWriter->endElement(); // effectLst + } + } + + private function writeShadow(XMLWriter $objWriter, Properties $xAxis): void { if (empty($xAxis->getShadowProperty('effect'))) { return; @@ -1441,12 +1452,7 @@ class Chart extends WriterPart $objWriter->endElement(); } - /** - * Write glow properties. - * - * @param Axis|GridLines $yAxis - */ - private function writeGlow(XMLWriter $objWriter, $yAxis): void + private function writeGlow(XMLWriter $objWriter, Properties $yAxis): void { $size = $yAxis->getGlowProperty('size'); if (empty($size)) { @@ -1458,12 +1464,7 @@ class Chart extends WriterPart $objWriter->endElement(); // glow } - /** - * Write soft edge properties. - * - * @param Axis|GridLines $yAxis - */ - private function writeSoftEdge(XMLWriter $objWriter, $yAxis): void + private function writeSoftEdge(XMLWriter $objWriter, Properties $yAxis): void { $softEdgeSize = $yAxis->getSoftEdgesSize(); if (empty($softEdgeSize)) { diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php new file mode 100644 index 00000000..da2ad9f6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php @@ -0,0 +1,58 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testLine4(): void + { + $file = self::DIRECTORY . '32readwriteLineChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $plotArea = $chart->getPlotArea(); + $dataSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $dataSeriesArray); + $dataSeries = $dataSeriesArray[0]; + $dataSeriesValuesArray = $dataSeries->getPlotValues(); + self::assertCount(3, $dataSeriesValuesArray); + $dataSeriesValues = $dataSeriesValuesArray[1]; + self::assertEquals(5, $dataSeriesValues->getGlowSize()); + self::assertSame('schemeClr', $dataSeriesValues->getGlowProperty(['color', 'type'])); + self::assertSame('accent2', $dataSeriesValues->getGlowProperty(['color', 'value'])); + self::assertSame(60, $dataSeriesValues->getGlowProperty(['color', 'alpha'])); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php new file mode 100644 index 00000000..74655a60 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php @@ -0,0 +1,73 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testBar4(): void + { + $file = self::DIRECTORY . '32readwriteBarChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $plotArea = $chart->getPlotArea(); + $dataSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $dataSeriesArray); + $dataSeries = $dataSeriesArray[0]; + $dataSeriesValuesArray = $dataSeries->getPlotValues(); + self::assertCount(1, $dataSeriesValuesArray); + $dataSeriesValues = $dataSeriesValuesArray[0]; + $layout = $dataSeriesValues->getLabelLayout(); + self::assertNotNull($layout); + self::assertTrue($layout->getShowVal()); + $fillColor = $layout->getLabelFillColor(); + self::assertNotNull($fillColor); + self::assertSame('schemeClr', $fillColor->getType()); + self::assertSame('accent1', $fillColor->getValue()); + $borderColor = $layout->getLabelBorderColor(); + self::assertNotNull($borderColor); + self::assertSame('srgbClr', $borderColor->getType()); + self::assertSame('FFC000', $borderColor->getValue()); + $fontColor = $layout->getLabelFontColor(); + self::assertNotNull($fontColor); + self::assertSame('srgbClr', $fontColor->getType()); + self::assertSame('FFFF00', $fontColor->getValue()); + self::assertEquals( + [15, 73, 61, 32], + $dataSeriesValues->getDataValues() + ); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php index 04ca7c31..26e8f1c6 100644 --- a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php @@ -129,6 +129,11 @@ class GridlinesShadowGlowTest extends AbstractFunctional foreach ($expectedShadow as $key => $value) { self::assertEquals($value, $minorGridlines->getShadowProperty($key), $key); } + $testShadow2 = $minorGridlines->getShadowArray(); + self::assertNull($testShadow2['presets']); + self::assertEquals(['sx' => null, 'sy' => null, 'kx' => null, 'ky' => null], $testShadow2['size']); + unset($testShadow2['presets'], $testShadow2['size']); + self::assertEquals($expectedShadow, $testShadow2); // Create the chart $chart = new Chart( diff --git a/tests/PhpSpreadsheetTests/Chart/LayoutTest.php b/tests/PhpSpreadsheetTests/Chart/LayoutTest.php index 8e927985..fbc878e1 100644 --- a/tests/PhpSpreadsheetTests/Chart/LayoutTest.php +++ b/tests/PhpSpreadsheetTests/Chart/LayoutTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PHPUnit\Framework\TestCase; @@ -27,4 +28,37 @@ class LayoutTest extends TestCase $result = $testInstance->getLayoutTarget(); self::assertEquals($LayoutTargetValue, $result); } + + public function testConstructorVsMethods(): void + { + $fillColor = new ChartColor('FF0000', 20, 'srgbClr'); + $borderColor = new ChartColor('accent1', 20, 'schemeClr'); + $fontColor = new ChartColor('red', 20, 'prstClr'); + $array = [ + 'xMode' => 'factor', + 'yMode' => 'edge', + 'x' => 1.0, + 'y' => 2.0, + 'w' => 3.0, + 'h' => 4.0, + 'showVal' => true, + 'labelFillColor' => $fillColor, + 'labelBorderColor' => $borderColor, + 'labelFontColor' => $fontColor, + ]; + $layout1 = new Layout($array); + $layout2 = new Layout(); + $layout2 + ->setXMode('factor') + ->setYMode('edge') + ->setXposition(1.0) + ->setYposition(2.0) + ->setWidth(3.0) + ->setHeight(4.0) + ->setShowVal(true) + ->setLabelFillColor($fillColor) + ->setLabelBorderColor($borderColor) + ->setLabelFontColor($fontColor); + self::assertEquals($layout1, $layout2); + } } From ad15232fc7942627dbbedbe0cc74324e0530b166 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 6 Jul 2022 03:13:09 +0200 Subject: [PATCH 042/156] Modify rangeBoundaries(), rangeDimension() and getRangeBoundaries() methods to work with row/column ranges as well as with cell ranges and cells --- src/PhpSpreadsheet/Cell/Coordinate.php | 38 +++++++++---------- tests/data/CellGetRangeBoundaries.php | 52 +++++++++++++++++--------- tests/data/CellRangeBoundaries.php | 52 +++++++++++++++++--------- 3 files changed, 87 insertions(+), 55 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 9aa879c3..50678397 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -183,12 +183,12 @@ abstract class Coordinate /** * Calculate range boundaries. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range coordinates [Start Cell, End Cell] * where Start Cell and End Cell are arrays (Column Number, Row Number) */ - public static function rangeBoundaries($range) + public static function rangeBoundaries(string $range): array { // Ensure $pRange is a valid range if (empty($range)) { @@ -205,6 +205,16 @@ abstract class Coordinate [$rangeA, $rangeB] = explode(':', $range); } + if (is_numeric($rangeA) && is_numeric($rangeB)) { + $rangeA = 'A' . $rangeA; + $rangeB = AddressRange::MAX_COLUMN . $rangeB; + } + + if (ctype_alpha($rangeA) && ctype_alpha($rangeB)) { + $rangeA = $rangeA . '1'; + $rangeB = $rangeB . AddressRange::MAX_ROW; + } + // Calculate range outer borders $rangeStart = self::coordinateFromString($rangeA); $rangeEnd = self::coordinateFromString($rangeB); @@ -219,7 +229,7 @@ abstract class Coordinate /** * Calculate range dimension. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range dimension (width, height) */ @@ -234,29 +244,19 @@ abstract class Coordinate /** * Calculate range boundaries. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range coordinates [Start Cell, End Cell] * where Start Cell and End Cell are arrays [Column ID, Row Number] */ public static function getRangeBoundaries($range) { - // Ensure $pRange is a valid range - if (empty($range)) { - $range = self::DEFAULT_RANGE; - } + [$rangeA, $rangeB] = self::rangeBoundaries($range); - // Uppercase coordinate - $range = strtoupper($range); - - // Extract range - if (strpos($range, ':') === false) { - $rangeA = $rangeB = $range; - } else { - [$rangeA, $rangeB] = explode(':', $range); - } - - return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)]; + return [ + [self::stringFromColumnIndex($rangeA[0]), $rangeA[1]], + [self::stringFromColumnIndex($rangeB[0]), $rangeB[1]], + ]; } /** diff --git a/tests/data/CellGetRangeBoundaries.php b/tests/data/CellGetRangeBoundaries.php index ff434796..7a56b4c0 100644 --- a/tests/data/CellGetRangeBoundaries.php +++ b/tests/data/CellGetRangeBoundaries.php @@ -2,29 +2,45 @@ return [ [ - [ - [ - 'B', - 4, - ], - [ - 'E', - 9, - ], + 'Cell Range' => [ + ['B', 4], + ['E', 9], ], 'B4:E9', ], - [ + 'Single Cell' => [ [ - [ - 'B', - 4, - ], - [ - 'B', - 4, - ], + ['B', 4], + ['B', 4], ], 'B4', ], + 'Column Range' => [ + [ + ['B', 1], + ['C', 1048576], + ], + 'B:C', + ], + 'Single Column Range' => [ + [ + ['B', 1], + ['B', 1048576], + ], + 'B:B', + ], + 'Row Range' => [ + [ + ['A', 2], + ['XFD', 3], + ], + '2:3', + ], + 'Single Row Range' => [ + [ + ['A', 2], + ['XFD', 2], + ], + '2:2', + ], ]; diff --git a/tests/data/CellRangeBoundaries.php b/tests/data/CellRangeBoundaries.php index 9e856ced..882f437c 100644 --- a/tests/data/CellRangeBoundaries.php +++ b/tests/data/CellRangeBoundaries.php @@ -1,30 +1,46 @@ [ [ - [ - 2, - 4, - ], - [ - 5, - 9, - ], + [2, 4], + [5, 9], ], 'B4:E9', ], - [ + 'Single Cell' => [ [ - [ - 2, - 4, - ], - [ - 2, - 4, - ], + [2, 4], + [2, 4], ], 'B4', ], + 'Column Range' => [ + [ + [2, 1], + [3, 1048576], + ], + 'B:C', + ], + 'Single Column Range' => [ + [ + [2, 1], + [2, 1048576], + ], + 'B:B', + ], + 'Row Range' => [ + [ + [1, 2], + [16384, 3], + ], + '2:3', + ], + 'Single Row Range' => [ + [ + [1, 2], + [16384, 2], + ], + '2:2', + ], ]; From 09406a6a3f300b100094d4aa58c25151adddcd15 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 7 Jul 2022 21:48:12 -0700 Subject: [PATCH 043/156] Move Gridlines from Chart to Axis (#2923) * Move Gridlines from Chart to Axis This could, I hope, be my last major change to Chart for a while. When I first noticed this problem, I thought it would be a breaking change. However, although this change establishes some deprecations, I don't think it breaks anything. Major and minor gridlines had only been settable by the Chart constructor. This PR moves them where they belong, to Axis (eexisting Chart constructor code will still work). This allows them to be specified from both X and Y axis. Chart is now entirely covered except for 2 statements, one deprecated and one that I just can't figure out. 99.71% for Charts, 88.96% overall. All references to the Chart directory in Phpstan baseline are eliminated. * Minor Fixes, Unit Tests Line style color type should default to null not prstClr. Chart X-axis and Y-axis should alway be Axis, never null. Add some unit tests. * More Tests, Some Improvements Make it easier to change line styles, adding an alternate method besides a setter function with at least a dozen parameters. --- CHANGELOG.md | 6 +- phpstan-baseline.neon | 85 ------- .../33_Chart_create_bar_labels_lines.php | 209 ++++++++++++++++++ samples/templates/32readwriteLineChart4.xlsx | Bin 13474 -> 13512 bytes src/PhpSpreadsheet/Chart/Axis.php | 38 ++++ src/PhpSpreadsheet/Chart/Chart.php | 158 ++++++------- src/PhpSpreadsheet/Chart/DataSeries.php | 4 +- src/PhpSpreadsheet/Chart/Legend.php | 16 +- src/PhpSpreadsheet/Chart/PlotArea.php | 9 +- src/PhpSpreadsheet/Chart/Properties.php | 42 +++- src/PhpSpreadsheet/Chart/Title.php | 9 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 27 ++- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 35 ++- .../Chart/BarChartCustomColorsTest.php | 1 + .../Chart/ChartMethodTest.php | 142 ++++++++++++ .../Chart/Charts32ColoredAxisLabelTest.php | 2 + .../Chart/Charts32DsvGlowTest.php | 19 ++ .../Chart/Charts32DsvLabelsTest.php | 1 + .../Chart/Charts32ScatterTest.php | 9 + .../Chart/ChartsOpenpyxlTest.php | 1 + .../Chart/DataSeriesValues2Test.php | 25 ++- .../Chart/GridlinesLineStyleTest.php | 201 +++++++++++++++++ .../Chart/GridlinesShadowGlowTest.php | 22 +- .../Chart/LineStylesTest.php | 42 ++++ .../PhpSpreadsheetTests/Chart/PieFillTest.php | 1 + .../Reader/Xlsx/SheetsXlsxChartTest.php | 2 + 26 files changed, 876 insertions(+), 230 deletions(-) create mode 100644 samples/Chart/33_Chart_create_bar_labels_lines.php create mode 100644 tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/LineStylesTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e3a4db1..48225e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,7 +49,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) -- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) [PR #2923](https://github.com/PHPOffice/PhpSpreadsheet/pull/2923) +- Adjust both coordinates for two-cell anchors when rows/columns are added/deleted. [Issue #2908](https://github.com/PHPOffice/PhpSpreadsheet/issues/2908) [PR #2909](https://github.com/PHPOffice/PhpSpreadsheet/pull/2909) +- Keep calculated string results below 32K. [PR #2921](https://github.com/PHPOffice/PhpSpreadsheet/pull/2921) +- Filter out illegal Unicode char values FFFE/FFFF. [Issue #2897](https://github.com/PHPOffice/PhpSpreadsheet/issues/2897) [PR #2910](https://github.com/PHPOffice/PhpSpreadsheet/pull/2910) +- Better handling of REF errors and propagation of all errors in Calculation engine. [PR #2902](https://github.com/PHPOffice/PhpSpreadsheet/pull/2902) - Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) ## 1.23.0 - 2022-04-24 diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 108836bc..5ef5522f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1110,86 +1110,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Cell/Coordinate.php - - - message: "#^Call to an undefined method object\\:\\:render\\(\\)\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$legend \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$majorGridlines \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$minorGridlines \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$plotArea \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$title \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$worksheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$xAxis \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$xAxisLabel \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$yAxis \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$yAxisLabel \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues and null will always evaluate to false\\.$#" - count: 2 - path: src/PhpSpreadsheet/Chart/DataSeries.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Legend.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\:\\:\\$positionXLref has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Legend.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/PlotArea.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Title.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Memory\\:\\:\\$cache has no type specified\\.$#" count: 1 @@ -3630,11 +3550,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Html.php - - - message: "#^Ternary operator condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - message: "#^Negated boolean expression is always false\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_bar_labels_lines.php b/samples/Chart/33_Chart_create_bar_labels_lines.php new file mode 100644 index 00000000..970d632c --- /dev/null +++ b/samples/Chart/33_Chart_create_bar_labels_lines.php @@ -0,0 +1,209 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Custom colors for dataSeries (gray, blue, red, orange) +$colors = [ + 'cccccc', '00abb8', 'b8292f', 'eb8500', +]; + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Custom colors +$dataSeriesValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4, [], null, $colors), +]; +$labelLayout = new Layout(); +$labelLayout + ->setShowVal(true) + ->setLabelFontColor(new ChartColor('FFFF00')) + ->setLabelFillColor(new ChartColor('accent2', null, 'schemeClr')); +$dataSeriesValues1[0]->setLabelLayout($labelLayout); + +// Build the dataseries +$series1 = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues +); + +// Set up a layout object for the Pie chart +$layout1 = new Layout(); +$layout1->setShowVal(true); +$layout1->setShowPercent(true); + +// Set the series in the plot area +$plotArea1 = new PlotArea($layout1, [$series1]); +// Set the chart legend +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title1 = new Title('Test Bar Chart'); + +// Create the chart +$chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Pie charts don't have a Y-Axis +); +$majorGridlinesY = new GridLines(); +$majorGridlinesY->setLineColorProperties('FF0000'); +$minorGridlinesY = new GridLines(); +$minorGridlinesY->setLineStyleProperty('dash', Properties::LINE_STYLE_DASH_ROUND_DOT); +$chart1 + ->getChartAxisY() + ->setMajorGridlines($majorGridlinesY) + ->setMinorGridlines($minorGridlinesY); +$majorGridlinesX = new GridLines(); +$majorGridlinesX->setLineColorProperties('FF00FF'); +$minorGridlinesX = new GridLines(); +$minorGridlinesX->activateObject(); +$chart1 + ->getChartAxisX() + ->setMajorGridlines($majorGridlinesX) + ->setMinorGridlines($minorGridlinesX); + +// Set the position where the chart should appear in the worksheet +$chart1->setTopLeftPosition('A7'); +$chart1->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart1); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels2 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues2 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Custom colors +$dataSeriesValues2 = [ + $dataSeriesValues2Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), +]; +$dataSeriesValues2Element->setFillColor($colors); + +// Build the dataseries +$series2 = new DataSeries( + DataSeries::TYPE_DONUTCHART, // plotType + null, // plotGrouping (Donut charts don't have any grouping) + range(0, count($dataSeriesValues2) - 1), // plotOrder + $dataSeriesLabels2, // plotLabel + $xAxisTickValues2, // plotCategory + $dataSeriesValues2 // plotValues +); + +// Set up a layout object for the Pie chart +$layout2 = new Layout(); +$layout2->setShowVal(true); +$layout2->setShowCatName(true); +$layout2->setLabelFillColor(new ChartColor('FFFF00')); + +// Set the series in the plot area +$plotArea2 = new PlotArea($layout2, [$series2]); + +$title2 = new Title('Test Donut Chart'); + +// Create the chart +$chart2 = new Chart( + 'chart2', // name + $title2, // title + null, // legend + $plotArea2, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Like Pie charts, Donut charts don't have a Y-Axis +); + +// Set the position where the chart should appear in the worksheet +$chart2->setTopLeftPosition('I7'); +$chart2->setBottomRightPosition('P20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart2); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteLineChart4.xlsx b/samples/templates/32readwriteLineChart4.xlsx index eb09c784e280fb27da5705b25a21f0deef57b981..0944fcbb6b56de1017491ff6b26f2f531120d7ab 100644 GIT binary patch delta 3927 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 = new ChartColor(); } + /** + * Chart Major Gridlines as. + * + * @var ?GridLines + */ + private $majorGridlines; + + /** + * Chart Minor Gridlines as. + * + * @var ?GridLines + */ + private $minorGridlines; + /** * Axis Number. * @@ -235,4 +249,28 @@ class Axis extends Properties { return $this->crossBetween; } + + public function getMajorGridlines(): ?GridLines + { + return $this->majorGridlines; + } + + public function getMinorGridlines(): ?GridLines + { + return $this->minorGridlines; + } + + public function setMajorGridlines(?GridLines $gridlines): self + { + $this->majorGridlines = $gridlines; + + return $this; + } + + public function setMinorGridlines(?GridLines $gridlines): self + { + $this->minorGridlines = $gridlines; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index ec6342c5..3f4dd2a7 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -17,42 +17,42 @@ class Chart /** * Worksheet. * - * @var Worksheet + * @var ?Worksheet */ private $worksheet; /** * Chart Title. * - * @var Title + * @var ?Title */ private $title; /** * Chart Legend. * - * @var Legend + * @var ?Legend */ private $legend; /** * X-Axis Label. * - * @var Title + * @var ?Title */ private $xAxisLabel; /** * Y-Axis Label. * - * @var Title + * @var ?Title */ private $yAxisLabel; /** * Chart Plot Area. * - * @var PlotArea + * @var ?PlotArea */ private $plotArea; @@ -84,20 +84,6 @@ class Chart */ private $xAxis; - /** - * Chart Major Gridlines as. - * - * @var GridLines - */ - private $majorGridlines; - - /** - * Chart Minor Gridlines as. - * - * @var GridLines - */ - private $minorGridlines; - /** * Top-Left Cell Position. * @@ -157,6 +143,7 @@ class Chart /** * Create a new Chart. + * majorGridlines and minorGridlines are deprecated, moved to Axis. * * @param mixed $name * @param mixed $plotVisibleOnly @@ -172,10 +159,14 @@ class Chart $this->plotArea = $plotArea; $this->plotVisibleOnly = $plotVisibleOnly; $this->displayBlanksAs = $displayBlanksAs; - $this->xAxis = $xAxis; - $this->yAxis = $yAxis; - $this->majorGridlines = $majorGridlines; - $this->minorGridlines = $minorGridlines; + $this->xAxis = $xAxis ?? new Axis(); + $this->yAxis = $yAxis ?? new Axis(); + if ($majorGridlines !== null) { + $this->yAxis->setMajorGridlines($majorGridlines); + } + if ($minorGridlines !== null) { + $this->yAxis->setMinorGridlines($minorGridlines); + } } /** @@ -190,10 +181,8 @@ class Chart /** * Get Worksheet. - * - * @return Worksheet */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->worksheet; } @@ -210,12 +199,7 @@ class Chart return $this; } - /** - * Get Title. - * - * @return Title - */ - public function getTitle() + public function getTitle(): ?Title { return $this->title; } @@ -232,12 +216,7 @@ class Chart return $this; } - /** - * Get Legend. - * - * @return Legend - */ - public function getLegend() + public function getLegend(): ?Legend { return $this->legend; } @@ -254,12 +233,7 @@ class Chart return $this; } - /** - * Get X-Axis Label. - * - * @return Title - */ - public function getXAxisLabel() + public function getXAxisLabel(): ?Title { return $this->xAxisLabel; } @@ -276,12 +250,7 @@ class Chart return $this; } - /** - * Get Y-Axis Label. - * - * @return Title - */ - public function getYAxisLabel() + public function getYAxisLabel(): ?Title { return $this->yAxisLabel; } @@ -298,16 +267,21 @@ class Chart return $this; } - /** - * Get Plot Area. - * - * @return PlotArea - */ - public function getPlotArea() + public function getPlotArea(): ?PlotArea { return $this->plotArea; } + /** + * Set Plot Area. + */ + public function setPlotArea(PlotArea $plotArea): self + { + $this->plotArea = $plotArea; + + return $this; + } + /** * Get Plot Visible Only. * @@ -356,62 +330,58 @@ class Chart return $this; } - /** - * Get yAxis. - * - * @return Axis - */ - public function getChartAxisY() + public function getChartAxisY(): Axis { - if ($this->yAxis !== null) { - return $this->yAxis; - } - $this->yAxis = new Axis(); - return $this->yAxis; } /** - * Get xAxis. - * - * @return Axis + * Set yAxis. */ - public function getChartAxisX() + public function setChartAxisY(?Axis $axis): self { - if ($this->xAxis !== null) { - return $this->xAxis; - } - $this->xAxis = new Axis(); + $this->yAxis = $axis ?? new Axis(); + return $this; + } + + public function getChartAxisX(): Axis + { return $this->xAxis; } + /** + * Set xAxis. + */ + public function setChartAxisX(?Axis $axis): self + { + $this->xAxis = $axis ?? new Axis(); + + return $this; + } + /** * Get Major Gridlines. * - * @return GridLines + * @Deprecated 1.24.0 Use Axis->getMajorGridlines + * + * @codeCoverageIgnore */ - public function getMajorGridlines() + public function getMajorGridlines(): ?GridLines { - if ($this->majorGridlines !== null) { - return $this->majorGridlines; - } - - return new GridLines(); + return $this->yAxis->getMajorGridLines(); } /** * Get Minor Gridlines. * - * @return GridLines + * @Deprecated 1.24.0 Use Axis->getMinorGridlines + * + * @codeCoverageIgnore */ - public function getMinorGridlines() + public function getMinorGridlines(): ?GridLines { - if ($this->minorGridlines !== null) { - return $this->minorGridlines; - } - - return new GridLines(); + return $this->yAxis->getMinorGridLines(); } /** @@ -668,17 +638,21 @@ class Chart public function refresh(): void { - if ($this->worksheet !== null) { + if ($this->worksheet !== null && $this->plotArea !== null) { $this->plotArea->refresh($this->worksheet); } } /** * Render the chart to given file (or stream). + * Unable to cover code until a usable current version of JpGraph + * is made available through Composer. * * @param string $outputDestination Name of the file render to * * @return bool true on success + * + * @codeCoverageIgnore */ public function render($outputDestination = null) { @@ -696,7 +670,7 @@ class Chart $renderer = new $libraryName($this); - return $renderer->render($outputDestination); + return $renderer->render($outputDestination); // @phpstan-ignore-line } public function getRotX(): ?int diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index d27db33e..548145e7 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -134,12 +134,12 @@ class DataSeries $this->plotOrder = $plotOrder; $keys = array_keys($plotValues); $this->plotValues = $plotValues; - if ((count($plotLabel) == 0) || ($plotLabel[$keys[0]] === null)) { + if (!isset($plotLabel[$keys[0]])) { $plotLabel[$keys[0]] = new DataSeriesValues(); } $this->plotLabel = $plotLabel; - if ((count($plotCategory) == 0) || ($plotCategory[$keys[0]] === null)) { + if (!isset($plotCategory[$keys[0]])) { $plotCategory[$keys[0]] = new DataSeriesValues(); } $this->plotCategory = $plotCategory; diff --git a/src/PhpSpreadsheet/Chart/Legend.php b/src/PhpSpreadsheet/Chart/Legend.php index 2f003cd8..fc16017c 100644 --- a/src/PhpSpreadsheet/Chart/Legend.php +++ b/src/PhpSpreadsheet/Chart/Legend.php @@ -18,7 +18,7 @@ class Legend const POSITION_TOP = 't'; const POSITION_TOPRIGHT = 'tr'; - private static $positionXLref = [ + const POSITION_XLREF = [ self::XL_LEGEND_POSITION_BOTTOM => self::POSITION_BOTTOM, self::XL_LEGEND_POSITION_CORNER => self::POSITION_TOPRIGHT, self::XL_LEGEND_POSITION_CUSTOM => '??', @@ -44,7 +44,7 @@ class Legend /** * Legend Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -80,7 +80,7 @@ class Legend */ public function setPosition($position) { - if (!in_array($position, self::$positionXLref)) { + if (!in_array($position, self::POSITION_XLREF)) { return false; } @@ -92,11 +92,11 @@ class Legend /** * Get legend position as an Excel internal numeric value. * - * @return int + * @return false|int */ public function getPositionXL() { - return array_search($this->position, self::$positionXLref); + return array_search($this->position, self::POSITION_XLREF); } /** @@ -108,11 +108,11 @@ class Legend */ public function setPositionXL($positionXL) { - if (!isset(self::$positionXLref[$positionXL])) { + if (!isset(self::POSITION_XLREF[$positionXL])) { return false; } - $this->position = self::$positionXLref[$positionXL]; + $this->position = self::POSITION_XLREF[$positionXL]; return true; } @@ -140,7 +140,7 @@ class Legend /** * Get Layout. * - * @return Layout + * @return ?Layout */ public function getLayout() { diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index ecb7b6c9..4bd49ece 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -9,7 +9,7 @@ class PlotArea /** * PlotArea Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -31,12 +31,7 @@ class PlotArea $this->plotSeries = $plotSeries; } - /** - * Get Layout. - * - * @return Layout - */ - public function getLayout() + public function getLayout(): ?Layout { return $this->layout; } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 2ee6572a..f737ca08 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -164,7 +164,7 @@ abstract class Properties * * @return $this */ - protected function activateObject() + public function activateObject() { $this->objectState = true; @@ -782,9 +782,9 @@ abstract class Properties * * @param string $value * @param ?int $alpha - * @param string $colorType + * @param ?string $colorType */ - public function setLineColorProperties($value, $alpha = null, $colorType = ChartColor::EXCEL_COLOR_TYPE_STANDARD): void + public function setLineColorProperties($value, $alpha = null, $colorType = null): void { $this->activateObject(); $this->lineColor->setColorPropertiesArray( @@ -873,6 +873,42 @@ abstract class Properties } } + public function getLineStyleArray(): array + { + return $this->lineStyleProperties; + } + + public function setLineStyleArray(array $lineStyleProperties = []): self + { + $this->activateObject(); + $this->lineStyleProperties['width'] = $lineStyleProperties['width'] ?? null; + $this->lineStyleProperties['compound'] = $lineStyleProperties['compound'] ?? ''; + $this->lineStyleProperties['dash'] = $lineStyleProperties['dash'] ?? ''; + $this->lineStyleProperties['cap'] = $lineStyleProperties['cap'] ?? ''; + $this->lineStyleProperties['join'] = $lineStyleProperties['join'] ?? ''; + $this->lineStyleProperties['arrow']['head']['type'] = $lineStyleProperties['arrow']['head']['type'] ?? ''; + $this->lineStyleProperties['arrow']['head']['size'] = $lineStyleProperties['arrow']['head']['size'] ?? ''; + $this->lineStyleProperties['arrow']['head']['w'] = $lineStyleProperties['arrow']['head']['w'] ?? ''; + $this->lineStyleProperties['arrow']['head']['len'] = $lineStyleProperties['arrow']['head']['len'] ?? ''; + $this->lineStyleProperties['arrow']['end']['type'] = $lineStyleProperties['arrow']['end']['type'] ?? ''; + $this->lineStyleProperties['arrow']['end']['size'] = $lineStyleProperties['arrow']['end']['size'] ?? ''; + $this->lineStyleProperties['arrow']['end']['w'] = $lineStyleProperties['arrow']['end']['w'] ?? ''; + $this->lineStyleProperties['arrow']['end']['len'] = $lineStyleProperties['arrow']['end']['len'] ?? ''; + + return $this; + } + + /** + * @param mixed $value + */ + public function setLineStyleProperty(string $propertyName, $value): self + { + $this->activateObject(); + $this->lineStyleProperties[$propertyName] = $value; + + return $this; + } + /** * Get Line Style Property. * diff --git a/src/PhpSpreadsheet/Chart/Title.php b/src/PhpSpreadsheet/Chart/Title.php index 090c4f3f..9b0540d8 100644 --- a/src/PhpSpreadsheet/Chart/Title.php +++ b/src/PhpSpreadsheet/Chart/Title.php @@ -16,7 +16,7 @@ class Title /** * Title Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -78,12 +78,7 @@ class Title return $this; } - /** - * Get Layout. - * - * @return Layout - */ - public function getLayout() + public function getLayout(): ?Layout { return $this->layout; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index fd156f13..cf77a7f2 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -72,7 +72,6 @@ class Chart $rotX = $rotY = $rAngAx = $perspective = null; $xAxis = new Axis(); $yAxis = new Axis(); - $majorGridlines = $minorGridlines = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': @@ -110,6 +109,23 @@ class Chart $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); } } + if (isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); + } + $xAxis->setMajorGridlines($majorGridlines); + } + if (isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); + } + $xAxis->setMinorGridlines($minorGridlines); + } $this->setAxisProperties($chartDetail, $xAxis); break; @@ -168,19 +184,22 @@ class Chart $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); } } - if (isset($chartDetail->majorGridlines)) { + if ($whichAxis !== null && isset($chartDetail->majorGridlines)) { $majorGridlines = new GridLines(); if (isset($chartDetail->majorGridlines->spPr)) { $this->readEffects($chartDetail->majorGridlines, $majorGridlines); $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); } + $whichAxis->setMajorGridlines($majorGridlines); } - if (isset($chartDetail->minorGridlines)) { + if ($whichAxis !== null && isset($chartDetail->minorGridlines)) { $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); if (isset($chartDetail->minorGridlines->spPr)) { $this->readEffects($chartDetail->minorGridlines, $minorGridlines); $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); } + $whichAxis->setMinorGridlines($minorGridlines); } $this->setAxisProperties($chartDetail, $whichAxis); @@ -304,7 +323,7 @@ class Chart } } } - $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis, $majorGridlines, $minorGridlines); + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); if (is_int($rotX)) { $chart->setRotX($rotX); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 876e5f06..356b44e9 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -6,7 +6,6 @@ use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; @@ -99,7 +98,7 @@ class Chart extends WriterPart } $objWriter->endElement(); // view3D - $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY(), $chart->getMajorGridlines(), $chart->getMinorGridlines()); + $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); $this->writeLegend($objWriter, $chart->getLegend()); @@ -218,11 +217,13 @@ class Chart extends WriterPart /** * Write Chart Plot Area. */ - private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null): void + private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null): void { if ($plotArea === null) { return; } + $majorGridlines = ($yAxis === null) ? null : $yAxis->getMajorGridlines(); + $minorGridlines = ($yAxis === null) ? null : $yAxis->getMinorGridlines(); $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; @@ -345,12 +346,12 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis(), $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } else { $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } - $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis(), $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis()); if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { $this->writeSerAxis($objWriter, $id2, $id3); } @@ -448,6 +449,8 @@ class Chart extends WriterPart } else { $objWriter->startElement('c:catAx'); } + $majorGridlines = $yAxis->getMajorGridlines(); + $minorGridlines = $yAxis->getMinorGridlines(); if ($id1 !== '0') { $objWriter->startElement('c:axId'); @@ -481,6 +484,24 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', 'b'); $objWriter->endElement(); + if ($majorGridlines !== null) { + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $majorGridlines); + $this->writeEffects($objWriter, $majorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines + } + + if ($minorGridlines !== null && $minorGridlines->getObjectState()) { + $objWriter->startElement('c:minorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $minorGridlines); + $this->writeEffects($objWriter, $minorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end minorGridLines + } + if ($xAxisLabel !== null) { $objWriter->startElement('c:title'); $objWriter->startElement('c:tx'); @@ -594,9 +615,11 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, ?GridLines $majorGridlines, ?GridLines $minorGridlines): void + private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis): void { $objWriter->startElement('c:valAx'); + $majorGridlines = $xAxis->getMajorGridlines(); + $minorGridlines = $xAxis->getMinorGridlines(); if ($id2 !== '0') { $objWriter->startElement('c:axId'); diff --git a/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php b/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php index 824e9600..72c541fb 100644 --- a/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php +++ b/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php @@ -140,6 +140,7 @@ class BarChartCustomColorsTest extends AbstractFunctional $chart2 = $charts2[0]; self::assertNotNull($chart2); $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); $dataSeries2 = $plotArea2->getPlotGroup(); self::assertCount(1, $dataSeries2); $plotValues = $dataSeries2[0]->getPlotValues(); diff --git a/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php b/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php new file mode 100644 index 00000000..027ddc53 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php @@ -0,0 +1,142 @@ +getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + $title = new Title('Method vs Constructor test'); + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + $xAxis = new Axis(); + $yAxis = new Axis(); + $xAxisLabel = new Title('X-Axis label'); + $yAxisLabel = new Title('Y-Axis label'); + $chart1 = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + $xAxisLabel, // xAxisLabel + $yAxisLabel, // yAxisLabel + $xAxis, // xAxis + $yAxis // yAxis + ); + $chart2 = new Chart('chart1'); + $chart2 + ->setLegend($legend) + ->setPlotArea($plotArea) + ->setPlotVisibleOnly(true) + ->setDisplayBlanksAs(DataSeries::EMPTY_AS_GAP) + ->setChartAxisX($xAxis) + ->setChartAxisY($yAxis) + ->setXAxisLabel($xAxisLabel) + ->setYAxisLabel($yAxisLabel) + ->setTitle($title); + self::assertEquals($chart1, $chart2); + $spreadsheet->disconnectWorksheets(); + } + + public function testPositions(): void + { + $chart = new Chart('chart1'); + $chart->setTopLeftPosition('B3', 2, 4); + self::assertSame('B3', $chart->getTopLeftCell()); + self::assertEquals(['X' => 2, 'Y' => 4], $chart->getTopLeftOffset()); + self::assertEquals(2, $chart->getTopLeftXOffset()); + self::assertEquals(4, $chart->getTopLeftYOffset()); + $chart->setTopLeftCell('B5'); + self::assertSame('B5', $chart->getTopLeftCell()); + self::assertEquals(2, $chart->getTopLeftXOffset()); + self::assertEquals(4, $chart->getTopLeftYOffset()); + $chart->setTopLeftOffset(6, 8); + self::assertSame('B5', $chart->getTopLeftCell()); + self::assertEquals(6, $chart->getTopLeftXOffset()); + self::assertEquals(8, $chart->getTopLeftYOffset()); + + $chart->setbottomRightPosition('H9', 3, 5); + self::assertSame('H9', $chart->getBottomRightCell()); + self::assertEquals(['X' => 3, 'Y' => 5], $chart->getBottomRightOffset()); + self::assertEquals(3, $chart->getBottomRightXOffset()); + self::assertEquals(5, $chart->getBottomRightYOffset()); + $chart->setbottomRightCell('H11'); + self::assertSame('H11', $chart->getBottomRightCell()); + self::assertEquals(3, $chart->getBottomRightXOffset()); + self::assertEquals(5, $chart->getBottomRightYOffset()); + $chart->setbottomRightOffset(7, 9); + self::assertSame('H11', $chart->getBottomRightCell()); + self::assertEquals(7, $chart->getBottomRightXOffset()); + self::assertEquals(9, $chart->getBottomRightYOffset()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php index b8041623..f0c84bf0 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php @@ -46,6 +46,7 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertNotNull($chart); $xAxisLabel = $chart->getXAxisLabel(); + self::assertNotNull($xAxisLabel); $captionArray = $xAxisLabel->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -64,6 +65,7 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertSame('srgbClr', $chartColor->getType()); $yAxisLabel = $chart->getYAxisLabel(); + self::assertNotNull($yAxisLabel); $captionArray = $yAxisLabel->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php index da2ad9f6..26ef34ee 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php @@ -42,6 +42,7 @@ class Charts32DsvGlowTest extends AbstractFunctional self::assertNotNull($chart); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $dataSeriesArray = $plotArea->getPlotGroup(); self::assertCount(1, $dataSeriesArray); $dataSeries = $dataSeriesArray[0]; @@ -53,6 +54,24 @@ class Charts32DsvGlowTest extends AbstractFunctional self::assertSame('accent2', $dataSeriesValues->getGlowProperty(['color', 'value'])); self::assertSame(60, $dataSeriesValues->getGlowProperty(['color', 'alpha'])); + $yAxis = $chart->getChartAxisY(); + $majorGridlines = $yAxis->getMajorGridlines(); + self::assertNotNull($majorGridlines); + self::assertSame('triangle', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertSame('triangle', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $minorGridlines = $yAxis->getMinorGridlines(); + self::assertNotNull($minorGridlines); + self::assertSame('sysDot', $minorGridlines->getLineStyleProperty('dash')); + self::assertSame('FFC000', $minorGridlines->getLineColor()->getValue()); + + $xAxis = $chart->getChartAxisX(); + $majorGridlines = $xAxis->getMajorGridlines(); + $minorGridlines = $xAxis->getMinorGridlines(); + self::assertNotNull($majorGridlines); + self::assertSame('7030A0', $majorGridlines->getLineColor()->getValue()); + self::assertNotNull($minorGridlines); + self::assertFalse($minorGridlines->getLineColor()->isUsable()); + $reloadedSpreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php index 74655a60..b6490776 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php @@ -42,6 +42,7 @@ class Charts32DsvLabelsTest extends AbstractFunctional self::assertNotNull($chart); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $dataSeriesArray = $plotArea->getPlotGroup(); self::assertCount(1, $dataSeriesArray); $dataSeries = $dataSeriesArray[0]; diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index 38884eaf..b655afc4 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -46,6 +46,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -72,6 +73,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -121,6 +123,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -182,6 +185,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -231,6 +235,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -257,6 +262,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -305,6 +311,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -334,6 +341,7 @@ class Charts32ScatterTest extends AbstractFunctional } $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -384,6 +392,7 @@ class Charts32ScatterTest extends AbstractFunctional self::assertNotNull($chart); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php index 7fcc2e90..e6f574fd 100644 --- a/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php @@ -27,6 +27,7 @@ class ChartsOpenpyxlTest extends TestCase self::assertTrue($chart->getOneCellAnchor()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; diff --git a/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php b/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php index a27397f9..bba1c1f1 100644 --- a/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php +++ b/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php @@ -120,8 +120,10 @@ class DataSeriesValues2Test extends AbstractFunctional // Add the chart to the worksheet $worksheet->addChart($chart); - self::assertSame(1, $chart->getPlotArea()->getPlotGroupCount()); - $plotValues = $chart->getPlotArea()->getPlotGroup()[0]->getPlotValues(); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertSame(1, $plotArea->getPlotGroupCount()); + $plotValues = $plotArea->getPlotGroup()[0]->getPlotValues(); self::assertCount(3, $plotValues); self::assertSame([], $plotValues[1]->getDataValues()); self::assertNull($plotValues[1]->getDataValue()); @@ -138,20 +140,27 @@ class DataSeriesValues2Test extends AbstractFunctional self::assertCount(1, $charts2); $chart2 = $charts2[0]; self::assertNotNull($chart2); - $plotValues2 = $chart2->getPlotArea()->getPlotGroup()[0]->getPlotValues(); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $plotGroup2 = $plotArea2->getPlotGroup()[0]; + self::assertNotNull($plotGroup2); + $plotValues2 = $plotGroup2->getPlotValues(); self::assertCount(3, $plotValues2); self::assertSame([15.0, 73.0, 61.0, 32.0], $plotValues2[1]->getDataValues()); self::assertSame([15.0, 73.0, 61.0, 32.0], $plotValues2[1]->getDataValue()); - $labels2 = $chart->getPlotArea()->getPlotGroup()[0]->getPlotLabels(); + $labels2 = $plotGroup2->getPlotLabels(); self::assertCount(3, $labels2); - self::assertSame(2010, $labels2[0]->getDataValue()); - $dataSeries = $chart->getPlotArea()->getPlotGroup()[0]; + self::assertEquals(2010, $labels2[0]->getDataValue()); + $dataSeries = $plotArea2->getPlotGroup()[0]; self::assertFalse($dataSeries->getPlotValuesByIndex(99)); self::assertNotFalse($dataSeries->getPlotValuesByIndex(0)); - self::assertSame([12, 56, 52, 30], $dataSeries->getPlotValuesByIndex(0)->getDataValues()); + self::assertEquals([12, 56, 52, 30], $dataSeries->getPlotValuesByIndex(0)->getDataValues()); self::assertSame(DataSeries::TYPE_AREACHART, $dataSeries->getPlotType()); self::assertSame(DataSeries::GROUPING_PERCENT_STACKED, $dataSeries->getPlotGrouping()); - self::assertTrue($dataSeries->getSmoothLine()); + // SmoothLine written out for DataSeries only for LineChart. + // Original test was wrong - used $chart rather than $chart2 + // to retrieve data which was read in. + //self::assertTrue($dataSeries->getSmoothLine()); $reloadedSpreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php index 997413c5..e13a5840 100644 --- a/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; @@ -138,6 +139,202 @@ class GridlinesLineStyleTest extends AbstractFunctional self::assertSame(30, $minorGridlines->getLineColorProperty('alpha')); self::assertSame('srgbClr', $minorGridlines->getLineColorProperty('type')); + // Create the chart + $yAxis = new Axis(); + $yAxis->setMajorGridlines($majorGridlines); + $yAxis->setMinorGridlines($minorGridlines); + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + null, // xAxis + $yAxis // yAxis + ); + $yAxis2 = $chart->getChartAxisY(); + $majorGridlines2 = $yAxis2->getMajorGridlines(); + self::assertNotNull($majorGridlines2); + self::assertEquals($width, $majorGridlines2->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines2->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines2->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines2->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines2->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines2 = $yAxis2->getMinorGridlines(); + self::assertNotNull($minorGridlines2); + self::assertSame('00FF00', $minorGridlines2->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines2->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines2->getLineColorProperty('type')); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + self::assertSame('A7', $chart2->getTopLeftCell()); + self::assertSame('H20', $chart2->getBottomRightCell()); + self::assertSame($sheet, $chart2->getWorksheet()); + $yAxis3 = $chart2->getChartAxisY(); + $majorGridlines3 = $yAxis3->getMajorGridlines(); + self::assertNotNull($majorGridlines3); + self::assertEquals($width, $majorGridlines3->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines3->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines3->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines3->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines3->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($endArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines3 = $yAxis3->getMinorGridlines(); + self::assertNotNull($minorGridlines3); + self::assertSame('00FF00', $minorGridlines3->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines3->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines3->getLineColorProperty('type')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testLineStylesDeprecated(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $majorGridlines = new GridLines(); + $width = 2; + $compound = Properties::LINE_STYLE_COMPOUND_THICKTHIN; + $dash = Properties::LINE_STYLE_DASH_ROUND_DOT; + $cap = Properties::LINE_STYLE_CAP_ROUND; + $join = Properties::LINE_STYLE_JOIN_MITER; + $headArrowType = Properties::LINE_STYLE_ARROW_TYPE_DIAMOND; + $headArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_2; + $endArrowType = Properties::LINE_STYLE_ARROW_TYPE_OVAL; + $endArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_3; + $majorGridlines->setLineStyleProperties( + $width, + $compound, + $dash, + $cap, + $join, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize + ); + $minorGridlines = new GridLines(); + $minorGridlines->setLineColorProperties('00FF00', 30, 'srgbClr'); + + self::assertEquals($width, $majorGridlines->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleArrowWidth('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowLength('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowParameters('end', 'len')); + + self::assertSame('00FF00', $minorGridlines->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines->getLineColorProperty('type')); + // Create the chart $chart = new Chart( 'chart1', // name @@ -154,6 +351,7 @@ class GridlinesLineStyleTest extends AbstractFunctional $minorGridlines // minorGridlines ); $majorGridlines2 = $chart->getMajorGridlines(); + self::assertNotNull($majorGridlines2); self::assertEquals($width, $majorGridlines2->getLineStyleProperty('width')); self::assertEquals($compound, $majorGridlines2->getLineStyleProperty('compound')); self::assertEquals($dash, $majorGridlines2->getLineStyleProperty('dash')); @@ -169,6 +367,7 @@ class GridlinesLineStyleTest extends AbstractFunctional self::assertEquals('lg', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'len'])); $minorGridlines2 = $chart->getMinorGridlines(); + self::assertNotNull($minorGridlines2); self::assertSame('00FF00', $minorGridlines2->getLineColorProperty('value')); self::assertSame(30, $minorGridlines2->getLineColorProperty('alpha')); self::assertSame('srgbClr', $minorGridlines2->getLineColorProperty('type')); @@ -193,6 +392,7 @@ class GridlinesLineStyleTest extends AbstractFunctional $chart2 = $charts2[0]; self::assertNotNull($chart2); $majorGridlines3 = $chart2->getMajorGridlines(); + self::assertNotNull($majorGridlines3); self::assertEquals($width, $majorGridlines3->getLineStyleProperty('width')); self::assertEquals($compound, $majorGridlines3->getLineStyleProperty('compound')); self::assertEquals($dash, $majorGridlines3->getLineStyleProperty('dash')); @@ -206,6 +406,7 @@ class GridlinesLineStyleTest extends AbstractFunctional self::assertEquals('lg', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'len'])); $minorGridlines3 = $chart2->getMinorGridlines(); + self::assertNotNull($minorGridlines3); self::assertSame('00FF00', $minorGridlines3->getLineColorProperty('value')); self::assertSame(30, $minorGridlines3->getLineColorProperty('alpha')); self::assertSame('srgbClr', $minorGridlines3->getLineColorProperty('type')); diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php index 26e8f1c6..e2c91eba 100644 --- a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; @@ -93,7 +94,9 @@ class GridlinesShadowGlowTest extends AbstractFunctional $title = new Title('Test %age-Stacked Area Chart'); $yAxisLabel = new Title('Value ($k)'); + $yAxis = new Axis(); $majorGridlines = new GridLines(); + $yAxis->setMajorGridlines($majorGridlines); $majorGlowSize = 10.0; $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); $softEdgeSize = 2.5; @@ -110,6 +113,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional self::assertEquals($softEdgeSize, $majorGridlines->getSoftEdgesSize()); $minorGridlines = new GridLines(); + $yAxis->setMinorGridlines($minorGridlines); $expectedShadow = [ 'effect' => 'outerShdw', 'algn' => 'tl', @@ -146,15 +150,16 @@ class GridlinesShadowGlowTest extends AbstractFunctional null, // xAxisLabel $yAxisLabel, // yAxisLabel null, // xAxis - null, // yAxis - $majorGridlines, - $minorGridlines + $yAxis // yAxis ); - $majorGridlines2 = $chart->getMajorGridlines(); + $yAxis2 = $chart->getChartAxisY(); + $majorGridlines2 = $yAxis2->getMajorGridlines(); + self::assertNotNull($majorGridlines2); self::assertEquals($majorGlowSize, $majorGridlines2->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $majorGridlines2->getGlowProperty('color')); self::assertEquals($softEdgeSize, $majorGridlines2->getSoftEdgesSize()); - $minorGridlines2 = $chart->getMinorGridlines(); + $minorGridlines2 = $yAxis2->getMinorGridlines(); + self::assertNotNull($minorGridlines2); foreach ($expectedShadow as $key => $value) { self::assertEquals($value, $minorGridlines2->getShadowProperty($key), $key); } @@ -178,11 +183,14 @@ class GridlinesShadowGlowTest extends AbstractFunctional self::assertCount(1, $charts2); $chart2 = $charts2[0]; self::assertNotNull($chart2); - $majorGridlines3 = $chart2->getMajorGridlines(); + $yAxis3 = $chart2->getChartAxisY(); + $majorGridlines3 = $yAxis3->getMajorGridlines(); + self::assertNotNull($majorGridlines3); self::assertEquals($majorGlowSize, $majorGridlines3->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $majorGridlines3->getGlowProperty('color')); self::assertEquals($softEdgeSize, $majorGridlines3->getSoftEdgesSize()); - $minorGridlines3 = $chart->getMinorGridlines(); + $minorGridlines3 = $yAxis3->getMinorGridlines(); + self::assertNotNull($minorGridlines3); foreach ($expectedShadow as $key => $value) { self::assertEquals($value, $minorGridlines3->getShadowProperty($key), $key); } diff --git a/tests/PhpSpreadsheetTests/Chart/LineStylesTest.php b/tests/PhpSpreadsheetTests/Chart/LineStylesTest.php new file mode 100644 index 00000000..ef940c99 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/LineStylesTest.php @@ -0,0 +1,42 @@ +getLineStyleArray(); + $gridlines1->setLineStyleProperties( + 3, // lineWidth + Properties::LINE_STYLE_COMPOUND_DOUBLE, // compoundType + '', // dashType + Properties::LINE_STYLE_CAP_SQUARE, // capType + '', // jointType + '', // headArrowType + '', // headArrowSize + '', // endArrowType + '', // endArrowSize + 'lg', // headArrowWidth + 'med', // headArrowLength + '', // endArrowWidth + '' // endArrowLength + ); + $gridlines2 = new GridLines(); + $lineStyleProperties = [ + 'width' => 3, + 'compound' => Properties::LINE_STYLE_COMPOUND_DOUBLE, + 'cap' => Properties::LINE_STYLE_CAP_SQUARE, + 'arrow' => ['head' => ['w' => 'lg', 'len' => 'med']], + ]; + $gridlines2->setLineStyleArray($lineStyleProperties); + self::assertSame($gridlines1->getLineStyleArray(), $gridlines2->getLineStyleArray()); + $gridlines2->setLineStyleArray(); // resets line styles + self::assertSame($originalLineStyle, $gridlines2->getLineStyleArray()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/PieFillTest.php b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php index 452fcb93..d659f21c 100644 --- a/tests/PhpSpreadsheetTests/Chart/PieFillTest.php +++ b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php @@ -138,6 +138,7 @@ class PieFillTest extends AbstractFunctional $chart2 = $charts2[0]; self::assertNotNull($chart2); $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); $dataSeries2 = $plotArea2->getPlotGroup(); self::assertCount(1, $dataSeries2); $plotValues = $dataSeries2[0]->getPlotValues(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php index 0cbd103d..d47ce311 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php @@ -23,6 +23,7 @@ class SheetsXlsxChartTest extends TestCase $chart1 = $charts[0]; self::assertNotNull($chart1); $pa1 = $chart1->getPlotArea(); + self::assertNotNull($pa1); self::assertEquals(2, $pa1->getPlotSeriesCount()); $pg1 = $pa1->getPlotGroup()[0]; @@ -35,6 +36,7 @@ class SheetsXlsxChartTest extends TestCase $chart2 = $charts[1]; self::assertNotNull($chart2); $pa1 = $chart2->getPlotArea(); + self::assertNotNull($pa1); self::assertEquals(2, $pa1->getPlotSeriesCount()); $pg1 = $pa1->getPlotGroupByIndex(0); From e050b3ba8a4be661eb741933c8f77b65b0385247 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 8 Jul 2022 18:22:17 +0200 Subject: [PATCH 044/156] Update Change Log --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e3a4db1..3418c04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed -- Better enforcement of value modification to match specified datatype when using setValueExplicit() +- Modify `rangeBoundaries()`, `rangeDimension()` and `getRangeBoundaries()` Coordinate methods to work with row/column ranges as well as with cell ranges and cells [PR #2926](https://github.com/PHPOffice/PhpSpreadsheet/pull/2926) +- Better enforcement of value modification to match specified datatype when using `setValueExplicit()` - Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776) - Memory and speed improvements, particularly for the Cell Collection, and the Writers. From d6a53a88294cb52beb0ce8fbedc39a5d743bef06 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 9 Jul 2022 15:37:15 +0200 Subject: [PATCH 045/156] Update Change log in preparation for release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76df82aa..a4a0b359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## Unreleased - TBD +## 1.24.0 - 2022-07-09 ### Added From ebe8745c92a7cac4514d040758393b5399633b83 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 9 Jul 2022 15:49:09 +0200 Subject: [PATCH 046/156] 1.24.0 - 2022-07-09 Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. ### Added - Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) - Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) - Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. - Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853) - Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) - Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) - Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) - Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty": - Treat rows/columns containing no cell records as empty (default) - Treat cells containing a null value as empty - Treat cells containing an empty string as empty ### Changed - Modify `rangeBoundaries()`, `rangeDimension()` and `getRangeBoundaries()` Coordinate methods to work with row/column ranges as well as with cell ranges and cells [PR #2926](https://github.com/PHPOffice/PhpSpreadsheet/pull/2926) - Better enforcement of value modification to match specified datatype when using `setValueExplicit()` - Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776) - Memory and speed improvements, particularly for the Cell Collection, and the Writers. See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions - Improved performance for removing rows/columns from a worksheet ### Deprecated - Nothing ### Removed - Nothing ### Fixed - Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) - Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) - Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) - Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) [PR #2923](https://github.com/PHPOffice/PhpSpreadsheet/pull/2923) - Adjust both coordinates for two-cell anchors when rows/columns are added/deleted. [Issue #2908](https://github.com/PHPOffice/PhpSpreadsheet/issues/2908) [PR #2909](https://github.com/PHPOffice/PhpSpreadsheet/pull/2909) - Keep calculated string results below 32K. [PR #2921](https://github.com/PHPOffice/PhpSpreadsheet/pull/2921) - Filter out illegal Unicode char values FFFE/FFFF. [Issue #2897](https://github.com/PHPOffice/PhpSpreadsheet/issues/2897) [PR #2910](https://github.com/PHPOffice/PhpSpreadsheet/pull/2910) - Better handling of REF errors and propagation of all errors in Calculation engine. [PR #2902](https://github.com/PHPOffice/PhpSpreadsheet/pull/2902) - Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a0b359..f820d903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ## 1.24.0 - 2022-07-09 +Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. + ### Added - Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) From 99ce5c2a91bb769d2ec036a6471e78fb0cc7cd7f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 9 Jul 2022 16:13:44 +0200 Subject: [PATCH 047/156] Reset ChangeLog ready for next release --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f820d903..f53aeda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## Unreleased - TBD + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + ## 1.24.0 - 2022-07-09 Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. From f0059bb4bcd65e5465673ca67d1274948301f9a3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 14 Jul 2022 07:48:53 -0700 Subject: [PATCH 048/156] Xlsx Chart Reader and Writer Mishandle Explosion Value (#2928) Fix #2506. Reader only tests if Explosion is set without capturing its value. Writer hard-codes value when it is set. --- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 2 +- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 16 ++-- .../Chart/Issue2506Test.php | 73 ++++++++++++++++++ tests/data/Reader/XLSX/issue.2506.xlsx | Bin 0 -> 22547 bytes 4 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/Issue2506Test.php create mode 100644 tests/data/Reader/XLSX/issue.2506.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index cf77a7f2..55365a17 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -228,7 +228,7 @@ class Chart case 'doughnutChart': case 'pieChart': case 'pie3DChart': - $explosion = isset($chartDetail->ser->explosion); + $explosion = self::getAttribute($chartDetail->ser->explosion, 'val', 'string'); $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle("$explosion"); $plotSeries[] = $plotSer; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 356b44e9..8d01038b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -875,10 +875,6 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', "$val"); $objWriter->endElement(); // c:idx - $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', '0'); - $objWriter->endElement(); // c:bubble3D - $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $fillColor); $objWriter->endElement(); // c:spPr @@ -1052,13 +1048,11 @@ class Chart extends WriterPart $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries(); if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { - if ($plotGroup->getPlotStyle() !== null) { - $plotStyle = $plotGroup->getPlotStyle(); - if ($plotStyle) { - $objWriter->startElement('c:explosion'); - $objWriter->writeAttribute('val', '25'); - $objWriter->endElement(); - } + $plotStyle = $plotGroup->getPlotStyle(); + if (is_numeric($plotStyle)) { + $objWriter->startElement('c:explosion'); + $objWriter->writeAttribute('val', $plotStyle); + $objWriter->endElement(); } } diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php new file mode 100644 index 00000000..a2a14c9d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php @@ -0,0 +1,73 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testDataSeriesValues(): void + { + $reader = new XlsxReader(); + self::readCharts($reader); + $spreadsheet = $reader->load(self::DIRECTORY . 'issue.2506.xlsx'); + $worksheet = $spreadsheet->getActiveSheet(); + $charts = $worksheet->getChartCollection(); + self::assertCount(4, $charts); + $originalChart1 = $charts[0]; + self::assertNotNull($originalChart1); + $originalPlotArea1 = $originalChart1->getPlotArea(); + self::assertNotNull($originalPlotArea1); + $originalPlotSeries1 = $originalPlotArea1->getPlotGroup(); + self::assertCount(1, $originalPlotSeries1); + self::assertSame('0', $originalPlotSeries1[0]->getPlotStyle()); + $originalChart2 = $charts[1]; + self::assertNotNull($originalChart2); + $originalPlotArea2 = $originalChart2->getPlotArea(); + self::assertNotNull($originalPlotArea2); + $originalPlotSeries2 = $originalPlotArea2->getPlotGroup(); + self::assertCount(1, $originalPlotSeries2); + self::assertSame('5', $originalPlotSeries2[0]->getPlotStyle()); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(4, $charts2); + $chart1 = $charts[0]; + self::assertNotNull($chart1); + $plotArea1 = $chart1->getPlotArea(); + self::assertNotNull($plotArea1); + $plotSeries1 = $plotArea1->getPlotGroup(); + self::assertCount(1, $plotSeries1); + self::assertSame('0', $plotSeries1[0]->getPlotStyle()); + $chart2 = $charts[1]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $plotSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $plotSeries2); + self::assertSame('5', $plotSeries2[0]->getPlotStyle()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.2506.xlsx b/tests/data/Reader/XLSX/issue.2506.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6b2c7fbcc99e967b846a8e78aba5c0327cf0d8c7 GIT binary patch literal 22547 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 L0AL9TPXYV~Pyzv~ literal 0 HcmV?d00001 From db57af0c7f11db427b0203fe56d1ebbbf9626cfb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 14 Jul 2022 08:30:36 -0700 Subject: [PATCH 049/156] Fix Chart Problems and Memory Leak in Xlsx Writer (#2930) This was supposed to be mopping up some longstanding chart issues. But one of the sample files exposed a memory leak in Xlsx Writer, unrelated to charts. Since that is my best sample file for this problem, I would like to fix both problems at the same time. Xlsx Writer for Worksheets calls getRowDimension for all rows on the sheet. As it happens, the sample file had data in the last rows after a huge gap of rows without any data. It correctly did not write anything for the unused rows. However, the call to getRowDimension actually creates a new RowDimension object if it doesn't already exist, and so it wound up creating over a million totally unneeded objects. This caused it to run out of memory when I tried to make a copy of the 8K input file. The logic is changed to call getRowDimension if and only if (there is data in the row or the RowDimension object already exists). It still has to loop through a million rows, but it no longer allocates the unneeded storage. As for the Chart problems - fix #1797. This is where the file that caused the memory leak originated. Many of its problems were already resolved by the earlier large set of changes to Charts. However, there were a few new properties that needed to be added to Layout to make things complete - numberFormat code and source-linked, and dLblPos (position for labels); and autoTitleDeleted needs to be added to Charts. Also fix #2077, by allowing the format to be specified in the Layout rather than the DataSeriesValues constructor. --- samples/templates/32readwriteLineChart5.xlsx | Bin 0 -> 8344 bytes src/PhpSpreadsheet/Chart/Chart.php | 15 +++ src/PhpSpreadsheet/Chart/Layout.php | 56 +++++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 16 +++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 5 + src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 13 ++- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 97 ++++++++-------- .../Chart/Issue2077Test.php | 108 ++++++++++++++++++ .../PhpSpreadsheetTests/Chart/LayoutTest.php | 6 + 9 files changed, 268 insertions(+), 48 deletions(-) create mode 100644 samples/templates/32readwriteLineChart5.xlsx create mode 100644 tests/PhpSpreadsheetTests/Chart/Issue2077Test.php diff --git a/samples/templates/32readwriteLineChart5.xlsx b/samples/templates/32readwriteLineChart5.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..430cc1d78bb11ba06435263adcee7c9b8b446b97 GIT binary patch literal 8344 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 literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 3f4dd2a7..3f89e6fb 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -141,6 +141,9 @@ class Chart /** @var bool */ private $oneCellAnchor = false; + /** @var bool */ + private $autoTitleDeleted = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -732,4 +735,16 @@ class Chart return $this; } + + public function getAutoTitleDeleted(): bool + { + return $this->autoTitleDeleted; + } + + public function setAutoTitleDeleted(bool $autoTitleDeleted): self + { + $this->autoTitleDeleted = $autoTitleDeleted; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Layout.php b/src/PhpSpreadsheet/Chart/Layout.php index 03a0ca3e..3dabcc63 100644 --- a/src/PhpSpreadsheet/Chart/Layout.php +++ b/src/PhpSpreadsheet/Chart/Layout.php @@ -53,6 +53,19 @@ class Layout */ private $height; + /** + * Position - t=top. + * + * @var string + */ + private $dLblPos = ''; + + /** @var string */ + private $numFmtCode = ''; + + /** @var bool */ + private $numFmtLinked = false; + /** * show legend key * Specifies that legend keys should be shown in data labels. @@ -143,6 +156,12 @@ class Layout if (isset($layout['h'])) { $this->height = (float) $layout['h']; } + if (isset($layout['dLblPos'])) { + $this->dLblPos = (string) $layout['dLblPos']; + } + if (isset($layout['numFmtCode'])) { + $this->numFmtCode = (string) $layout['numFmtCode']; + } $this->initBoolean($layout, 'showLegendKey'); $this->initBoolean($layout, 'showVal'); $this->initBoolean($layout, 'showCatName'); @@ -150,6 +169,7 @@ class Layout $this->initBoolean($layout, 'showPercent'); $this->initBoolean($layout, 'showBubbleSize'); $this->initBoolean($layout, 'showLeaderLines'); + $this->initBoolean($layout, 'numFmtLinked'); $this->initColor($layout, 'labelFillColor'); $this->initColor($layout, 'labelBorderColor'); $this->initColor($layout, 'labelFontColor'); @@ -484,4 +504,40 @@ class Layout return $this; } + + public function getDLblPos(): string + { + return $this->dLblPos; + } + + public function setDLblPos(string $dLblPos): self + { + $this->dLblPos = $dLblPos; + + return $this; + } + + public function getNumFmtCode(): string + { + return $this->numFmtCode; + } + + public function setNumFmtCode(string $numFmtCode): self + { + $this->numFmtCode = $numFmtCode; + + return $this; + } + + public function getNumFmtLinked(): bool + { + return $this->numFmtLinked; + } + + public function setNumFmtLinked(bool $numFmtLinked): self + { + $this->numFmtLinked = $numFmtLinked; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 55365a17..eb425646 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -72,12 +72,18 @@ class Chart $rotX = $rotY = $rAngAx = $perspective = null; $xAxis = new Axis(); $yAxis = new Axis(); + $autoTitleDeleted = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($this->cNamespace); switch ($chartDetailsKey) { + case 'autoTitleDeleted': + /** @var bool */ + $autoTitleDeleted = self::getAttribute($chartElementsC->chart->autoTitleDeleted, 'val', 'boolean'); + + break; case 'view3D': $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer'); @@ -324,6 +330,9 @@ class Chart } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if (is_bool($autoTitleDeleted)) { + $chart->setAutoTitleDeleted($autoTitleDeleted); + } if (is_int($rotX)) { $chart->setRotX($rotX); } @@ -967,6 +976,13 @@ class Chart { $plotAttributes = []; if (isset($chartDetail->dLbls)) { + if (isset($chartDetail->dLbls->dLblPos)) { + $plotAttributes['dLblPos'] = self::getAttribute($chartDetail->dLbls->dLblPos, 'val', 'string'); + } + if (isset($chartDetail->dLbls->numFmt)) { + $plotAttributes['numFmtCode'] = self::getAttribute($chartDetail->dLbls->numFmt, 'formatCode', 'string'); + $plotAttributes['numFmtLinked'] = self::getAttribute($chartDetail->dLbls->numFmt, 'sourceLinked', 'boolean'); + } if (isset($chartDetail->dLbls->showLegendKey)) { $plotAttributes['showLegendKey'] = self::getAttribute($chartDetail->dLbls->showLegendKey, 'val', 'string'); } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index be717053..45111f3c 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1413,6 +1413,11 @@ class Worksheet implements IComparable return $this->rowDimensions[$row]; } + public function rowDimensionExists(int $row): bool + { + return isset($this->rowDimensions[$row]); + } + /** * Get column dimension at a specific column. * diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 8d01038b..08935721 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -68,7 +68,7 @@ class Chart extends WriterPart $this->writeTitle($objWriter, $chart->getTitle()); $objWriter->startElement('c:autoTitleDeleted'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted()); $objWriter->endElement(); $objWriter->startElement('c:view3D'); @@ -420,6 +420,17 @@ class Chart extends WriterPart $objWriter->endElement(); // c:txPr } + if ($chartLayout->getNumFmtCode() !== '') { + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', $chartLayout->getnumFmtCode()); + $objWriter->writeAttribute('sourceLinked', (string) (int) $chartLayout->getnumFmtLinked()); + $objWriter->endElement(); // c:numFmt + } + if ($chartLayout->getDLblPos() !== '') { + $objWriter->startElement('c:dLblPos'); + $objWriter->writeAttribute('val', $chartLayout->getDLblPos()); + $objWriter->endElement(); // c:dLblPos + } $this->writeDataLabelsBool($objWriter, 'showLegendKey', $chartLayout->getShowLegendKey()); $this->writeDataLabelsBool($objWriter, 'showVal', $chartLayout->getShowVal()); $this->writeDataLabelsBool($objWriter, 'showCatName', $chartLayout->getShowCatName()); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index f6c0c035..4b0cb632 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1155,58 +1155,61 @@ class Worksheet extends WriterPart $currentRow = 0; while ($currentRow++ < $highestRow) { - // Get row dimension - $rowDimension = $worksheet->getRowDimension($currentRow); + $isRowSet = isset($cellsByRow[$currentRow]); + if ($isRowSet || $worksheet->rowDimensionExists($currentRow)) { + // Get row dimension + $rowDimension = $worksheet->getRowDimension($currentRow); - // Write current row? - $writeCurrentRow = isset($cellsByRow[$currentRow]) || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; + // Write current row? + $writeCurrentRow = $isRowSet || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; - if ($writeCurrentRow) { - // Start a new row - $objWriter->startElement('row'); - $objWriter->writeAttribute('r', $currentRow); - $objWriter->writeAttribute('spans', '1:' . $colCount); + if ($writeCurrentRow) { + // Start a new row + $objWriter->startElement('row'); + $objWriter->writeAttribute('r', $currentRow); + $objWriter->writeAttribute('spans', '1:' . $colCount); - // Row dimensions - if ($rowDimension->getRowHeight() >= 0) { - $objWriter->writeAttribute('customHeight', '1'); - $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); - } - - // Row visibility - if (!$rowDimension->getVisible() === true) { - $objWriter->writeAttribute('hidden', 'true'); - } - - // Collapsed - if ($rowDimension->getCollapsed() === true) { - $objWriter->writeAttribute('collapsed', 'true'); - } - - // Outline level - if ($rowDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); - } - - // Style - if ($rowDimension->getXfIndex() !== null) { - $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); - $objWriter->writeAttribute('customFormat', '1'); - } - - // Write cells - if (isset($cellsByRow[$currentRow])) { - // We have a comma-separated list of column names (with a trailing entry); split to an array - $columnsInRow = explode(',', $cellsByRow[$currentRow]); - array_pop($columnsInRow); - foreach ($columnsInRow as $column) { - // Write cell - $this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable); + // Row dimensions + if ($rowDimension->getRowHeight() >= 0) { + $objWriter->writeAttribute('customHeight', '1'); + $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); } - } - // End row - $objWriter->endElement(); + // Row visibility + if (!$rowDimension->getVisible() === true) { + $objWriter->writeAttribute('hidden', 'true'); + } + + // Collapsed + if ($rowDimension->getCollapsed() === true) { + $objWriter->writeAttribute('collapsed', 'true'); + } + + // Outline level + if ($rowDimension->getOutlineLevel() > 0) { + $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); + } + + // Style + if ($rowDimension->getXfIndex() !== null) { + $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); + $objWriter->writeAttribute('customFormat', '1'); + } + + // Write cells + if (isset($cellsByRow[$currentRow])) { + // We have a comma-separated list of column names (with a trailing entry); split to an array + $columnsInRow = explode(',', $cellsByRow[$currentRow]); + array_pop($columnsInRow); + foreach ($columnsInRow as $column) { + // Write cell + $this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable); + } + } + + // End row + $objWriter->endElement(); + } } } diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2077Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2077Test.php new file mode 100644 index 00000000..2d1688c6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2077Test.php @@ -0,0 +1,108 @@ +getActiveSheet(); + $worksheet->fromArray( + [ + ['', '2010', '2011', '2012'], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 60], + ] + ); + + // Set the Labels for each data series we want to plot + $dataSeriesLabels1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2012 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2013 + ]; + + // Set the X-Axis Labels + $xAxisTickValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + + // Set the Data values for each data series we want to plot + // TODO I think the third parameter can be set,but I didn't succeed + $dataSeriesValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', NumberFormat::FORMAT_NUMBER_00, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', NumberFormat::FORMAT_PERCENTAGE_00, 4), + ]; + + // Build the dataseries + $series1 = [ + new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ), + ]; + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + // Set the layout to show percentage with 2 decimal points + $layout1->setShowPercent(true); + $layout1->setNumFmtCode(NumberFormat::FORMAT_PERCENTAGE_00); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, $series1); + + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + $yAxisLabel = new Title('Value ($k)'); + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + 'gap', // displayBlanksAs + null, // xAxisLabel + $yAxisLabel + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart1); + self::assertStringContainsString('', $data); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/LayoutTest.php b/tests/PhpSpreadsheetTests/Chart/LayoutTest.php index fbc878e1..5df139e3 100644 --- a/tests/PhpSpreadsheetTests/Chart/LayoutTest.php +++ b/tests/PhpSpreadsheetTests/Chart/LayoutTest.php @@ -42,6 +42,9 @@ class LayoutTest extends TestCase 'w' => 3.0, 'h' => 4.0, 'showVal' => true, + 'dLblPos' => 't', + 'numFmtCode' => '0.00%', + 'numFmtLinked' => true, 'labelFillColor' => $fillColor, 'labelBorderColor' => $borderColor, 'labelFontColor' => $fontColor, @@ -56,6 +59,9 @@ class LayoutTest extends TestCase ->setWidth(3.0) ->setHeight(4.0) ->setShowVal(true) + ->setDLblPos('t') + ->setNumFmtCode('0.00%') + ->setNumFmtLinked(true) ->setLabelFillColor($fillColor) ->setLabelBorderColor($borderColor) ->setLabelFontColor($fontColor); From 5de82981d8583c7a8503ae41b16978f72eb4e82c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 16 Jul 2022 22:08:44 -0700 Subject: [PATCH 050/156] Html Reader Not Handling non-ASCII Data Correctly (#2943) * Html Reader Not Handling non-ASCII Data Correctly Fix #2942. Code was changed by #2894 because PHP8.2 will deprecate how it was being done. See linked issue for more details. Dom loadhtml assumes ISO-8859-1 in the absence of a charset attribute or equivalent, and there is no way to override that assumption. Sigh. The suggested replacements are unsuitable in one way or another. I think this will work with minimal disruption (replace ampersand, less than, and greater than with entities representing illegal characters, then use htmlentities, then restore ampersand, less than, and greater than). * Better Implementation Use regexp to escape non-ASCII. Less kludgey, less reliant on the vagaries of the PHP maintainers. * Additional Tests Test non-ASCII outside of cell contents: sheet title, image alt attribute. * Apply Same Change in Second Location Forgot to change loadFromString. * Additional Test Confirm escaped ampersand is handled correctly. --- src/PhpSpreadsheet/Reader/Html.php | 23 ++++++++++-- .../Reader/Html/HtmlImageTest.php | 4 +-- .../Reader/Html/Issue2942Test.php | 36 +++++++++++++++++++ tests/data/Reader/HTML/utf8chars.html | 28 +++++++++++++++ 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php create mode 100644 tests/data/Reader/HTML/utf8chars.html diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 3d859e15..76f128e0 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -201,7 +201,7 @@ class Html extends BaseReader /** * Loads Spreadsheet from file. */ - protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + public function loadSpreadsheetFromFile(string $filename): Spreadsheet { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -651,7 +651,13 @@ class Html extends BaseReader // Reload the HTML file into the DOM object try { $convert = $this->securityScanner->scanFile($filename); - $loaded = $dom->loadHTML($convert); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -662,6 +668,11 @@ class Html extends BaseReader return $this->loadDocument($dom, $spreadsheet); } + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + /** * Spreadsheet from content. * @@ -674,7 +685,13 @@ class Html extends BaseReader // Reload the HTML file into the DOM object try { $convert = $this->securityScanner->scan($content); - $loaded = $dom->loadHTML($convert); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php index cf4157e3..fe0117ca 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php @@ -13,7 +13,7 @@ class HtmlImageTest extends TestCase $html = ' - +
test imagetest image voilà
'; $filename = HtmlHelper::createHtml($html); @@ -24,7 +24,7 @@ class HtmlImageTest extends TestCase $drawing = $firstSheet->getDrawingCollection()[0]; self::assertEquals($imagePath, $drawing->getPath()); self::assertEquals('A1', $drawing->getCoordinates()); - self::assertEquals('test image', $drawing->getName()); + self::assertEquals('test image voilà', $drawing->getName()); self::assertEquals('100', $drawing->getWidth()); self::assertEquals('100', $drawing->getHeight()); } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php new file mode 100644 index 00000000..3a41805c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php @@ -0,0 +1,36 @@ +éàâèî'; + $reader = new Html(); + $spreadsheet = $reader->loadFromString($content); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + } + + public function testLoadFromFile(): void + { + $file = 'tests/data/Reader/HTML/utf8chars.html'; + $reader = new Html(); + $spreadsheet = $reader->loadSpreadsheetFromFile($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Test Utf-8 characters voilà', $sheet->getTitle()); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + self::assertSame('αβγδε', $sheet->getCell('B1')->getValue()); + self::assertSame('𐐁𐐂𐐃 & だけち', $sheet->getCell('A2')->getValue()); + self::assertSame('אבגדה', $sheet->getCell('B2')->getValue()); + self::assertSame('𪔀𪔁𪔂', $sheet->getCell('C2')->getValue()); + self::assertSame('᠐᠑᠒', $sheet->getCell('A3')->getValue()); + self::assertSame('അആ', $sheet->getCell('B3')->getValue()); + self::assertSame('กขฃ', $sheet->getCell('C3')->getValue()); + self::assertSame('✀✐✠', $sheet->getCell('D3')->getValue()); + } +} diff --git a/tests/data/Reader/HTML/utf8chars.html b/tests/data/Reader/HTML/utf8chars.html new file mode 100644 index 00000000..8d58c798 --- /dev/null +++ b/tests/data/Reader/HTML/utf8chars.html @@ -0,0 +1,28 @@ + + + + +Test Utf-8 characters voilà + + + + + + + + + + + + + + + + + + + + +
éàâèîαβγδε
𐐁𐐂𐐃 & だけちאבגדה𪔀𪔁𪔂
᠐᠑᠒അആกขฃ✀✐✠
+ + From a062521a18b52040863939b5b71ae93a6b66231d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 16 Jul 2022 22:30:11 -0700 Subject: [PATCH 051/156] Fixes for Surface Charts (#2933) Fix #2931. If surface charts are written with c:grouping tag, Excel will treat them as corrupt. Also, some 3D-ish Xml tags are required when 2D surface charts are written, else they will be treated as 3D. Also eliminate a duplicate line identified by #2932. --- src/PhpSpreadsheet/Chart/Axis.php | 1 - src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 54 +++++---- .../Chart/Issue2931Test.php | 111 ++++++++++++++++++ 3 files changed, 140 insertions(+), 26 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/Issue2931Test.php diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 1f55cf03..222ee8e8 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -144,7 +144,6 @@ class Axis extends Properties $this->setAxisOption('orientation', $axisOrientation); $this->setAxisOption('major_tick_mark', $majorTmt); $this->setAxisOption('minor_tick_mark', $minorTmt); - $this->setAxisOption('minor_tick_mark', $minorTmt); $this->setAxisOption('minimum', $minimum); $this->setAxisOption('maximum', $maximum); $this->setAxisOption('major_unit', $majorUnit); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 08935721..bfcf91a2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -72,30 +72,22 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:view3D'); - $rotX = $chart->getRotX(); - if (is_int($rotX)) { - $objWriter->startElement('c:rotX'); - $objWriter->writeAttribute('val', "$rotX"); - $objWriter->endElement(); - } - $rotY = $chart->getRotY(); - if (is_int($rotY)) { - $objWriter->startElement('c:rotY'); - $objWriter->writeAttribute('val', "$rotY"); - $objWriter->endElement(); - } - $rAngAx = $chart->getRAngAx(); - if (is_int($rAngAx)) { - $objWriter->startElement('c:rAngAx'); - $objWriter->writeAttribute('val', "$rAngAx"); - $objWriter->endElement(); - } - $perspective = $chart->getPerspective(); - if (is_int($perspective)) { - $objWriter->startElement('c:perspective'); - $objWriter->writeAttribute('val', "$perspective"); - $objWriter->endElement(); + $surface2D = false; + $plotArea = $chart->getPlotArea(); + if ($plotArea !== null) { + $seriesArray = $plotArea->getPlotGroup(); + foreach ($seriesArray as $series) { + if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) { + $surface2D = true; + + break; + } + } } + $this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90); + $this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D); + $this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D); + $this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D); $objWriter->endElement(); // view3D $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); @@ -124,6 +116,18 @@ class Chart extends WriterPart return $objWriter->getData(); } + private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void + { + if ($value === null && $surface2D) { + $value = $default; + } + if ($value !== null) { + $objWriter->startElement($tag); + $objWriter->writeAttribute('val', "$value"); + $objWriter->endElement(); + } + } + /** * Write Chart Title. */ @@ -913,8 +917,8 @@ class Chart extends WriterPart $objWriter->endElement(); } - if ($plotGroup->getPlotGrouping() !== null) { - $plotGroupingType = $plotGroup->getPlotGrouping(); + $plotGroupingType = $plotGroup->getPlotGrouping(); + if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) { $objWriter->startElement('c:grouping'); $objWriter->writeAttribute('val', $plotGroupingType); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php new file mode 100644 index 00000000..960eb04e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php @@ -0,0 +1,111 @@ +getActiveSheet(); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['5-6']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['6-7']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['7-8']), + ]; + + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [1, 2, 3, 4, 5, 6, 7, 8, 9]), + ]; + + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6, 6, 6, 5.9, 6, 6]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6.5, 7, 7, 7, 7, 7]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 7, 8, 8, 8, 8, 7.9]), + ]; + + $series = new DataSeries( + DataSeries::TYPE_SURFACECHART, + DataSeries::GROUPING_STANDARD, // grouping should not be written for surface chart + range(0, count($dataSeriesValues) - 1), + $dataSeriesLabels, + $xAxisTickValues, + $dataSeriesValues, + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + ); + + $plotArea = new PlotArea(null, [$series]); + $legend = new ChartLegend(ChartLegend::POSITION_BOTTOM, null, false); + + $title = new Title('График распредления температур в пределах кр'); + + $chart = new Chart( + 'chart2', + $title, + $legend, + $plotArea, + true, + DataSeries::EMPTY_AS_GAP, + ); + + $chart->setTopLeftPosition('$A$1'); + $chart->setBottomRightPosition('$P$20'); + + $sheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + + // rotX etc. should be generated for surfaceChart 2D + // even when unspecified. + $expectedXml2D = [ + '', + ]; + $expectedXml3D = [ + '', + ]; + $expectedXmlNoX = [ + 'c:grouping', + ]; + + // confirm that file contains expected tags + foreach ($expectedXml2D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $series->setPlotType(DataSeries::TYPE_SURFACECHART_3D); + $plotArea = new PlotArea(null, [$series]); + $chart->setPlotArea($plotArea); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + // confirm that file contains expected tags + foreach ($expectedXml3D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $spreadsheet->disconnectWorksheets(); + } +} From 4bf4278a39350a7500e4923b101243f010749ae3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 17 Jul 2022 06:27:56 -0700 Subject: [PATCH 052/156] VLOOKUP Breaks When Array Contains Null Cells (#2939) Fix #2934. Null is passed to StringHelper::strtolower which expects string. Same problem appears to be applicable to HLOOKUP. I noted the following problem in the code, but will document it here as well. Excel's results are not consistent when a non-numeric string is passed as the third parameter. For example, if cell Z1 contains `xyz`, Excel will return a REF error for function `VLOOKUP(whatever,whatever,Z1)`, but it returns a VALUE error for function `VLOOKUP(whatever,whatever,"xyz")`. I don't think PhpSpreadsheet can match both behaviors. For now, it will return VALUE for both, with similar results for other errors. While studying the returned errors, I realized there is something that needs to be deprecated. `ExcelError::$errorCodes` is a public static array. This means that a user can change its value, which should not be allowed. It is replaced by a constant. Since the original is public, I think it needs to stay, but with a deprecation notice; users can reference and change it, but it will be unused in the rest of the code. I suppose this might be considered a break in functionality (that should not have been allowed in the first place). --- .../Calculation/Information/ErrorValue.php | 2 +- .../Calculation/Information/ExcelError.php | 33 ++++++++++------ .../Calculation/LookupRef/HLookup.php | 2 +- .../Calculation/LookupRef/LookupBase.php | 12 +++++- .../Calculation/LookupRef/VLookup.php | 6 +-- .../Functions/LookupRef/VLookupTest.php | 34 ++++++++++++----- tests/data/Calculation/LookupRef/HLOOKUP.php | 10 +++++ tests/data/Calculation/LookupRef/VLOOKUP.php | 38 +++++++++++++++++-- 8 files changed, 106 insertions(+), 31 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dda2c705..4b9f818f 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes, true); + return in_array($value, ExcelError::ERROR_CODES, true); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 5ca74a3e..06f38663 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -13,7 +13,7 @@ class ExcelError * * @var array */ - public static $errorCodes = [ + public const ERROR_CODES = [ 'null' => '#NULL!', // 1 'divisionbyzero' => '#DIV/0!', // 2 'value' => '#VALUE!', // 3 @@ -30,12 +30,23 @@ class ExcelError 'calculation' => '#CALC!', //14 ]; + /** + * List of error codes. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use ERROR_CODES constant instead. + * + * @var array + */ + public static $errorCodes = self::ERROR_CODES; + /** * @param mixed $value */ public static function throwError($value): string { - return in_array($value, self::$errorCodes, true) ? $value : self::$errorCodes['value']; + return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value']; } /** @@ -52,7 +63,7 @@ class ExcelError } $i = 1; - foreach (self::$errorCodes as $errorCode) { + foreach (self::ERROR_CODES as $errorCode) { if ($value === $errorCode) { return $i; } @@ -71,7 +82,7 @@ class ExcelError */ public static function null(): string { - return self::$errorCodes['null']; + return self::ERROR_CODES['null']; } /** @@ -83,7 +94,7 @@ class ExcelError */ public static function NAN(): string { - return self::$errorCodes['num']; + return self::ERROR_CODES['num']; } /** @@ -95,7 +106,7 @@ class ExcelError */ public static function REF(): string { - return self::$errorCodes['reference']; + return self::ERROR_CODES['reference']; } /** @@ -111,7 +122,7 @@ class ExcelError */ public static function NA(): string { - return self::$errorCodes['na']; + return self::ERROR_CODES['na']; } /** @@ -123,7 +134,7 @@ class ExcelError */ public static function VALUE(): string { - return self::$errorCodes['value']; + return self::ERROR_CODES['value']; } /** @@ -135,7 +146,7 @@ class ExcelError */ public static function NAME(): string { - return self::$errorCodes['name']; + return self::ERROR_CODES['name']; } /** @@ -145,7 +156,7 @@ class ExcelError */ public static function DIV0(): string { - return self::$errorCodes['divisionbyzero']; + return self::ERROR_CODES['divisionbyzero']; } /** @@ -155,6 +166,6 @@ class ExcelError */ public static function CALC(): string { - return self::$errorCodes['calculation']; + return self::ERROR_CODES['calculation']; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index d67718ce..e2d27bde 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -66,7 +66,7 @@ class HLookup extends LookupBase */ private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray[$column] as $rowKey => $rowData) { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 8e451fe4..a001540c 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -19,8 +19,16 @@ abstract class LookupBase protected static function validateIndexLookup(array $lookup_array, $index_number): int { - // index_number must be a number greater than or equal to 1 - if (!is_numeric($index_number) || $index_number < 1) { + // index_number must be a number greater than or equal to 1. + // Excel results are inconsistent when index is non-numeric. + // VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but + // VLOOKUP(whatever, whatever, cellref) yields REF error + // when cellref is '=SQRT(-1)'. So just try our best here. + // Similar results if string (literal yields VALUE, cellRef REF). + if (!is_numeric($index_number)) { + throw new Exception(ExcelError::throwError($index_number)); + } + if ($index_number < 1) { throw new Exception(ExcelError::VALUE()); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 53a7badc..edeb1aa8 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -68,8 +68,8 @@ class VLookup extends LookupBase { reset($a); $firstColumn = key($a); - $aLower = StringHelper::strToLower($a[$firstColumn]); - $bLower = StringHelper::strToLower($b[$firstColumn]); + $aLower = StringHelper::strToLower((string) $a[$firstColumn]); + $bLower = StringHelper::strToLower((string) $b[$firstColumn]); if ($aLower == $bLower) { return 0; @@ -84,7 +84,7 @@ class VLookup extends LookupBase */ private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray as $rowKey => $rowData) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php index 4e05ea8a..5b7647be 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php @@ -3,26 +3,42 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class VLookupTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerVLOOKUP * * @param mixed $expectedResult + * @param mixed $value + * @param mixed $table + * @param mixed $index */ - public function testVLOOKUP($expectedResult, ...$args): void + public function testVLOOKUP($expectedResult, $value, $table, $index, ?bool $lookup = null): void { - $result = LookupRef::VLOOKUP(...$args); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + if (is_array($table)) { + $sheet->fromArray($table); + $dimension = $sheet->calculateWorksheetDimension(); + } else { + $sheet->getCell('A1')->setValue($table); + $dimension = 'A1'; + } + if ($lookup === null) { + $lastarg = ''; + } else { + $lastarg = $lookup ? ',TRUE' : ',FALSE'; + } + $sheet->getCell('Z98')->setValue($value); + $sheet->getCell('Z97')->setValue($index); + + $sheet->getCell('Z99')->setValue("=VLOOKUP(Z98,$dimension,Z97$lastarg)"); + $result = $sheet->getCell('Z99')->getCalculatedValue(); self::assertEquals($expectedResult, $result); + $spreadsheet->disconnectWorksheets(); } public function providerVLOOKUP(): array diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 078ed007..ee1b919e 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -186,4 +186,14 @@ return [ 3, true, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, 102], + [null, 'Red'], + ], + 2, + false, + ], ]; diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index 2162d49a..21146638 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -98,7 +98,7 @@ return [ ['10y1', 7.0], ['10y2', 10.0], ], - 'NaN', + -5, ], [ '#REF!', @@ -111,9 +111,9 @@ return [ '#REF!', '10y2', [ - 2.0, - 7.0, - 10.0, + [2.0], + [7.0], + [10.0], ], 2.0, ], @@ -163,4 +163,34 @@ return [ 3, null, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, null], + [102, 'Red'], + ], + 2, + false, + ], + 'string supplied as index' => [ + '#VALUE!', + 102, + [ + [null, null], + [102, 'Red'], + ], + 'xyz', + false, + ], + 'num error propagated' => [ + '#NUM!', + 102, + [ + [null, null], + [102, 'Red'], + ], + '=SQRT(-1)', + false, + ], ]; From 051598ecfad3fdc508b728db7d2e1d500d33216a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 17 Jul 2022 06:46:22 -0700 Subject: [PATCH 053/156] Add Chart Axis Option textRotation (#2940) Fix #2705. Add to Axis class, Reader Xlsx Chart, and Writer Xlsx Chart. Add feature to an existing 32* sample, to an existing 33* sample, and a formal unit test. --- samples/Chart/33_Chart_create_scatter2.php | 1 + .../templates/32readwriteScatterChart8.xlsx | Bin 12409 -> 12438 bytes src/PhpSpreadsheet/Chart/Axis.php | 5 ++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 10 ++++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 34 ++++++++++++++++++ .../Chart/Charts32ScatterTest.php | 3 ++ 6 files changed, 52 insertions(+), 1 deletion(-) diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index ef6353bb..eed87119 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -124,6 +124,7 @@ $dataSeriesValues[2]->setScatterLines(false); // points not connected $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); $yAxis = new Axis(); $yAxis->setLineStyleProperties( diff --git a/samples/templates/32readwriteScatterChart8.xlsx b/samples/templates/32readwriteScatterChart8.xlsx index fdd85b0ed3bf9fbac400a46a350da93f74e5d9a4..1d4080d795448bc8e74f527f22750a80208cd8be 100644 GIT binary patch delta 4502 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 self::AXIS_LABELS_NEXT_TO, 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, + 'textRotation' => null, ]; /** @@ -136,7 +137,8 @@ class Axis extends Properties ?string $minimum = null, ?string $maximum = null, ?string $majorUnit = null, - ?string $minorUnit = null + ?string $minorUnit = null, + ?string $textRotation = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -148,6 +150,7 @@ class Axis extends Properties $this->setAxisOption('maximum', $maximum); $this->setAxisOption('major_unit', $majorUnit); $this->setAxisOption('minor_unit', $minorUnit); + $this->setAxisOption('textRotation', $textRotation); } /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index eb425646..d49a5238 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -1279,5 +1279,15 @@ class Chart if (isset($chartDetail->minorUnit)) { $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); } + if (isset($chartDetail->txPr)) { + $children = $chartDetail->txPr->children($this->aNamespace); + if (isset($children->bodyPr)) { + /** @var string */ + $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); + if (is_numeric($textRotation)) { + $whichAxis->setAxisOption('textRotation', (string) Properties::xmlToAngle($textRotation)); + } + } + } } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index bfcf91a2..d9d96da6 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -573,6 +573,23 @@ class Chart extends WriterPart $objWriter->endElement(); } + $textRotation = $yAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $yAxis->getFillColorObject()); $this->writeEffects($objWriter, $yAxis); @@ -748,6 +765,23 @@ class Chart extends WriterPart $objWriter->endElement(); } + $textRotation = $xAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + $objWriter->startElement('c:spPr'); $this->writeColor($objWriter, $xAxis->getFillColorObject()); $this->writeLineStyles($objWriter, $xAxis); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index b655afc4..77b0a9b2 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -391,6 +391,9 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + self::assertEquals(45, $xAxis->getAxisOptionsProperty('textRotation')); + $plotArea = $chart->getPlotArea(); self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); From 69991111e05fca3ff7398e1e7fca9ebed33efec6 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 18 Jul 2022 21:50:48 +0200 Subject: [PATCH 054/156] 1.24.1 - 2022-07-18 ### Added - Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) ### Changed - Nothing ### Deprecated - Nothing ### Removed - Nothing ### Fixed - Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) - Additional Chart fixes - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) - VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f53aeda2..3356440d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## Unreleased - TBD +## 1.24.1 - 2022-07-18 ### Added -- Nothing +- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) ### Changed @@ -25,7 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing +- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) +- Additional Chart fixes + - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) + - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) + - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) +- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) ## 1.24.0 - 2022-07-09 From 48d531c476681ba8316dd1702697f422db7a21f3 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 18 Jul 2022 22:13:48 +0200 Subject: [PATCH 055/156] Reset ChangeLog ready for next release --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3356440d..a2fd0cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## Unreleased - TBD + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + ## 1.24.1 - 2022-07-18 ### Added From 93b0de0414bda8aafac23ed28e811c64908feee2 Mon Sep 17 00:00:00 2001 From: Jonathan Goode Date: Wed, 20 Jul 2022 11:36:40 +0100 Subject: [PATCH 056/156] Typo --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c3a74ede..e2e66a4a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -34,7 +34,7 @@ If this is an issue with reading a specific spreadsheet file, then it may be app - [ ] Writer - [ ] Styles - [ ] Data Validations -- [ ] Formula Calulations +- [ ] Formula Calculations - [ ] Charts - [ ] AutoFilter - [ ] Form Elements From faa7c870e91673c0a99b84ab01da6c739d5f98fc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 22 Jul 2022 11:23:12 +0200 Subject: [PATCH 057/156] Add throws tag to writer::save --- src/PhpSpreadsheet/Writer/IWriter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PhpSpreadsheet/Writer/IWriter.php b/src/PhpSpreadsheet/Writer/IWriter.php index b0a62726..d8bf1f0c 100644 --- a/src/PhpSpreadsheet/Writer/IWriter.php +++ b/src/PhpSpreadsheet/Writer/IWriter.php @@ -62,6 +62,8 @@ interface IWriter * Save PhpSpreadsheet to file. * * @param resource|string $filename Name of the file to save + * + * @throws Exception */ public function save($filename, int $flags = 0): void; From b3f319a8d38db2961083f61be1733d687d6ded3c Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 22 Jul 2022 14:37:20 +0200 Subject: [PATCH 058/156] Do not remove @throws --- .php-cs-fixer.dist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index ca2feb42..16809b84 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -55,7 +55,7 @@ $config 'function_declaration' => true, 'function_to_constant' => true, 'function_typehint_space' => true, - 'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright', 'throws']], + 'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright']], 'global_namespace_import' => true, 'header_comment' => false, // We don't use common header in all our files 'heredoc_indentation' => false, // Requires PHP >= 7.3 From abc1d3db70c51d59ad60ae493bd58e7985392598 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:46:32 -0700 Subject: [PATCH 059/156] Big Memory Leak in One Test (#2958) * Big Memory Leak in One Test Many tests leak a little by not issuing disconnectWorksheets at the end. CellMatcherTest leaks a lot by opening a spreadsheet many times. Phpunit reported a high watermark of 390MB; fixing CellMatcherTest brings that figure down to 242MB. I have also changed it to use a formal assertion in many cases where markTestSkipped was (IMO inappropriately) used. * Another Leak Issue2301Test lacks a disconnect. Adding one reduces HWM from 242MB to 224. --- .../Reader/Xlsx/Issue2301Test.php | 2 + .../ConditionalFormatting/CellMatcherTest.php | 105 +++++++----------- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php index 4484b9e0..7a5829f7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php @@ -24,5 +24,7 @@ class Issue2301Test extends \PHPUnit\Framework\TestCase self::assertSame('Arial CE', $font->getName()); self::assertSame(9.0, $font->getSize()); self::assertSame('protected', $sheet->getCell('BT10')->getStyle()->getProtection()->getHidden()); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); } } diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 3c96403b..2c6d0da8 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -10,15 +10,24 @@ use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase { /** - * @var Spreadsheet + * @var ?Spreadsheet */ protected $spreadsheet; - protected function setUp(): void + protected function loadSpreadsheet(): Spreadsheet { $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); - $this->spreadsheet = $reader->load($filename); + + return $reader->load($filename); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } /** @@ -26,16 +35,13 @@ class CellMatcherTest extends TestCase */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "{$cellAddress} is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -82,16 +88,13 @@ class CellMatcherTest extends TestCase */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -128,16 +131,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -167,16 +167,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -216,16 +213,13 @@ class CellMatcherTest extends TestCase */ public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -329,16 +323,13 @@ class CellMatcherTest extends TestCase */ public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -365,16 +356,13 @@ class CellMatcherTest extends TestCase */ public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -400,16 +388,13 @@ class CellMatcherTest extends TestCase */ public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -447,16 +432,13 @@ class CellMatcherTest extends TestCase */ public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::AssertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -486,16 +468,13 @@ class CellMatcherTest extends TestCase */ public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); From b8456105dd5ea44210b5f33dcac3ab732af4444d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:06:13 -0700 Subject: [PATCH 060/156] Charts - Gradients, Transparency, Hidden Axes (#2950) Fix #2257. Fix #2929. Fix #2935 (probably in a way that will not satisfy the requester). 2257 and 2929 requested changes that ultimately affect the same section of code, so it's appropriate to deal with them together. 2257 requests the ability to make the chart background transparent (so that the Excel gridlines are visible beneath the chart), and the ability to hide an Axis. 2929 requests the ability to set a gradient background on the chart. --- samples/Chart/33_Chart_create_scatter3.php | 192 ++++++++++++++++++ samples/Chart/33_Chart_create_scatter4.php | 130 ++++++++++++ .../templates/32readwriteScatterChart10.xlsx | Bin 0 -> 12363 bytes .../templates/32readwriteScatterChart9.xlsx | Bin 0 -> 12554 bytes src/PhpSpreadsheet/Chart/Axis.php | 5 +- src/PhpSpreadsheet/Chart/Chart.php | 15 ++ src/PhpSpreadsheet/Chart/ChartColor.php | 32 ++- src/PhpSpreadsheet/Chart/DataSeries.php | 2 +- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Chart/PlotArea.php | 62 ++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 51 +++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 56 ++++- .../Chart/Charts32ScatterTest.php | 85 ++++++++ 13 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 samples/Chart/33_Chart_create_scatter3.php create mode 100644 samples/Chart/33_Chart_create_scatter4.php create mode 100644 samples/templates/32readwriteScatterChart10.xlsx create mode 100644 samples/templates/32readwriteScatterChart9.xlsx diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php new file mode 100644 index 00000000..b4fc97b1 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -0,0 +1,192 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); +$xAxis->setAxisOption('hidden', '1'); + +$yAxis = new Axis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$yAxis->setAxisOption('hidden', '1'); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setNoFill(true); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend Chart'); +//$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null, //$yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); +$chart->setNoFill(true); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('P20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_scatter4.php b/samples/Chart/33_Chart_create_scatter4.php new file mode 100644 index 00000000..d42933b6 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter4.php @@ -0,0 +1,130 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 +]; +// Set the X-Axis Labels +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), +]; + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); + +$pos1 = 0; // pos = 0% (extreme low side or lower left corner) +$brightness1 = 0; // 0% +$gsColor1 = new ChartColor(); +$gsColor1->setColorProperties('FF0000', 75, 'srgbClr', $brightness1); // red +$gradientStop1 = [$pos1, $gsColor1]; + +$pos2 = 0.5; // pos = 50% (middle) +$brightness2 = 0.5; // 50% +$gsColor2 = new ChartColor(); +$gsColor2->setColorProperties('FFFF00', 50, 'srgbClr', $brightness2); // yellow +$gradientStop2 = [$pos2, $gsColor2]; + +$pos3 = 1.0; // pos = 100% (extreme high side or upper right corner) +$brightness3 = 0.5; // 50% +$gsColor3 = new ChartColor(); +$gsColor3->setColorProperties('00B050', 50, 'srgbClr', $brightness3); // green +$gradientStop3 = [$pos3, $gsColor3]; + +$gradientFillStops = [ + $gradientStop1, + $gradientStop2, + $gradientStop3, +]; +$gradientFillAngle = 315.0; // 45deg above horiz + +$plotArea->setGradientFillProperties($gradientFillStops, $gradientFillAngle); + +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart10.xlsx b/samples/templates/32readwriteScatterChart10.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7d5ac0d280f328c2685e6de590a78a2e30a9cb00 GIT binary patch literal 12363 zcmeHtg3b1$TFs;2IzVcMI?et4s6vI61T)74)4XW!(4gT=zQ>M?a$(Hb!j9pyMs9u&>+_>+iSdk7@zR8&`$}Of69)@EM1w2 z^Mn5kl~oB2T{r$UX*QdM>J;wDp7nw&3<}bG9P5Jz7w9VIx4TwClHn_C11_`9tZ72g zhcxL;AusT^@yuMEZ%+sjclyeOHcm#a-E2R&dr~k%Zrfvz#y)+gUf6q8o0h1I38vf1 zcc61oprQT#Xn%jM=NUxKy6B@E0gN9KMjD=ys$T{{(U!KCfthK27rnCw0S=cFR${29}be-d}S;Bgo@5f zR_|IClz4CN3`I-nkRWDXy4j23GJ8FHlPoUfM(xrTNn6rbm?b^5P9`>eE>eX&!Jvu@ zgO-OMgvp=mr_nE~zGiS&4l*mObW#>n*}$237(0>bGoO%ufFKaUEpt4Tj4|kFV7gfD zIcP<4{T=t6k{P#or9rv_52>53kyYoVNNNY#56@R}X@iQSY^ZlE6XF9Dnb$rVwQT1j zpWS*nVR}jizYY0+A}U-1u4I2F$>|CA0z8mDlTZKv67ZLKZ^h_p>tJbMYis#)ean5P zZJWx3?v+vd?E5I!1+xtnFGB+UN>S;Z^<1lRF}|JuyUx+1pyETHb@r%S^L!dh1p~wN zqb6WkbO4MMp0Tf)j%FI`c&M;+qIB*q}l%q?0_0grXCI#;s!Q zg;Zbywe>Yiz#rYe4N$UuUX|11S)?`2+JMj{FO-p7 zyqO-q?-ldTHgcBNSO0Rv3_JIoWuM=+7wXuW<5A|9ofL|z_ULl4i^r7QVnvuKRK-E_ zUh;6QnP8Ijsh+?OYzn(NL#UN5$2RjK{br1Ncv*h;<^gpf>Qz?GlA3{#i0T-eBK;aPs93UT!3eQPXXFxc*H1*G zB5$QcpWuVfZA-h7-(HsuU6-Y1ExE z&n@aB%kPoK#&7aLRc`0`bC$o#`>@XxQ;d+dJ4Z*vTj)jvFE2G3MTe0LX3c^mnw(M@ zr($Cp)}(`frZZvSG1%q|1AXr_i_grmrci7vm8o*8p;Dq*R7KcRR?0BNGeK-7ZcS!_ zQhwB^i4#Mv>>pI_f^Sg6s!UiLqxzxL0~sAGD4qY?=xJq+CJ3Sv9FpwnrF(+SgV6k` zp$|(P*gI0^Mc#N}EuRZ`q%e1OifZs^IPVoDz+~fI-MSwsAvNqG@R6SrvJ^^X*cjClA z#T|nKDJ1|T4=w;21W24e7MQ=%=UKhmBx%%^fJMV-hOxr_|k6A;w2ws zZ}LSk$vV~B*Dj7r#sEswYx~Snk!Pqg%BGrGqWhNcW6QFSYuOz+5w0nQktD2tsf)3p zCB#|Z$QW5jrS2l@rUPnX-0x7+tjE%S`&8c>9YgqhPdT#R$X)1 zBuO~4`V$6X9)GY-Reaylp{s`2%VWoQRh@>))XgzfN_YP-Z*i53O4MlHSPe<*4tPt1 z<(jd}d&JQW1`Q6=uUZS2#fWp64L0Uw;=#l1W|4XC=z?egZ0oB`bNMr+46`v}J&Vl-1pAYk zRNwI^uC87zQupaZWusO(zLCP?#Ds*a*oPwcL7`k3$F zfv1m*3P)BC^n5EMkqS#UHA(=I%Ci@qOc6!D&#(La>kt1yUxU*pX(Qg5yA$K+nH`-hJ0TSC&SrX%;+t8n!Bv-Ry}ade9Nf+n+? zW&5}lYTKpN^Kzb5mqhPKiD4FUQOlZmnAEpRtw=G@d=h@kA7@|ZkSM(lfa($3+ytTH zlftV%R7}gI6+qP8dDz>800FrdI}Xr+c&#oyeozl>O^f}5CO6uI-;W-07$lPrdV3Mq z4NoU#FP$&SY!m};r-sjK+@3FW$In1t2`A@MDeQMhG}bO1z99UZN}yl<~Q2s=x9xN)X$ zn;V(HJ983n3b&;aT$@k}0Ts1Ellfh0h~#E=fie$L^%YOyt?Khi7E7`-#x-Bne8)MJa|d zqbjIKb(A>UC(W+U-e*(pzPa7%fjAQqbV@qV-cv3*yN69iMDQ1%Y3)59v8dwT^5s zD=9Jt%HW$#^)BjHekm+QaUkWD$$T@a0rKtRok9kmJcxlB?fWmVw7ZOIi>=N6oT0KZ zuNF0z=2?CE+6~^YX|VH&@+C2s-B^s#EwkQ3cFvHNFQ2$H&*Jcdb?QT>Xs8dfS+-qU zcu3WNR|{a7yV71ujrAq&SG*crv1(YpHX->$VIg>9Y7Nq%V?I-_LeL7L?&$@hYiaAu zg8*eKh`FxTd0Wng$Btzy2$QpD22w__bc8Wu8bxBtt8>TGIZp@S8R2i1B${Z#yq&Cp zX-U&QA&LaR9jwg8QH)K4w})0(={gXUFU8qF@RzndR;0#CU}GeIBjh*>`+Ahws5fn` zWN*F0{RzFz{b~Ddvt}z;fIz_A>G7Mm^vGgkKuu%o`A$Q|kMGw5`;i6ft)7n`h!{ui zbbao&6Eg&E!AJsjQQPh=hHNL&Tkj61*E>9p{cK_QsCypRC{R~P#mzDbGEHHUl6ZLSKx_GPTWIZl8z_^$+gX3bJuCP9x(TL3wC-xeFVU-}fV77vc?fXjHnnDG zg>1}ni>F^9GOfyJ!cMe*z$UtX?`pT(SkF72Wg!M0T; z-r^C*!NUUa!OrIOs^Jo?t;owiGV z+&Z0x>mtDAB^?73>A-=DWUN3t-Q5aAU}k;?Db=i0q?FQDZp8}g{Gh#{07>n?<>l6xo}6xWS397FyqM1r;3qFMfi8_YnG|wyxL3ek!TE| zEV>6}&E0KbpyX!)L|M_3hU_>(c3>Xqmpq}c;H++lyJp3eDWG7+vurC;m{}4a4%D*K zq?545s&UMQV3tfo@+O;8&1=f{kTtoT9L4%&GOCiz3&Q5u{AySz@wE^Ssyi( z&6}PjFCqN`#BJeXYkD+1+}(F5p!BQ(ZP7k;h$d~+5LUSze(07*$nZX?=z8Mwbw*s| zS+u+@wFamB@R`(ko~0pN64_*IdWKbzT|an~g)%~<8qb+9gakUT)9UNpb};uy4~7#B zAWi3)N6@t5G(Trg%hcP&4zS{k#>UTuKsCD=a}Iyj#fZJY%9Y{{dp0fJzXUHQo+CZ+ zL$(Qq5aZF+1i_-+l>cxM7{m{PWt#6ISlfxg*cz1S&ueknK^%PVlj@idBx#vMKKnX6 zI@6A@NCdeW%6Ld*ou;?AZ_!w?+CS=JVN4@Sg8|YYiN2RM7~Z`Vl00aZO|hf`B>bt- zW7C8vB!a=}_M;LnQR?WPHIkF7o+f?Eb6A^1nevsixX`r6O>(-03Zjl5DP^fX|Cs3~sF6wy083$& zz;z1Yk2T8C$<4~x@sB)z9hm1Mdjs?Qt_xoF$tVgG`B$>hwANncR3}-CkTM3d0#VEa zi?z>Qro=D`1?Rm$dd%Xw-^oqi4EjtR$82#76qL zC5437GteZPy1}}oFnz#Ci`*ENhXD>sv+AYC@t@FoqNR@ z-;_tNfi-c<*bTW~tQ8on*n8G^JKK4C>L- zzK$bc1j&_x2=$gHgqV6>VS=BUMPj^J76!Vn#~&IrT$gBKSA-?RCfbo_dO1du^-mDbs1Y1DL%Ct*RH+)a_ru=Xe2FgRpBF7p7`9Tb-#=p zian|l?n^fqW*)YE_O>4UEoFw5A)+GK3C*oB*R6n<-JB7eV z-vRFEe@48SzJsxml9PkEjp?tzw;Gp)?PfxT*l61z=svc-2P2Kih3i7w2NC975@an( zF4{jSgZuJfjYrk^L@q#TAsL(#8vjGva&5!V9nicRh zg~rlW`pC&Q8jhTLS6aC+=H?7E;*8Rxs$)uG>zBUN_A}1vHqKXmg3jiYRg=CcG%F^2 zp4F`QX((6<4*Rq@eKvFCI;F#5CFM+4gT-M(a1sH^`}#ef4=x5 zaa10&S!RN7M_S;8ZsvU|ilq=cHu!!npSka@MkE-uvyemq5tEWjeSex`%{GqF~;g+QHukMr4BQ2e88b6ahehUPoB3uG3<_)%Px4jJ722URknc#RONEYYqo_ z``LV)o;*K7^$LH)>h!Tm+;0`JkJA{#L;PgCUrqFMiphU|GnJkZ{HZ$1N~_O9(#P`C z4$tn?(m}=ijWV4XPfVBdB+USmHlqs!QzBM&TMVKG6g$^<{L(Tk&fci@r)PePnF|=t z$x+F4==+b%N6~zp!TIe}j${X{rrlRL6>59#!ki~MhPf=uzK7+Z{FXU*yf;snJ@IBT zk)~CaAj_C$dWRXDo!=L+O78x#Thak%&$I+w8qRV4O%_8SS%7tjU#CBF810`qj2BVm zb0&6e&|~MFI)*kKI8XN@ z=J@?^y}ibM23CHtX~Dqvp%ns8I3lsQ-aIB@as}600@L8k!QGRlFO@bkd_5%@h}@h&DIP~?qIwHtX23nwgRegpL3I6GGw_ycT~ zmZm$ooymB$CF%$ux!^FzHw5d=Z;bSKT{Klt<0hOT#x;o;oGEff__`!T)DcG%Y9_wg ziGZ7MH-?%q5L*POx*&hWG2k4@hS-1p&$W?ev-HbMO8)*w{8^~6sM;*O)s6VmL!stdvVR>^N-kWctm29(QJV1$EohU{2 zokUTn@?LFdojx3^M+tr0th!x~i3Ju0E0Klx9kr3Heb;sdxNe6k$4Rl&Yz>^r5C-1D ztVNbQd<7HMoN*X9eYkUBKd0Dj;t)aNI zP=%4cKMW?k95?;j2Ii{=uD{t1+uVT~o-cp78;nO&MbJYf*#I|J z4uNLTPV$^2!|e9|vi%f=>DmJir^Y?|EZ3zfA!6(h=j|jEuIV>CCLFWUOte7IfCWhH zE5u%S#C%;%inFkR)h|d=KrWk4LsCq904v@*irB||%&fXdqDUVVQFqiCIjpmKsBC4;PsJvpz1X~cUd!lSe%-`Gjb9dLzm1d5HxEOuti9RN>NFUs_d zWug|$PqDfxut`t@b%lx25siF^-3&H?D~>NyHKC#g2VyBPE-sfk$IU?H^^XawnNDSF ziH1#sOLd5Zt*f*`|GbD|{C1ctm$wrKucy#%Mpd>e+fyf1iBRa(@png#t8(!O+%~`L zQx~!z9T$&I56Fw4@oDu;6sRP4Sx|heen%B&VUb_>{66%)efeytJQX~?sq9j1H^S=N zi)-5j$BjK_oFPYUtgyuz+|bsf0B*;dT0uh~d`>N!eJyhC3_F5cq#coZp4Y}|JD3Y% z)zFqm#;;*HU!-V@%{m=W1TKF06_aQ)o@-B#+a6kXE+!A`!ExpwjZ0y>!C&&Wbcgpx zA^Yp2_hmKdoags#a+4pk8S7elYHSg`PbQCpM)=yazY4(L`lvX8bZFSv?bbc6IjtN< zw%E{Je6FcFll}2TeV2XH7OHOj=ztkRH`Z^<)I#!zTa%hXmCWzXT*Ha^kUV+{@mhXw zdYk1lt#`Z_J8bZ3W0u{Rs4S>$^#sG(c7t@UT`Wfn)by^hm|8mJgV|-(WzjNU#(f*u zctOT?@tyYc6(_~KM2zKC*%14}+w!(?Z-rq z?xVf>;TE!EvC6z|%(Jhxa^En1iQ#pmJsZ5}n`?|a$?iLpD<-0t^{RHIW(H+!kHVh) zfbg|!Ro$n0?be{}qfy%srwRYW>V!KLlon0Pj~}JCQ+PX(a*k^wW(?dJDCJi{5N!ej zj!{BSz0TTTr|FZRXHlLuCXx<-Aj)WmUVQ>w2u(o;V_!A`ZLlMe`%nN zS{t66EN&HRDc^FHYxZl(x8}U_-mpCNO~SK)<%<`WYFs>kb4Q+Jr$CR(LoPr?cdk@V z=@6xJ%l-Be4<0lagyqe% zqDy=~r{dWcvTfoGi$vs!Q!$XwfF{~7e7SwPVqU3(*vK$?@~%=QFc`UdeY6(k%BQZ< zHS4fG{(iTQ5}+ft$VHv2eh$`A^Ww?JB8#o=sen|hDOb?Ogt7{NT~U;EgtLS*#d+tB zwX~WtuexN|VaYJ0er``NXnq_=T~za1tGXn4jXT6I7}VRK%-|YUN-kwH%KWIkGR0iR zS42ur3{*j0qdXKejQzyxYu}=W#<*!mjll;^NF~1#6joIVHOwwZDXt;l>h(`)X&I2{ z9ns;&X%Lzy$!jnxsb+$XF&}rir8rtbE?+ic`4$$uwt{=o2f?VP7=~QcPT=9Nayt;k zqSa+usIIg?XRU-A27hDT^cj~KL@M=p2L2h?_WXNq2cx2xuMH>|>41q8*&o93XIJOX zy8It)p5N8^UrnCC$g!U~GVsj*4Ee@p${jg6Uq|E{?KMb((L;bWYV~=6tu2GU|Aw_` z;t!MjC4mBk6`G5U&q0cq#Dh4tQ1j-EYy-2H;8aeS;%5>)GVl-XO@-rZ2E%Vxko-jR zx#lJd&|Rde4$OrUUPp~2&JDmQB~&SajA8I99%U~S-VOQQf1;67dBeAeD#99k{FrgF z?84;qZI>fjT7W(-{BCfJ1=UboG9d1Zpd(mucQEr%%IjUJF3`8Xdygn`wcZN+gyOgN z{}z{j`2JUk`QN;63z;mLfl`|~L8_M=6sxke&BTJ}ny%zM=HwNzFPF4+TWU|#GO4g_^2_p$}a ziyuO{<|x1N1)dZ^7Mf&YcMmc%2u`p|{`al*KH!X}NeiNim8BNQ5|oG$Hn0qP(*{uf zHTgmh5``Nd;k`IB>cl)8YnP&_5qe>*#kn)=b57;Kv*3DFMyUKt?j za5%TcXQ2zu<3^+7a}aAS0=hdY9xv{D&`O`OzWb%QIRA5|kU1u>5edAJ5J1s~0&G(n z*&50_*xCVAovnlMPlte`F#qdL0&jF!%*f9PF40?w$2Z$vhmAXVg$Ss`D#TILpjE6l zlPUd3i(n)^kz2J{LiB3tthe|3?){gSjM}Cd8v)fOLJPUXIN17mEF}{uB|Z+a3UaK6oJR(Iy>uSm@IZCO zzG{;$b2^(V3pYwSBF&|91wo`*REn8+HMjyz5Eho*j0u}MmFa#<^c&Bk8wrhvTv?^_ zx$JV|gB6Xty`2vPJ9cV*GFuHg*{4KEiB{WWgw58Wckh zE@00oir4V&y%&_KzT z#wh2=p;^6kkrZG0{vbQ@)HU`H&mHfpKyOWi8h-q-QDh^S@`0m6VEnbU04j627*WGL zFa`PhBWd=N(QP;|im8B6jQrOq*0;0!Ul0SM_s=aYrt4?EA9MzGgBWxgo>opBB&ed0 z`^sJI3qZ1km1??5UY&(}PU7Y&&w9pr>9Y&jceIYz(aNg9{l*0SMgfey4$+_B+hFpp zAB6c%PRG4N=~3=bwZ5r^kWt#_b@(WyVrzsMF&kTLDv?-ZyHFUjuyaIV(tD^BBjXXv zZRrFRS3}zRyID`h@=aVye}od&HE^4g3o>xha=z-wH)i73bzdXUgq%BUw*Jg6_$d8D zPZpj=Ic}8@o`+&D+o1VX%DmY@p(Dj!(Gsp*qluxmpLF52xX>!u)thDCaLJMg6#`sX zX6YNo$P+CiA7cGTr4Ynyh__+04dFw`EgT9n9dA4(*-0rVn)K-rH(Ol^m*Lv*ak;px zj}gR>G38KP6Y%o~EWo0lgj&p+vvkfl$V{>-zc=e z4R}TIhmU_%WiPG&Q}z6|1ps;}0f4`0pqJ+Vx!nBKJe1}y=6^0e^3o7M7X2Kb PL;~ysi6~3|^RNF0pR-I- literal 0 HcmV?d00001 diff --git a/samples/templates/32readwriteScatterChart9.xlsx b/samples/templates/32readwriteScatterChart9.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3a0b51b2f85429405f5d07484620113e44ffb455 GIT binary patch literal 12554 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

KR-z20Xg!(?ezsbIY$dW zrc}%URBH?l^bwEt>IawW%z8s~=zU&nRoNims0v&j!m{!5*-$onTpK1T(R7bKdvSW_ zk9mKFQ=u*#L)MePtczXl4@-iW7A#&FTT(x2+8PwzF2dv{3K+{& zz*t84Yb+bu+y5_=fie8&krDUlXC))#4E!1?@Z3$N~l`A$wJe8TBbZ_#wV zX@rqcIuv&Ks-)v+MVc_1T5qb5SmwD>n6j{Q#9-0CQ!hoqCzjvR3n{IIvJ3FAnTqF| z{4VnzT3Fx6V_rVQ$XUnbvg@NM6TiOaI-xey{9&t2CcEH+%%g!E0*z|I+A9PeioHCe zR)(|%z(KJS#a_uWo_({Kv97;N@wSA}8rh}DieHpe*#~t(JUC{VYsTmk9TQ(-!)TQ- zq&!H4$hoGdVU#uwrP(eKFDZ6X3W^p(dZf*Ew^u9h9Rzq>TsFsu;wV`1sBTFF9|tYL zW1ocDGtiL{$j+z+7{hw2e$*&jBY!cBjt!>K3jGM_yFHEZSo=i5-_T|c9&{%1hN093 z5mjb=v_7aL=hUO~^A+V0-ovZWo2)8fU$n`swWFOK(&yOkjppnW00eSJ4>CBggFK5(#n{ETk+%G29UK;;> zPUp8N2nZqEFXR6*weu3^<*deUBtwLM{}cZ)x$zR^We@o`3Nmoh2DZ(rN2!JasM!VSvP%&@UkNL8^IUv7s9{g)?bCzOO%&IsNX2lz_Qd& z-SV;|^%CKw%>9i}Oa2St|0jB10=`s^zX7W${_fa+(UC6!U#hX+fEAQ~`1n_C_R{+A z+UK_|2#7Zo2*`gZp_k@=-){bDE=Btn^FKEqMHxsSi+)ajB7^J$iHN}P^SA#8ZMLTE literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 70ff1b31..bd7082ff 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -61,6 +61,7 @@ class Axis extends Properties 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, 'textRotation' => null, + 'hidden' => null, ]; /** @@ -138,7 +139,8 @@ class Axis extends Properties ?string $maximum = null, ?string $majorUnit = null, ?string $minorUnit = null, - ?string $textRotation = null + ?string $textRotation = null, + ?string $hidden = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -151,6 +153,7 @@ class Axis extends Properties $this->setAxisOption('major_unit', $majorUnit); $this->setAxisOption('minor_unit', $minorUnit); $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); } /** diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 3f89e6fb..e850f502 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -144,6 +144,9 @@ class Chart /** @var bool */ private $autoTitleDeleted = false; + /** @var bool */ + private $noFill = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -747,4 +750,16 @@ class Chart return $this; } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php index 7f87e391..87f31020 100644 --- a/src/PhpSpreadsheet/Chart/ChartColor.php +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -24,15 +24,18 @@ class ChartColor /** @var ?int */ private $alpha; + /** @var ?int */ + private $brightness; + /** * @param string|string[] $value */ - public function __construct($value = '', ?int $alpha = null, ?string $type = null) + public function __construct($value = '', ?int $alpha = null, ?string $type = null, ?int $brightness = null) { if (is_array($value)) { $this->setColorPropertiesArray($value); } else { - $this->setColorProperties($value, $alpha, $type); + $this->setColorProperties($value, $alpha, $type, $brightness); } } @@ -72,10 +75,23 @@ class ChartColor return $this; } + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + /** * @param null|float|int|string $alpha + * @param null|float|int|string $brightness */ - public function setColorProperties(?string $color, $alpha = null, ?string $type = null): self + public function setColorProperties(?string $color, $alpha = null, ?string $type = null, $brightness = null): self { if (empty($type) && !empty($color)) { if (substr($color, 0, 1) === '*') { @@ -99,6 +115,11 @@ class ChartColor } elseif (is_numeric($alpha)) { $this->setAlpha((int) $alpha); } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } return $this; } @@ -108,7 +129,8 @@ class ChartColor return $this->setColorProperties( $color['value'] ?? '', $color['alpha'] ?? null, - $color['type'] ?? null + $color['type'] ?? null, + $color['brightness'] ?? null ); } @@ -133,6 +155,8 @@ class ChartColor $retVal = $this->type; } elseif ($propertyName === 'alpha') { $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; } return $retVal; diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index 548145e7..5d33e96d 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -94,7 +94,7 @@ class DataSeries private $plotCategory = []; /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index bc0e04d1..cb5fa742 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -536,7 +536,7 @@ class DataSeriesValues extends Properties } /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index 4bd49ece..ccde4bb2 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -6,6 +6,30 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PlotArea { + /** + * No fill in plot area (show Excel gridlines through chart). + * + * @var bool + */ + private $noFill = false; + + /** + * PlotArea Gradient Stop list. + * Each entry is a 2-element array. + * First is position in %. + * Second is ChartColor. + * + * @var array[] + */ + private $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + * + * @var ?float + */ + private $gradientFillAngle; + /** * PlotArea Layout. * @@ -101,4 +125,42 @@ class PlotArea $plotSeries->refresh($worksheet); } } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops() + { + return $this->gradientFillStops; + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index d49a5238..12ee0ade 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -73,8 +73,18 @@ class Chart $xAxis = new Axis(); $yAxis = new Axis(); $autoTitleDeleted = null; + $chartNoFill = false; + $gradientArray = []; + $gradientLin = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'spPr': + $possibleNoFill = $chartElementsC->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $chartNoFill = true; + } + + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetailsC = $chartDetails->children($this->cNamespace); @@ -95,8 +105,29 @@ class Chart $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; $catAxRead = false; + $plotNoFill = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + /** @var float */ + $pos = self::getAttribute($gradient, 'pos', 'float'); + $gradientArray[] = [ + $pos / Properties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = Properties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + } + + break; case 'layout': $plotAreaLayout = $this->chartLayoutDetails($chartDetail); @@ -288,6 +319,12 @@ class Chart } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } break; case 'plotVisOnly': @@ -330,6 +367,9 @@ class Chart } } $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); } @@ -1147,6 +1187,7 @@ class Chart 'type' => null, 'value' => null, 'alpha' => null, + 'brightness' => null, ]; foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { if (isset($colorXml->$type)) { @@ -1159,6 +1200,13 @@ class Chart $result['alpha'] = ChartColor::alphaFromXml($alpha); } } + if (isset($colorXml->$type->lumMod)) { + /** @var string */ + $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } break; } @@ -1236,6 +1284,9 @@ class Chart if (!isset($whichAxis)) { return; } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string')); + } if (isset($chartDetail->numFmt)) { $whichAxis->setAxisNumberProperties( (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index d9d96da6..278b64e7 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -106,11 +106,17 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:chart + if ($chart->getNoFill()) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + $objWriter->endElement(); // c:spPr + } $this->writePrintSettings($objWriter); - $objWriter->endElement(); + $objWriter->endElement(); // c:chartSpace // Return return $objWriter->getData(); @@ -360,8 +366,35 @@ class Chart extends WriterPart $this->writeSerAxis($objWriter, $id2, $id3); } } + $stops = $plotArea->getGradientFillStops(); + if ($plotArea->getNoFill() || !empty($stops)) { + $objWriter->startElement('c:spPr'); + if ($plotArea->getNoFill()) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + } + if (!empty($stops)) { + $objWriter->startElement('a:gradFill'); + $objWriter->startElement('a:gsLst'); + foreach ($stops as $stop) { + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0])); + $this->writeColor($objWriter, $stop[1], false); + $objWriter->endElement(); // a:gs + } + $objWriter->endElement(); // a:gsLst + $angle = $plotArea->getGradientFillAngle(); + if ($angle !== null) { + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', Properties::angleToXml($angle)); + $objWriter->endElement(); // a:lin + } + $objWriter->endElement(); // a:gradFill + } + $objWriter->endElement(); // c:spPr + } - $objWriter->endElement(); + $objWriter->endElement(); // c:plotArea } private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void @@ -492,7 +525,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -682,7 +715,7 @@ class Chart extends WriterPart $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); @@ -1612,7 +1645,18 @@ class Chart extends WriterPart if (is_numeric($alpha)) { $objWriter->startElement('a:alpha'); $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); - $objWriter->endElement(); + $objWriter->endElement(); // a:alpha + } + $brightness = $chartColor->getBrightness(); + if (is_numeric($brightness)) { + $brightness = (int) $brightness; + $lumOff = 100 - $brightness; + $objWriter->startElement('a:lumMod'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness)); + $objWriter->endElement(); // a:lumMod + $objWriter->startElement('a:lumOff'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); + $objWriter->endElement(); // a:lumOff } $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr if ($solidFill) { diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index 77b0a9b2..fcde7ae3 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -447,4 +448,88 @@ class Charts32ScatterTest extends AbstractFunctional $reloadedSpreadsheet->disconnectWorksheets(); } + + public function testScatter9(): void + { + // gradient testing + $file = self::DIRECTORY . '32readwriteScatterChart9.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertFalse($plotArea->getNoFill()); + self::assertEquals(315.0, $plotArea->getGradientFillAngle()); + $stops = $plotArea->getGradientFillStops(); + self::assertCount(3, $stops); + self::assertEquals(0.43808, $stops[0][0]); + self::assertEquals(0, $stops[1][0]); + self::assertEquals(0.91, $stops[2][0]); + $color = $stops[0][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('CDDBEC', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(20, $color->getBrightness()); + $color = $stops[1][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('FFC000', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertNull($color->getBrightness()); + $color = $stops[2][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('00B050', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(4, $color->getBrightness()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter10(): void + { + // nofill for Chart and PlotArea, hidden Axis + $file = self::DIRECTORY . '32readwriteScatterChart10.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertTrue($plotArea->getNoFill()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } } From e460c826067102c8d36e8c61fe5b5bf91532259f Mon Sep 17 00:00:00 2001 From: Jonathan Goode Date: Thu, 28 Jul 2022 03:29:02 +0100 Subject: [PATCH 061/156] Fully flatten an array (#2956) * Fully flatten an array * Provide test coverage for CONCAT combined with INDEX/MATCH --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Calculation/Functions.php | 22 +++++------- .../Calculation/ArrayTest.php | 34 +++++++++++++++++++ .../Functions/TextData/ConcatenateTest.php | 25 ++++++++++++++ 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/ArrayTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a2fd0cc1..7f07303a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing +- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) ## 1.24.1 - 2022-07-18 diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index ddd3e200..b3d6998c 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -573,24 +573,20 @@ class Functions return (array) $array; } - $arrayValues = []; - foreach ($array as $value) { + $flattened = []; + $stack = array_values($array); + + while ($stack) { + $value = array_shift($stack); + if (is_array($value)) { - foreach ($value as $val) { - if (is_array($val)) { - foreach ($val as $v) { - $arrayValues[] = $v; - } - } else { - $arrayValues[] = $val; - } - } + array_unshift($stack, ...array_values($value)); } else { - $arrayValues[] = $value; + $flattened[] = $value; } } - return $arrayValues; + return $flattened; } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php new file mode 100644 index 00000000..60c336cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php @@ -0,0 +1,34 @@ + [ + 0 => [ + 32 => [ + 'B' => 'PHP', + ], + ], + ], + 1 => [ + 0 => [ + 32 => [ + 'C' => 'Spreadsheet', + ], + ], + ], + ]; + + $values = Functions::flattenArray($array); + + self::assertIsNotArray($values[0]); + self::assertIsNotArray($values[1]); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php index 6c9a871d..31fb94fa 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; +use PhpOffice\PhpSpreadsheet\Spreadsheet; + class ConcatenateTest extends AllSetupTeardown { /** @@ -30,4 +32,27 @@ class ConcatenateTest extends AllSetupTeardown { return require 'tests/data/Calculation/TextData/CONCATENATE.php'; } + + public function testConcatenateWithIndexMatch(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->fromArray( + [ + ['Number', 'Formula'], + ['52101293', '=CONCAT(INDEX(Lookup!$B$2, MATCH($A2, Lookup!$A$2, 0)))'], + ] + ); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->fromArray( + [ + ['Lookup', 'Match'], + ['52101293', 'PHP'], + ] + ); + self::assertSame('PHP', $sheet1->getCell('B2')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } } From c0809b0c6cb2f8ad6e1ce359cfc44219ba7c841d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 28 Jul 2022 07:03:26 -0700 Subject: [PATCH 062/156] Fix Spreadsheet Copy, Disable Clone, Improve Coverage (#2951) * Fix Spreadsheet Copy, Disable Clone, Improve Coverage This PR was supposed to be merely to increase coverage in Spreadsheet. However, in doing so, I discovered that neither clone nor copy worked correctly. Neither had been covered in the test suite. Copy not only did not work, it broke the source spreadsheet as well. I tried to debug and got nowhere; I even tried using myclabs/deep-copy which is already in use in the test suite, but it failed as well. However, write and reload ought to work just fine for copy. It can't be used for clone; however, since copy does what clone ought to do, there's no reason why clone needs to be used, so __clone is changed to throw an exception if attempted. One other source change was needed, an obvious bug where an if condition uses 'or' when it should use 'and'. Also, one docblock declaration needed a change. Aside from that, the rest of this PR is test cases, and overall coverage passes 89% for the first time. * Clone is Okay After All But copy wasn't, changing it to just return clone. Perhaps save and reload will be needed instead at some point, but not yet. * An Error I Cannot Reproduce PHP8.1 unit test says error because GdImage can't be serialized. I can't reproduce this error on any of my test systems. I have no idea why GdImage is even involved. Using try/catch to see if it helps. * Weird Failures in Github I thought restoring clone was a good idea. That left me in a state where, after one change, copy/clone no longer worked on Github (unable to reproduce on any of my test systems). After a second change, copy worked but clone didn't, again unable to reproduce. So, reverting to original version - copy does save and reload, clone throws exception. --- phpstan-baseline.neon | 15 -- src/PhpSpreadsheet/Spreadsheet.php | 35 ++- tests/PhpSpreadsheetTests/DefinedNameTest.php | 12 + .../PhpSpreadsheetTests/NamedFormulaTest.php | 6 + tests/PhpSpreadsheetTests/NamedRangeTest.php | 6 + .../Reader/Xlsx/RibbonTest.php | 30 +++ .../SpreadsheetCoverageTest.php | 209 ++++++++++++++++++ tests/PhpSpreadsheetTests/Style/FontTest.php | 17 ++ 8 files changed, 297 insertions(+), 33 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5ef5522f..024f1fbd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2860,11 +2860,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Comparison operation \"\\<\\=\" between int\\ and 1000 is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 @@ -2875,21 +2870,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#" count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 33b4fe0c..4bb93987 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet { @@ -1120,28 +1123,24 @@ class Spreadsheet */ public function copy() { - $copied = clone $this; + $filename = File::temporaryFilename(); + $writer = new XlsxWriter($this); + $writer->setIncludeCharts(true); + $writer->save($filename); - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); - $this->workSheetCollection[$i]->rebindParent($this); - } + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $reloadedSpreadsheet = $reader->load($filename); + unlink($filename); - return $copied; + return $reloadedSpreadsheet; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ public function __clone() { - // @phpstan-ignore-next-line - foreach ($this as $key => $val) { - if (is_object($val) || (is_array($val))) { - $this->{$key} = unserialize(serialize($val)); - } - } + throw new Exception( + 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.' + ); } /** @@ -1562,7 +1561,7 @@ class Spreadsheet * Workbook window is hidden and cannot be shown in the * user interface. * - * @param string $visibility visibility status of the workbook + * @param null|string $visibility visibility status of the workbook */ public function setVisibility($visibility): void { @@ -1596,7 +1595,7 @@ class Spreadsheet */ public function setTabRatio($tabRatio): void { - if ($tabRatio >= 0 || $tabRatio <= 1000) { + if ($tabRatio >= 0 && $tabRatio <= 1000) { $this->tabRatio = (int) $tabRatio; } else { throw new Exception('Tab ratio must be between 0 and 1000.'); diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 43eddc8a..82950880 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -85,6 +85,18 @@ class DefinedNameTest extends TestCase self::assertCount(1, $this->spreadsheet->getDefinedNames()); } + public function testRemoveGlobalDefinedName(): void + { + $this->spreadsheet->addDefinedName( + DefinedName::createInstance('Any', $this->spreadsheet->getActiveSheet(), '=A1') + ); + self::assertCount(1, $this->spreadsheet->getDefinedNames()); + + $this->spreadsheet->removeDefinedName('Any'); + self::assertCount(0, $this->spreadsheet->getDefinedNames()); + $this->spreadsheet->removeDefinedName('Other'); + } + public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void { $this->spreadsheet->addDefinedName( diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 4c4a6b11..02e9d818 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -133,4 +133,10 @@ class NamedFormulaTest extends TestCase $formula->getValue() ); } + + public function testRemoveNonExistentNamedFormula(): void + { + self::assertCount(0, $this->spreadsheet->getNamedFormulae()); + $this->spreadsheet->removeNamedFormula('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index c72b7b73..402e7eba 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -133,4 +133,10 @@ class NamedRangeTest extends TestCase $range->getValue() ); } + + public function testRemoveNonExistentNamedRange(): void + { + self::assertCount(0, $this->spreadsheet->getNamedRanges()); + $this->spreadsheet->removeNamedRange('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php index 197ad47f..ab304e7b 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -44,4 +44,34 @@ class RibbonTest extends AbstractFunctional self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); $reloadedSpreadsheet->disconnectWorksheets(); } + + /** + * Same as above but discard macros. + */ + public function testDiscardMacros(): void + { + $filename = 'tests/data/Reader/XLSX/ribbon.donotopen.zip'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen($data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + $spreadsheet->discardMacros(); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertNull($reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php new file mode 100644 index 00000000..584c53fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -0,0 +1,209 @@ +getProperties(); + $properties->setCreator('Anyone'); + $properties->setTitle('Description'); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $spreadsheet2->getProperties()); + $properties2 = clone $properties; + $spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $spreadsheet2->getProperties()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testDocumentSecurity(): void + { + $spreadsheet = new Spreadsheet(); + $security = $spreadsheet->getSecurity(); + $security->setLockRevision(true); + $revisionsPassword = 'revpasswd'; + $security->setRevisionsPassword($revisionsPassword); + $spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $spreadsheet2->getSecurity()); + $security2 = clone $security; + $spreadsheet2->setSecurity($security2); + self::assertEquals($security, $spreadsheet2->getSecurity()); + $spreadsheet->disconnectWorksheets(); + $spreadsheet2->disconnectWorksheets(); + } + + public function testCellXfCollection(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $collection = $spreadsheet->getCellXfCollection(); + self::assertCount(4, $collection); + $font1Style = $collection[1]; + self::assertTrue($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + + $spreadsheet->removeCellXfByIndex(1); + self::assertFalse($spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $spreadsheet->removeCellXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveDefaultStyle(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('No default style found for this workbook'); + // Removing default style probably should be disallowed. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellXfByIndex(0); + $style = $spreadsheet->getDefaultStyle(); + $spreadsheet->disconnectWorksheets(); + } + + public function testCellStyleXF(): void + { + $spreadsheet = new Spreadsheet(); + $collection = $spreadsheet->getCellStyleXfCollection(); + self::assertCount(1, $collection); + $styleXf = $collection[0]; + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0)); + $hash = $styleXf->getHashCode(); + self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x')); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidRemoveCellStyleXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet->removeCellStyleXfByIndex(5); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidFirstSheetIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('First sheet index must be a positive integer.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setFirstSheetIndex(-1); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidVisibility(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Invalid visibility value.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility()); + $spreadsheet->setVisibility('badvalue'); + $spreadsheet->disconnectWorksheets(); + } + + public function testInvalidTabRatio(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); + $spreadsheet = new Spreadsheet(); + $spreadsheet->setTabRatio(2000); + $spreadsheet->disconnectWorksheets(); + } + + public function testCopy(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $sheet->getCell('A1')->setValue('this is a1'); + $sheet->getCell('A2')->setValue('this is a2'); + $sheet->getCell('A3')->setValue('this is a3'); + $sheet->getCell('B1')->setValue('this is b1'); + $sheet->getCell('B2')->setValue('this is b2'); + $copied = $spreadsheet->copy(); + $copysheet = $copied->getActiveSheet(); + $copysheet->getStyle('A2')->getFont()->setName('font12'); + $copysheet->getCell('A2')->setValue('this was a2'); + + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $sheet->getCell('A1')->getValue()); + self::assertSame('this is a2', $sheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $sheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $sheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $sheet->getCell('B2')->getValue()); + + self::assertSame('font1', $copysheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font12', $copysheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $copysheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $copysheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $copysheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $copysheet->getCell('A1')->getValue()); + self::assertSame('this was a2', $copysheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); + + $spreadsheet->disconnectWorksheets(); + $copied->disconnectWorksheets(); + } + + public function testClone(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); + $spreadsheet = new Spreadsheet(); + $clone = clone $spreadsheet; + $spreadsheet->disconnectWorksheets(); + $clone->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/FontTest.php b/tests/PhpSpreadsheetTests/Style/FontTest.php index 02814afa..6cd4d950 100644 --- a/tests/PhpSpreadsheetTests/Style/FontTest.php +++ b/tests/PhpSpreadsheetTests/Style/FontTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Style; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; use PHPUnit\Framework\TestCase; class FontTest extends TestCase @@ -88,4 +89,20 @@ class FontTest extends TestCase self::assertEquals('Calibri', $font->getName(), 'Null string changed to default'); $spreadsheet->disconnectWorksheets(); } + + public function testUnderlineHash(): void + { + $font1 = new Font(); + $font2 = new Font(); + $font2aHash = $font2->getHashCode(); + self::assertSame($font1->getHashCode(), $font2aHash); + $font2->setUnderlineColor( + [ + 'type' => 'srgbClr', + 'value' => 'FF0000', + ] + ); + $font2bHash = $font2->getHashCode(); + self::assertNotEquals($font1->getHashCode(), $font2bHash); + } } From 88bfa9829109e4b848d8ff71f0afe4161413ae05 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 18 Jul 2022 12:26:11 +0200 Subject: [PATCH 063/156] Initial Implementation of the new Excel TEXTBEFORE() and TEXTAFTER() functions --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 8 +- .../Calculation/TextData/Extract.php | 156 +++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php | 2 + .../Functions/TextData/TextAfterTest.php | 49 ++++ .../Functions/TextData/TextBeforeTest.php | 33 +++ tests/data/Calculation/TextData/TEXTAFTER.php | 215 ++++++++++++++++++ .../data/Calculation/TextData/TEXTBEFORE.php | 207 +++++++++++++++++ 8 files changed, 667 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php create mode 100644 tests/data/Calculation/TextData/TEXTAFTER.php create mode 100644 tests/data/Calculation/TextData/TEXTBEFORE.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f53aeda2..732badb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Nothing +- Implementation of the `TEXTBEFORE()` and `TEXTAFTER()` Excel Functions ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 5b1c5520..b73c7eaa 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2490,13 +2490,13 @@ class Calculation ], 'TEXTAFTER' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2-4', + 'functionCall' => [TextData\Extract::class, 'after'], + 'argumentCount' => '2-6', ], 'TEXTBEFORE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2-4', + 'functionCall' => [TextData\Extract::class, 'before'], + 'argumentCount' => '2-6', ], 'TEXTJOIN' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index d29f80ca..1a9e84db 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -4,6 +4,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Extract { @@ -95,4 +98,157 @@ class Extract return mb_substr($value ?? '', mb_strlen($value ?? '', 'UTF-8') - $chars, $chars, 'UTF-8'); } + + /** + * TEXTBEFORE. + * + * @param mixed $text the text that you're searching + * Or can be an array of values + * @param ?string $delimiter the text that marks the point before which you want to extract + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param mixed $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function before($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A') + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + $text = Helpers::extractString($text ?? ''); + $delimiter = Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')); + $instance = (int) $instance; + $matchMode = (int) $matchMode; + $matchEnd = (int) $matchEnd; + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_array($split) === false) { + return $split; + } + if ($delimiter === '') { + return ($instance > 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $adjust = preg_match('/^' . preg_quote($delimiter) . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, 0, max(count($split) - (abs($instance) * 2 - 1) - $adjust - $oddReverseAdjustment, 0)) + : array_slice($split, 0, $instance * 2 - 1 - $adjust); + + return implode('', $split); + } + + /** + * TEXTAFTER. + * + * @param mixed $text the text that you're searching + * @param ?string $delimiter the text that marks the point before which you want to extract + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param mixed $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function after($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A') + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + $text = Helpers::extractString($text ?? ''); + $delimiter = Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')); + $instance = (int) $instance; + $matchMode = (int) $matchMode; + $matchEnd = (int) $matchEnd; + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_array($split) === false) { + return $split; + } + if ($delimiter === '') { + return ($instance < 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $adjust = preg_match('/^' . preg_quote($delimiter) . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, count($split) - (abs($instance + 1) * 2) - $adjust - $oddReverseAdjustment) + : array_slice($split, $instance * 2 - $adjust); + + return implode('', $split); + } + + /** + * @param int $matchMode + * @param int $matchEnd + * @param mixed $ifNotFound + * + * @return string|string[] + */ + private static function validateTextBeforeAfter(string $text, string $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) + { + $flags = self::matchFlags($matchMode); + + if (preg_match('/' . preg_quote($delimiter) . "/{$flags}", $text) === 0 && $matchEnd === 0) { + return $ifNotFound; + } + + $split = preg_split('/(' . preg_quote($delimiter) . ")/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + if ($split === false) { + return ExcelError::NA(); + } + + if ($instance === 0 || abs($instance) > StringHelper::countCharacters($text)) { + return ExcelError::VALUE(); + } + + if ($matchEnd === 0 && (abs($instance) > floor(count($split) / 2))) { + return ExcelError::NA(); + } elseif ($matchEnd !== 0 && (abs($instance) - 1 > ceil(count($split) / 2))) { + return ExcelError::NA(); + } + + return $split; + } + + private static function matchFlags(int $matchMode): string + { + return ($matchMode === 0) ? 'mu' : 'miu'; + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php index 6fc0c66a..b623c573 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -144,6 +144,8 @@ class Xlfn . '|call' . '|let' . '|register[.]id' + . '|textafter' + . '|textbefore' . '|valuetotext' . ')(?=\\s*[(])/i'; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php new file mode 100644 index 00000000..b3b01d24 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php @@ -0,0 +1,49 @@ +getSheet(); + $worksheet->getCell('A1')->setValue($text); + $worksheet->getCell('A2')->setValue($delimiter); + $worksheet->getCell('B1')->setValue("=TEXTAFTER({$args})"); + + $result = $worksheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerTEXTAFTER(): array + { + return require 'tests/data/Calculation/TextData/TEXTAFTER.php'; + } + + public function testTextAfterWithArray(): void + { + $calculation = Calculation::getInstance(); + + $text = "Red Riding Hood's red riding hood"; + $delimiter = 'red'; + + $args = "\"{$text}\", \"{$delimiter}\", 1, {0;1}"; + + $formula = "=TEXTAFTER({$args})"; + $result = $calculation->_calculateFormulaValue($formula); + self::assertEquals([[' riding hood'], [" Riding Hood's red riding hood"]], $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php new file mode 100644 index 00000000..17938b5e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php @@ -0,0 +1,33 @@ +getSheet(); + $worksheet->getCell('A1')->setValue($text); + $worksheet->getCell('A2')->setValue($delimiter); + $worksheet->getCell('B1')->setValue("=TEXTBEFORE({$args})"); + + $result = $worksheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerTEXTBEFORE(): array + { + return require 'tests/data/Calculation/TextData/TEXTBEFORE.php'; + } +} diff --git a/tests/data/Calculation/TextData/TEXTAFTER.php b/tests/data/Calculation/TextData/TEXTAFTER.php new file mode 100644 index 00000000..c594ecdc --- /dev/null +++ b/tests/data/Calculation/TextData/TEXTAFTER.php @@ -0,0 +1,215 @@ + [ + "'s red hood", + [ + "Red riding hood's red hood", + 'hood', + ], + ], + 'END Case-sensitive Offset 2' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + 2, + ], + ], + 'END Case-sensitive Offset -1' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + -1, + ], + ], + 'END Case-sensitive Offset -2' => [ + "'s red hood", + [ + "Red riding hood's red hood", + 'hood', + -2, + ], + ], + 'END Case-sensitive Offset 3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + 3, + ], + ], + 'END Case-sensitive Offset -3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + -3, + ], + ], + 'END Case-sensitive Offset 3 with end' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + 3, + 0, + 1, + ], + ], + 'END Case-sensitive Offset -3 with end' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + 'hood', + -3, + 0, + 1, + ], + ], + 'END Case-sensitive - No Match' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'HOOD', + ], + ], + 'END Case-insensitive Offset 1' => [ + "'s red hood", + [ + "Red riding hood's red hood", + 'HOOD', + 1, + 1, + ], + ], + 'END Case-insensitive Offset 2' => [ + '', + [ + "Red riding hood's red hood", + 'HOOD', + 2, + 1, + ], + ], + 'END Offset 0' => [ + ExcelError::VALUE(), + [ + "Red riding hood's red hood", + 'hood', + 0, + ], + ], + 'Empty match positive' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + '', + ], + ], + 'Empty match negative' => [ + '', + [ + "Red riding hood's red hood", + '', + -1, + ], + ], + 'START Case-sensitive Offset 1' => [ + ' riding hood', + [ + "Red Riding Hood's red riding hood", + 'red', + ], + ], + 'START Case-insensitive Offset 1' => [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 1, + ], + ], + 'START Case-sensitive Offset -2' => [ + "Red Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 0, + 1, + ], + ], + 'START Case-insensitive Offset -2' => [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 1, + 1, + ], + ], + [ + ' riding hood', + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 0, + ], + ], + [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 1, + ], + ], + [ + "Red Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 0, + 1, + ], + ], + [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 1, + 1, + ], + ], + [ + ExcelError::NA(), + [ + 'Socrates', + ' ', + 1, + 0, + 0, + ], + ], + [ + '', + [ + 'Socrates', + ' ', + 1, + 0, + 1, + ], + ], +]; diff --git a/tests/data/Calculation/TextData/TEXTBEFORE.php b/tests/data/Calculation/TextData/TEXTBEFORE.php new file mode 100644 index 00000000..f94d5f28 --- /dev/null +++ b/tests/data/Calculation/TextData/TEXTBEFORE.php @@ -0,0 +1,207 @@ + [ + 'Red riding ', + [ + "Red riding hood's red hood", + 'hood', + ], + ], + 'END Case-sensitive Offset 2' => [ + "Red riding hood's red ", + [ + "Red riding hood's red hood", + 'hood', + 2, + ], + ], + 'END Case-sensitive Offset -1' => [ + "Red riding hood's red ", + [ + "Red riding hood's red hood", + 'hood', + -1, + ], + ], + 'END Case-sensitive Offset -2' => [ + 'Red riding ', + [ + "Red riding hood's red hood", + 'hood', + -2, + ], + ], + 'END Case-sensitive Offset 3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + 3, + ], + ], + 'END Case-sensitive Offset -3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + -3, + ], + ], + 'END Case-sensitive Offset 3 with end' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + 'hood', + 3, + 0, + 1, + ], + ], + 'END Case-sensitive Offset -3 with end' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + -3, + 0, + 1, + ], + ], + 'END Case-sensitive - No Match' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'HOOD', + ], + ], + 'END Case-insensitive Offset 1' => [ + 'Red riding ', + [ + "Red riding hood's red hood", + 'HOOD', + 1, + 1, + ], + ], + 'END Case-insensitive Offset 2' => [ + "Red riding hood's red ", + [ + "Red riding hood's red hood", + 'HOOD', + 2, + 1, + ], + ], + 'END Offset 0' => [ + ExcelError::VALUE(), + [ + "Red riding hood's red hood", + 'hood', + 0, + ], + ], + 'Empty match positive' => [ + '', + [ + "Red riding hood's red hood", + '', + ], + ], + 'Empty match negative' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + '', + -1, + ], + ], + 'START Case-sensitive Offset 1' => [ + "Red Riding Hood's ", + [ + "Red Riding Hood's red riding hood", + 'red', + ], + ], + 'START Case-insensitive Offset 1' => [ + '', + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 1, + ], + ], + 'START Case-sensitive Offset -2' => [ + '', + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 0, + 1, + ], + ], + 'START Case-insensitive Offset -2' => [ + '', + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 1, + 1, + ], + ], + [ + ExcelError::NA(), + [ + 'ABACADAEA', + 'A', + 6, + 0, + 0, + ], + ], + [ + 'ABACADAEA', + [ + 'ABACADAEA', + 'A', + 6, + 0, + 1, + ], + ], + [ + ExcelError::NA(), + [ + 'Socrates', + ' ', + 1, + 0, + 0, + ], + ], + [ + 'Socrates', + [ + 'Socrates', + ' ', + 1, + 0, + 1, + ], + ], + [ + 'Immanuel', + [ + 'Immanuel Kant', + ' ', + 1, + 0, + 1, + ], + ], +]; From 39df9c3bcce10f4a907ad23502c4dcf9c38a644a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Jul 2022 06:14:28 -0700 Subject: [PATCH 064/156] Fix Some Pdf Problems (#2960) * Fix Some Pdf Problems Fix #1747. No support for text rotation in Pdf. That issue actually has a decent workaround, but PhpSpreadsheet should handle it on its own. Mpdf requires the proprietary text-rotate css attribute; Html and Dompdf will use the CSS3 attribute transform:rotate. Fix #1713. Some paper-size values in PhpSpreadsheet are strings, some are 2-element float arrays. Dompdf accepts strings or 4-element float arrays, where the first 2 elements are always 0. Convert the PhpSpreadsheet array accordingly before passing it to Dompdf. Some tests had been disabled when Dompdf and Tcpdf were slow to achieve PHP8 compliance. They achieved it some time ago. Re-enable the tests. * Remove Tcpdf From One Test No problem with the other tests I added it in for. --- samples/Basic/26_Utf8.php | 10 ++- samples/Pdf/21b_Pdf.php | 24 +++---- src/PhpSpreadsheet/Writer/Html.php | 15 +++++ src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 3 + src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 3 + .../Functional/StreamTest.php | 3 +- .../Writer/Dompdf/PaperSizeArrayTest.php | 65 +++++++++++++++++++ .../Writer/Dompdf/TextRotationTest.php | 24 +++++++ .../Writer/Html/TextRotationTest.php | 24 +++++++ .../Writer/Mpdf/TextRotationTest.php | 24 +++++++ 10 files changed, 174 insertions(+), 21 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php diff --git a/samples/Basic/26_Utf8.php b/samples/Basic/26_Utf8.php index 52953251..52a64509 100644 --- a/samples/Basic/26_Utf8.php +++ b/samples/Basic/26_Utf8.php @@ -12,12 +12,10 @@ $spreadsheet = $reader->load(__DIR__ . '/../templates/26template.xlsx'); // at this point, we could do some manipulations with the template, but we skip this step $helper->write($spreadsheet, __FILE__, ['Xlsx', 'Xls', 'Html']); -if (\PHP_VERSION_ID < 80000) { - // Export to PDF (.pdf) - $helper->log('Write to PDF format'); - IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class); - $helper->write($spreadsheet, __FILE__, ['Pdf']); -} +// Export to PDF (.pdf) +$helper->log('Write to PDF format'); +IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class); +$helper->write($spreadsheet, __FILE__, ['Pdf']); // Remove first two rows with field headers before exporting to CSV $helper->log('Removing first two heading rows for CSV export'); diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index ad2f609b..c67ff3d2 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -32,13 +32,11 @@ $spreadsheet->getActiveSheet()->setShowGridLines(false); $helper->log('Set orientation to landscape'); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); -if (\PHP_VERSION_ID < 80000) { - $helper->log('Write to Dompdf'); - $writer = new Dompdf($spreadsheet); - $filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); - $writer->setEditHtmlCallback('replaceBody'); - $writer->save($filename); -} +$helper->log('Write to Dompdf'); +$writer = new Dompdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); $helper->log('Write to Mpdf'); $writer = new Mpdf($spreadsheet); @@ -46,10 +44,8 @@ $filename = $helper->getFileName('21b_Pdf_mpdf.xlsx', 'pdf'); $writer->setEditHtmlCallback('replaceBody'); $writer->save($filename); -if (\PHP_VERSION_ID < 80000) { - $helper->log('Write to Tcpdf'); - $writer = new Tcpdf($spreadsheet); - $filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); - $writer->setEditHtmlCallback('replaceBody'); - $writer->save($filename); -} +$helper->log('Write to Tcpdf'); +$writer = new Tcpdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index da32025a..6e51b7a8 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -126,6 +126,13 @@ class Html extends BaseWriter */ protected $isPdf = false; + /** + * Is the current writer creating mPDF? + * + * @var bool + */ + protected $isMPdf = false; + /** * Generate the Navigation block. * @@ -1003,6 +1010,14 @@ class Html extends BaseWriter $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; } } + $rotation = $alignment->getTextRotation(); + if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { + if ($this->isMPdf) { + $css['text-rotate'] = "$rotation"; + } else { + $css['transform'] = "rotate({$rotation}deg)"; + } + } return $css; } diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index fc96f904..bf9e28cb 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -35,6 +35,9 @@ class Dompdf extends Pdf $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize(); $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault(); + if (is_array($paperSize) && count($paperSize) === 2) { + $paperSize = [0.0, 0.0, $paperSize[0], $paperSize[1]]; + } $orientation = ($orientation == 'L') ? 'landscape' : 'portrait'; diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 281e1a4f..d0ce9ed4 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -8,6 +8,9 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Mpdf extends Pdf { + /** @var bool */ + protected $isMPdf = true; + /** * Gets the implementation of external PDF library that should be used. * diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php index 3911aaa6..a84a2490 100644 --- a/tests/PhpSpreadsheetTests/Functional/StreamTest.php +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -17,12 +17,13 @@ class StreamTest extends TestCase ['Csv'], ['Html'], ['Mpdf'], + ['Dompdf'], ]; if (\PHP_VERSION_ID < 80000) { $providerFormats = array_merge( $providerFormats, - [['Tcpdf'], ['Dompdf']] + [['Tcpdf']] ); } diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php new file mode 100644 index 00000000..815d7e3d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php @@ -0,0 +1,65 @@ +outfile !== '') { + unlink($this->outfile); + $this->outfile = ''; + } + } + + public function testPaperSizeArray(): void + { + // Issue 1713 - array in PhpSpreadsheet is 2 elements, + // but in Dompdf it is 4 elements, first 2 are zero. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + // TABLOID is a 2-element array in Writer/Pdf.php $paperSizes + $size = PageSetup::PAPERSIZE_TABLOID; + $sheet->getPageSetup()->setPaperSize($size); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + $contents = file_get_contents($this->outfile); + self::assertNotFalse($contents); + self::assertStringContainsString('/MediaBox [0.000 0.000 792.000 1224.000]', $contents); + } + + public function testPaperSizeNotArray(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + // LETTER is a string in Writer/Pdf.php $paperSizes + $size = PageSetup::PAPERSIZE_LETTER; + $sheet->getPageSetup()->setPaperSize($size); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + $contents = file_get_contents($this->outfile); + self::assertNotFalse($contents); + self::assertStringContainsString('/MediaBox [0.000 0.000 612.000 792.000]', $contents); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php new file mode 100644 index 00000000..2aa9d5ed --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' transform:rotate(90deg);', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php new file mode 100644 index 00000000..da928457 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Html($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' transform:rotate(90deg);', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php new file mode 100644 index 00000000..000a33b4 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Mpdf/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Mpdf($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' text-rotate:90;', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} From 641b6d0ccb8aa3f5265a2382db19c8c81d5eb1be Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 29 Jul 2022 07:11:37 -0700 Subject: [PATCH 065/156] Improve Coverage for Shared/Font (#2961) Shared/Font is hardly covered in unit tests (as opposed to Style/Font which is completely covered). And it presented some good opportunities for code optimization. I wrote and tested the new unit tests first, then optimized the code and confirmed that everything still works. There is still a bit of a gap with "exact" measurements. I had tests ready, but had to withdraw them when I discovered they weren't quite portable (see https://github.com/php/php-src/issues/9073). --- phpstan-baseline.neon | 45 -- src/PhpSpreadsheet/Shared/Font.php | 538 +++++++----------- .../Reader/Xls/Rc4Test.php | 21 + .../PhpSpreadsheetTests/Shared/Font2Test.php | 149 +++++ .../PhpSpreadsheetTests/Shared/Font3Test.php | 53 ++ 5 files changed, 424 insertions(+), 382 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php create mode 100644 tests/PhpSpreadsheetTests/Shared/Font2Test.php create mode 100644 tests/PhpSpreadsheetTests/Shared/Font3Test.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 024f1fbd..49ab167e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2270,51 +2270,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php - - - message: "#^Cannot access offset 0 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 2 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 4 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 6 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Parameter \\#1 \\$size of function imagettfbbox expects float, float\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Parameter \\#2 \\$defaultFont of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pixelsToCellDimension\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:\\$autoSizeMethods has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Variable \\$cellText on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\EigenvalueDecomposition\\:\\:\\$cdivi has no type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 1adf213e..33796b4c 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -13,7 +13,7 @@ class Font const AUTOSIZE_METHOD_APPROX = 'approx'; const AUTOSIZE_METHOD_EXACT = 'exact'; - private static $autoSizeMethods = [ + private const AUTOSIZE_METHODS = [ self::AUTOSIZE_METHOD_APPROX, self::AUTOSIZE_METHOD_EXACT, ]; @@ -101,6 +101,105 @@ class Font const VERDANA_ITALIC = 'verdanai.ttf'; const VERDANA_BOLD_ITALIC = 'verdanaz.ttf'; + const FONT_FILE_NAMES = [ + 'Arial' => [ + 'x' => self::ARIAL, + 'xb' => self::ARIAL_BOLD, + 'xi' => self::ARIAL_ITALIC, + 'xbi' => self::ARIAL_BOLD_ITALIC, + ], + 'Calibri' => [ + 'x' => self::CALIBRI, + 'xb' => self::CALIBRI_BOLD, + 'xi' => self::CALIBRI_ITALIC, + 'xbi' => self::CALIBRI_BOLD_ITALIC, + ], + 'Comic Sans MS' => [ + 'x' => self::COMIC_SANS_MS, + 'xb' => self::COMIC_SANS_MS_BOLD, + 'xi' => self::COMIC_SANS_MS, + 'xbi' => self::COMIC_SANS_MS_BOLD, + ], + 'Courier New' => [ + 'x' => self::COURIER_NEW, + 'xb' => self::COURIER_NEW_BOLD, + 'xi' => self::COURIER_NEW_ITALIC, + 'xbi' => self::COURIER_NEW_BOLD_ITALIC, + ], + 'Georgia' => [ + 'x' => self::GEORGIA, + 'xb' => self::GEORGIA_BOLD, + 'xi' => self::GEORGIA_ITALIC, + 'xbi' => self::GEORGIA_BOLD_ITALIC, + ], + 'Impact' => [ + 'x' => self::IMPACT, + 'xb' => self::IMPACT, + 'xi' => self::IMPACT, + 'xbi' => self::IMPACT, + ], + 'Liberation Sans' => [ + 'x' => self::LIBERATION_SANS, + 'xb' => self::LIBERATION_SANS_BOLD, + 'xi' => self::LIBERATION_SANS_ITALIC, + 'xbi' => self::LIBERATION_SANS_BOLD_ITALIC, + ], + 'Lucida Console' => [ + 'x' => self::LUCIDA_CONSOLE, + 'xb' => self::LUCIDA_CONSOLE, + 'xi' => self::LUCIDA_CONSOLE, + 'xbi' => self::LUCIDA_CONSOLE, + ], + 'Lucida Sans Unicode' => [ + 'x' => self::LUCIDA_SANS_UNICODE, + 'xb' => self::LUCIDA_SANS_UNICODE, + 'xi' => self::LUCIDA_SANS_UNICODE, + 'xbi' => self::LUCIDA_SANS_UNICODE, + ], + 'Microsoft Sans Serif' => [ + 'x' => self::MICROSOFT_SANS_SERIF, + 'xb' => self::MICROSOFT_SANS_SERIF, + 'xi' => self::MICROSOFT_SANS_SERIF, + 'xbi' => self::MICROSOFT_SANS_SERIF, + ], + 'Palatino Linotype' => [ + 'x' => self::PALATINO_LINOTYPE, + 'xb' => self::PALATINO_LINOTYPE_BOLD, + 'xi' => self::PALATINO_LINOTYPE_ITALIC, + 'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC, + ], + 'Symbol' => [ + 'x' => self::SYMBOL, + 'xb' => self::SYMBOL, + 'xi' => self::SYMBOL, + 'xbi' => self::SYMBOL, + ], + 'Tahoma' => [ + 'x' => self::TAHOMA, + 'xb' => self::TAHOMA_BOLD, + 'xi' => self::TAHOMA, + 'xbi' => self::TAHOMA_BOLD, + ], + 'Times New Roman' => [ + 'x' => self::TIMES_NEW_ROMAN, + 'xb' => self::TIMES_NEW_ROMAN_BOLD, + 'xi' => self::TIMES_NEW_ROMAN_ITALIC, + 'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC, + ], + 'Trebuchet MS' => [ + 'x' => self::TREBUCHET_MS, + 'xb' => self::TREBUCHET_MS_BOLD, + 'xi' => self::TREBUCHET_MS_ITALIC, + 'xbi' => self::TREBUCHET_MS_BOLD_ITALIC, + ], + 'Verdana' => [ + 'x' => self::VERDANA, + 'xb' => self::VERDANA_BOLD, + 'xi' => self::VERDANA_ITALIC, + 'xbi' => self::VERDANA_BOLD_ITALIC, + ], + ]; + /** * AutoSize method. * @@ -113,54 +212,65 @@ class Font * * @var string */ - private static $trueTypeFontPath; + private static $trueTypeFontPath = ''; /** * How wide is a default column for a given default font and size? * Empirical data found by inspecting real Excel files and reading off the pixel width * in Microsoft Office Excel 2007. + * Added height in points. + */ + public const DEFAULT_COLUMN_WIDTHS = [ + 'Arial' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + 11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5], + 9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25], + 10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75], + ], + ]; + + /** + * List of column widths. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use DEFAULT_COLUMN_WIDTHS constant instead. * * @var array */ - public static $defaultColumnWidths = [ - 'Arial' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 64, 'width' => 9.14062500], - 10 => ['px' => 64, 'width' => 9.14062500], - ], - 'Calibri' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 56, 'width' => 9.33203125], - 10 => ['px' => 64, 'width' => 9.14062500], - 11 => ['px' => 64, 'width' => 9.14062500], - ], - 'Verdana' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 64, 'width' => 9.14062500], - 9 => ['px' => 72, 'width' => 9.00000000], - 10 => ['px' => 72, 'width' => 9.00000000], - ], - ]; + public static $defaultColumnWidths = self::DEFAULT_COLUMN_WIDTHS; /** * Set autoSize method. @@ -171,7 +281,7 @@ class Font */ public static function setAutoSizeMethod($method) { - if (!in_array($method, self::$autoSizeMethods)) { + if (!in_array($method, self::AUTOSIZE_METHODS)) { return false; } self::$autoSizeMethod = $method; @@ -219,7 +329,7 @@ class Font * Calculate an (approximate) OpenXML column width, based on font size and text contained. * * @param FontStyle $font Font object - * @param RichText|string $cellText Text to calculate width + * @param null|RichText|string $cellText Text to calculate width * @param int $rotation Rotation angle * @param null|FontStyle $defaultFont Font object * @param bool $filterAdjustment Add space for Autofilter or Table dropdown @@ -238,7 +348,8 @@ class Font } // Special case if there are one or more newline characters ("\n") - if (strpos($cellText ?? '', "\n") !== false) { + $cellText = $cellText ?? ''; + if (strpos($cellText, "\n") !== false) { $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { @@ -281,7 +392,7 @@ class Font } // Convert from pixel width to column width - $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont); + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle()); // Return return (int) round($columnWidth, 6); @@ -299,7 +410,12 @@ class Font // font size should really be supplied in pixels in GD2, // but since GD2 seems to assume 72dpi, pixels and points are the same $fontFile = self::getTrueTypeFontFileFromFont($font); - $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text); + $textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text); + if ($textBox === false) { + // @codeCoverageIgnoreStart + throw new PhpSpreadsheetException('imagettfbbox failed'); + // @codeCoverageIgnoreEnd + } // Get corners positions $lowerLeftCornerX = $textBox[0]; @@ -409,129 +525,48 @@ class Font * * @return string Path to TrueType font file */ - public static function getTrueTypeFontFileFromFont(FontStyle $font) + public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true) { - if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) { + if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) { throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); } $name = $font->getName(); + if (!isset(self::FONT_FILE_NAMES[$name])) { + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + } $bold = $font->getBold(); $italic = $font->getItalic(); - - // Check if we can map font to true type font file - switch ($name) { - case 'Arial': - $fontFile = ( - $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) - : ($italic ? self::ARIAL_ITALIC : self::ARIAL) - ); - - break; - case 'Calibri': - $fontFile = ( - $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD) - : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI) - ); - - break; - case 'Courier New': - $fontFile = ( - $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD) - : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW) - ); - - break; - case 'Comic Sans MS': - $fontFile = ( - $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS - ); - - break; - case 'Georgia': - $fontFile = ( - $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) - : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) - ); - - break; - case 'Impact': - $fontFile = self::IMPACT; - - break; - case 'Liberation Sans': - $fontFile = ( - $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) - : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) - ); - - break; - case 'Lucida Console': - $fontFile = self::LUCIDA_CONSOLE; - - break; - case 'Lucida Sans Unicode': - $fontFile = self::LUCIDA_SANS_UNICODE; - - break; - case 'Microsoft Sans Serif': - $fontFile = self::MICROSOFT_SANS_SERIF; - - break; - case 'Palatino Linotype': - $fontFile = ( - $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) - : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) - ); - - break; - case 'Symbol': - $fontFile = self::SYMBOL; - - break; - case 'Tahoma': - $fontFile = ( - $bold ? self::TAHOMA_BOLD : self::TAHOMA - ); - - break; - case 'Times New Roman': - $fontFile = ( - $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD) - : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN) - ); - - break; - case 'Trebuchet MS': - $fontFile = ( - $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD) - : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS) - ); - - break; - case 'Verdana': - $fontFile = ( - $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) - : ($italic ? self::VERDANA_ITALIC : self::VERDANA) - ); - - break; - default: - throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); - - break; + $index = 'x'; + if ($bold) { + $index .= 'b'; } + if ($italic) { + $index .= 'i'; + } + $fontFile = self::FONT_FILE_NAMES[$name][$index]; - $fontFile = self::$trueTypeFontPath . $fontFile; + $separator = ''; + if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') { + $separator = DIRECTORY_SEPARATOR; + } + $fontFile = self::$trueTypeFontPath . $separator . $fontFile; // Check if file actually exists - if (!file_exists($fontFile)) { + if ($checkPath && !file_exists($fontFile)) { throw new PhpSpreadsheetException('TrueType Font file not found'); } return $fontFile; } + public const CHARSET_FROM_FONT_NAME = [ + 'EucrosiaUPC' => self::CHARSET_ANSI_THAI, + 'Wingdings' => self::CHARSET_SYMBOL, + 'Wingdings 2' => self::CHARSET_SYMBOL, + 'Wingdings 3' => self::CHARSET_SYMBOL, + ]; + /** * Returns the associated charset for the font name. * @@ -541,19 +576,7 @@ class Font */ public static function getCharsetFromFontName($fontName) { - switch ($fontName) { - // Add more cases. Check FONT records in real Excel files. - case 'EucrosiaUPC': - return self::CHARSET_ANSI_THAI; - case 'Wingdings': - return self::CHARSET_SYMBOL; - case 'Wingdings 2': - return self::CHARSET_SYMBOL; - case 'Wingdings 3': - return self::CHARSET_SYMBOL; - default: - return self::CHARSET_ANSI_LATIN; - } + return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN; } /** @@ -567,17 +590,17 @@ class Font */ public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false) { - if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) { + if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()])) { // Exact width can be determined $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] - : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['px'] + : self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['width']; } else { // We don't have data for this particular font and size, use approximation by // extrapolating from Calibri 11 $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths['Calibri'][11]['px'] - : self::$defaultColumnWidths['Calibri'][11]['width']; + self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] + : self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width']; $columnWidth = $columnWidth * $font->getSize() / 11; // Round pixels to closest integer @@ -599,173 +622,14 @@ class Font */ public static function getDefaultRowHeightByFont(FontStyle $font) { - switch ($font->getName()) { - case 'Arial': - switch ($font->getSize()) { - case 10: - // inspection of Arial 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Arial 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Arial 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Arial 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Arial 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Arial 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Arial 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Arial 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Arial 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - case 'Calibri': - switch ($font->getSize()) { - case 11: - // inspection of Calibri 11 workbook says 15.00pt ~20px - $rowHeight = 15; - - break; - case 10: - // inspection of Calibri 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Calibri 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Calibri 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Calibri 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Calibri 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Calibri 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Calibri 3 workbook says 6.00pt ~8px - $rowHeight = 6.00; - - break; - case 2: - case 1: - // inspection of Calibri 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Calibri 11 workbook as an approximation, extrapolation - $rowHeight = 15 * $font->getSize() / 11; - - break; - } - - break; - case 'Verdana': - switch ($font->getSize()) { - case 10: - // inspection of Verdana 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Verdana 9 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 8: - // inspection of Verdana 8 workbook says 10.50pt ~14px - $rowHeight = 10.50; - - break; - case 7: - // inspection of Verdana 7 workbook says 9.00pt ~12px - $rowHeight = 9.00; - - break; - case 6: - case 5: - // inspection of Verdana 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Verdana 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Verdana 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Verdana 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Verdana 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - default: - // just use Calibri as an approximation - $rowHeight = 15 * $font->getSize() / 11; - - break; + $name = $font->getName(); + $size = $font->getSize(); + if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$size])) { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$size]['height']; + } elseif ($name === 'Arial' || $name === 'Verdana') { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0; + } else { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0; } return $rowHeight; diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php b/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php new file mode 100644 index 00000000..c3056c08 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php @@ -0,0 +1,21 @@ +RC4($string)); + $expectedResult = '2ac2fecdd8fbb84638e3a4820eb205cc8e29c28b9d5d6b2ef974f311964971c90e8b9ca16467ef2dc6fc3520'; + self::assertSame($expectedResult, $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Font2Test.php b/tests/PhpSpreadsheetTests/Shared/Font2Test.php new file mode 100644 index 00000000..4bb247cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Font2Test.php @@ -0,0 +1,149 @@ +providerCharsetFromFontName(); + foreach ($tests as $test) { + $thisTest = $test[0]; + if (array_key_exists($thisTest, $covered)) { + $covered[$thisTest] = 1; + } else { + $defaultCovered = true; + } + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "FontName $key not tested"); + } + self::assertTrue($defaultCovered, 'Default key not tested'); + } + + public function providerCharsetFromFontName(): array + { + return [ + ['EucrosiaUPC', Font::CHARSET_ANSI_THAI], + ['Wingdings', Font::CHARSET_SYMBOL], + ['Wingdings 2', Font::CHARSET_SYMBOL], + ['Wingdings 3', Font::CHARSET_SYMBOL], + ['Default', Font::CHARSET_ANSI_LATIN], + ]; + } + + public function testColumnWidths(): void + { + $widths = Font::DEFAULT_COLUMN_WIDTHS; + $fontNames = ['Arial', 'Calibri', 'Verdana']; + $font = new StyleFont(); + foreach ($fontNames as $fontName) { + $font->setName($fontName); + $array = $widths[$fontName]; + foreach ($array as $points => $array2) { + $font->setSize($points); + $px = $array2['px']; + $width = $array2['width']; + self::assertEquals($px, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals($width, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + } + } + $pxCalibri11 = $widths['Calibri'][11]['px']; + $widthCalibri11 = $widths['Calibri'][11]['width']; + $fontName = 'unknown'; + $points = 11; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals($pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals($widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + $points = 22; + $font->setSize($points); + self::assertEquals(2 * $pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals(2 * $widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + $fontName = 'Arial'; + $points = 33; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals(3 * $widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + } + + public function testRowHeights(): void + { + $heights = Font::DEFAULT_COLUMN_WIDTHS; + $fontNames = ['Arial', 'Calibri', 'Verdana']; + $font = new StyleFont(); + foreach ($fontNames as $fontName) { + $font->setName($fontName); + $array = $heights[$fontName]; + foreach ($array as $points => $array2) { + $font->setSize($points); + $height = $array2['height']; + self::assertEquals($height, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + } + } + $heightArial10 = $heights['Arial'][10]['height']; + $fontName = 'Arial'; + $points = 20; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(2 * $heightArial10, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $heightVerdana10 = $heights['Verdana'][10]['height']; + $fontName = 'Verdana'; + $points = 30; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $heightVerdana10, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $heightCalibri11 = $heights['Calibri'][11]['height']; + $fontName = 'Calibri'; + $points = 22; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(2 * $heightCalibri11, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $fontName = 'unknown'; + $points = 33; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $heightCalibri11, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + } + + public function testGetTrueTypeFontFileFromFont(): void + { + $fileNames = Font::FONT_FILE_NAMES; + $font = new StyleFont(); + foreach ($fileNames as $fontName => $fontNameArray) { + $font->setName($fontName); + $font->setBold(false); + $font->setItalic(false); + self::assertSame($fileNames[$fontName]['x'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName not bold not italic"); + $font->setBold(true); + $font->setItalic(false); + self::assertSame($fileNames[$fontName]['xb'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName bold not italic"); + $font->setBold(false); + $font->setItalic(true); + self::assertSame($fileNames[$fontName]['xi'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName not bold italic"); + $font->setBold(true); + $font->setItalic(true); + self::assertSame($fileNames[$fontName]['xbi'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName bold italic"); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Font3Test.php b/tests/PhpSpreadsheetTests/Shared/Font3Test.php new file mode 100644 index 00000000..e91e7acf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Font3Test.php @@ -0,0 +1,53 @@ +holdDirectory = Font::getTrueTypeFontPath(); + } + + protected function tearDown(): void + { + Font::setTrueTypeFontPath($this->holdDirectory); + } + + public function testGetTrueTypeException1(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Valid directory to TrueType Font files not specified'); + $font = new StyleFont(); + $font->setName('unknown'); + Font::getTrueTypeFontFileFromFont($font); + } + + public function testGetTrueTypeException2(): void + { + Font::setTrueTypeFontPath(__DIR__); + $this->expectException(SSException::class); + $this->expectExceptionMessage('Unknown font name'); + $font = new StyleFont(); + $font->setName('unknown'); + Font::getTrueTypeFontFileFromFont($font); + } + + public function testGetTrueTypeException3(): void + { + Font::setTrueTypeFontPath(__DIR__); + $this->expectException(SSException::class); + $this->expectExceptionMessage('TrueType Font file not found'); + $font = new StyleFont(); + $font->setName('Calibri'); + Font::getTrueTypeFontFileFromFont($font); + } +} From 290d0731fe7c9508b9226c0e502a4768eee646b1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 30 Jul 2022 10:27:31 +0200 Subject: [PATCH 066/156] Allow multiple delimiters for `TEXTBEFORE()` and `TEXTAFTER()` functions --- .../Calculation/TextData/Extract.php | 48 ++++++++++++++----- .../Functions/TextData/TextAfterTest.php | 23 ++------- .../Functions/TextData/TextBeforeTest.php | 7 ++- tests/data/Calculation/TextData/TEXTAFTER.php | 36 ++++++++++++++ .../data/Calculation/TextData/TEXTBEFORE.php | 36 ++++++++++++++ 5 files changed, 119 insertions(+), 31 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 1a9e84db..ee7e31b7 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -104,7 +104,8 @@ class Extract * * @param mixed $text the text that you're searching * Or can be an array of values - * @param ?string $delimiter the text that marks the point before which you want to extract + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values * @param mixed $instance The instance of the delimiter after which you want to extract the text. * By default, this is the first instance (1). * A negative value means start searching from the end of the text string. @@ -132,7 +133,6 @@ class Extract } $text = Helpers::extractString($text ?? ''); - $delimiter = Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')); $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; @@ -141,13 +141,14 @@ class Extract if (is_array($split) === false) { return $split; } - if ($delimiter === '') { + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { return ($instance > 0) ? '' : $text; } // Adjustment for a match as the first element of the split $flags = self::matchFlags($matchMode); - $adjust = preg_match('/^' . preg_quote($delimiter) . "\$/{$flags}", $split[0]); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); $oddReverseAdjustment = count($split) % 2; $split = ($instance < 0) @@ -161,7 +162,8 @@ class Extract * TEXTAFTER. * * @param mixed $text the text that you're searching - * @param ?string $delimiter the text that marks the point before which you want to extract + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values * @param mixed $instance The instance of the delimiter after which you want to extract the text. * By default, this is the first instance (1). * A negative value means start searching from the end of the text string. @@ -189,7 +191,6 @@ class Extract } $text = Helpers::extractString($text ?? ''); - $delimiter = Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')); $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; @@ -198,13 +199,14 @@ class Extract if (is_array($split) === false) { return $split; } - if ($delimiter === '') { + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { return ($instance < 0) ? '' : $text; } // Adjustment for a match as the first element of the split $flags = self::matchFlags($matchMode); - $adjust = preg_match('/^' . preg_quote($delimiter) . "\$/{$flags}", $split[0]); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); $oddReverseAdjustment = count($split) % 2; $split = ($instance < 0) @@ -215,21 +217,23 @@ class Extract } /** + * @param null|array|string $delimiter * @param int $matchMode * @param int $matchEnd * @param mixed $ifNotFound * * @return string|string[] */ - private static function validateTextBeforeAfter(string $text, string $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) + private static function validateTextBeforeAfter(string $text, $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) { $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); - if (preg_match('/' . preg_quote($delimiter) . "/{$flags}", $text) === 0 && $matchEnd === 0) { + if (preg_match('/' . $delimiter . "/{$flags}", $text) === 0 && $matchEnd === 0) { return $ifNotFound; } - $split = preg_split('/(' . preg_quote($delimiter) . ")/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $split = preg_split('/' . $delimiter . "/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); if ($split === false) { return ExcelError::NA(); } @@ -247,6 +251,28 @@ class Extract return $split; } + /** + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + if (is_array($delimiter)) { + $delimiter = Functions::flattenArray($delimiter); + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $delimiter + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote($delimiter ?? '') . ')'; + } + private static function matchFlags(int $matchMode): string { return ($matchMode === 0) ? 'mu' : 'miu'; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php index b3b01d24..00483260 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php @@ -2,8 +2,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; - class TextAfterTest extends AllSetupTeardown { /** @@ -14,14 +12,17 @@ class TextAfterTest extends AllSetupTeardown $text = $arguments[0]; $delimiter = $arguments[1]; - $args = 'A1, A2'; + $args = (is_array($delimiter)) ? 'A1, {A2,A3}' : 'A1, A2'; $args .= (isset($arguments[2])) ? ", {$arguments[2]}" : ','; $args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ','; $args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ','; $worksheet = $this->getSheet(); $worksheet->getCell('A1')->setValue($text); - $worksheet->getCell('A2')->setValue($delimiter); + $worksheet->getCell('A2')->setValue((is_array($delimiter)) ? $delimiter[0] : $delimiter); + if (is_array($delimiter)) { + $worksheet->getCell('A3')->setValue($delimiter[1]); + } $worksheet->getCell('B1')->setValue("=TEXTAFTER({$args})"); $result = $worksheet->getCell('B1')->getCalculatedValue(); @@ -32,18 +33,4 @@ class TextAfterTest extends AllSetupTeardown { return require 'tests/data/Calculation/TextData/TEXTAFTER.php'; } - - public function testTextAfterWithArray(): void - { - $calculation = Calculation::getInstance(); - - $text = "Red Riding Hood's red riding hood"; - $delimiter = 'red'; - - $args = "\"{$text}\", \"{$delimiter}\", 1, {0;1}"; - - $formula = "=TEXTAFTER({$args})"; - $result = $calculation->_calculateFormulaValue($formula); - self::assertEquals([[' riding hood'], [" Riding Hood's red riding hood"]], $result); - } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php index 17938b5e..37e46636 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php @@ -12,14 +12,17 @@ class TextBeforeTest extends AllSetupTeardown $text = $arguments[0]; $delimiter = $arguments[1]; - $args = 'A1, A2'; + $args = (is_array($delimiter)) ? 'A1, {A2,A3}' : 'A1, A2'; $args .= (isset($arguments[2])) ? ", {$arguments[2]}" : ','; $args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ','; $args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ','; $worksheet = $this->getSheet(); $worksheet->getCell('A1')->setValue($text); - $worksheet->getCell('A2')->setValue($delimiter); + $worksheet->getCell('A2')->setValue((is_array($delimiter)) ? $delimiter[0] : $delimiter); + if (is_array($delimiter)) { + $worksheet->getCell('A3')->setValue($delimiter[1]); + } $worksheet->getCell('B1')->setValue("=TEXTBEFORE({$args})"); $result = $worksheet->getCell('B1')->getCalculatedValue(); diff --git a/tests/data/Calculation/TextData/TEXTAFTER.php b/tests/data/Calculation/TextData/TEXTAFTER.php index c594ecdc..ebcfecb9 100644 --- a/tests/data/Calculation/TextData/TEXTAFTER.php +++ b/tests/data/Calculation/TextData/TEXTAFTER.php @@ -212,4 +212,40 @@ return [ 1, ], ], + 'Multi-delimiter Case-Insensitive Offset 1' => [ + " riding hood's red riding hood", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 1, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 2' => [ + "'s red riding hood", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 2, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 3' => [ + ' riding hood', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 3, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset -2' => [ + ' riding hood', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + -2, + 1, + ], + ], ]; diff --git a/tests/data/Calculation/TextData/TEXTBEFORE.php b/tests/data/Calculation/TextData/TEXTBEFORE.php index f94d5f28..1929354c 100644 --- a/tests/data/Calculation/TextData/TEXTBEFORE.php +++ b/tests/data/Calculation/TextData/TEXTBEFORE.php @@ -204,4 +204,40 @@ return [ 1, ], ], + 'Multi-delimiter Case-Insensitive Offset 1' => [ + 'Little ', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 1, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 2' => [ + 'Little Red riding ', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 2, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 3' => [ + "Little Red riding hood's ", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 3, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset -2' => [ + "Little Red riding hood's ", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + -2, + 1, + ], + ], ]; From db2bc3b28913d33fb762f7ca543e5b2d84b7b398 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Aug 2022 06:03:17 -0700 Subject: [PATCH 067/156] Bump phpstan/phpstan from 1.8.0 to 1.8.2 (#2977) Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan) from 1.8.0 to 1.8.2. - [Release notes](https://github.com/phpstan/phpstan/releases) - [Changelog](https://github.com/phpstan/phpstan/blob/1.8.x/CHANGELOG.md) - [Commits](https://github.com/phpstan/phpstan/compare/1.8.0...1.8.2) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- composer.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index 4ac9f631..3f82754d 100644 --- a/composer.lock +++ b/composer.lock @@ -763,12 +763,12 @@ "source": { "type": "git", "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "04f4e8f6716241cb9200774ff73cb99fbb81e09a" + "reference": "231b4e82eee01f16537c8ac3fce31c1f83320c80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/04f4e8f6716241cb9200774ff73cb99fbb81e09a", - "reference": "04f4e8f6716241cb9200774ff73cb99fbb81e09a", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/231b4e82eee01f16537c8ac3fce31c1f83320c80", + "reference": "231b4e82eee01f16537c8ac3fce31c1f83320c80", "shasum": "" }, "require": { @@ -829,7 +829,7 @@ "stylecheck", "tests" ], - "time": "2022-06-26T10:27:07+00:00" + "time": "2022-07-26T12:51:47+00:00" }, { "name": "doctrine/annotations", @@ -2076,16 +2076,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5" + "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b7648d4ee9321665acaf112e49da9fd93df8fbd5", - "reference": "b7648d4ee9321665acaf112e49da9fd93df8fbd5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c53312ecc575caf07b0e90dee43883fdf90ca67c", + "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c", "shasum": "" }, "require": { @@ -2127,7 +2127,7 @@ "type": "tidelift" } ], - "time": "2022-06-29T08:53:31+00:00" + "time": "2022-07-20T09:57:31+00:00" }, { "name": "phpstan/phpstan-phpunit", From 07f4fbe39629ce5c5d3a1c936e4e1b7d65ce9cb5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 29 Jul 2022 18:33:12 +0200 Subject: [PATCH 068/156] Initial implementation of the `TEXTSPLIT()` Excel Function --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 5 +- .../Calculation/TextData/Text.php | 130 ++++++++++++++++++ src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php | 1 + .../Functions/TextData/TextSplitTest.php | 60 ++++++++ tests/data/Calculation/TextData/TEXTSPLIT.php | 107 ++++++++++++++ 6 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php create mode 100644 tests/data/Calculation/TextData/TEXTSPLIT.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 206892ad..017e31ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Implementation of the `TEXTBEFORE()` and `TEXTAFTER()` Excel Functions +- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index b73c7eaa..13f38f66 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -7,7 +7,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack; use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger; use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; -use PhpOffice\PhpSpreadsheet\Calculation\Information\Value; use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; @@ -2505,8 +2504,8 @@ class Calculation ], 'TEXTSPLIT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2-5', + 'functionCall' => [TextData\Text::class, 'split'], + 'argumentCount' => '2-6', ], 'THAIDAYOFWEEK' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 490c43c2..bd533f20 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Text { @@ -77,4 +78,133 @@ class Text return null; } + + /** + * TEXTSPLIT. + * + * @param mixed $text the text that you're searching + * @param null|array|string $columnDelimiter The text that marks the point where to spill the text across columns. + * Multiple delimiters can be passed as an array of string values + * @param null|array|string $rowDelimiter The text that marks the point where to spill the text down rows. + * Multiple delimiters can be passed as an array of string values + * @param bool $ignoreEmpty Specify FALSE to create an empty cell when two delimiters are consecutive. + * true = create empty cells + * false = skip empty cells + * Defaults to TRUE, which creates an empty cell + * @param bool $matchMode Determines whether the match is case-sensitive or not. + * true = case-sensitive + * false = case-insensitive + * By default, a case-sensitive match is done. + * @param mixed $padding The value with which to pad the result. + * The default is #N/A. + * + * @return array the array built from the text, split by the row and column delimiters + */ + public static function split($text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, $padding = '#N/A') + { + $text = Functions::flattenSingleValue($text); + + $flags = self::matchFlags($matchMode); + + if ($rowDelimiter !== null) { + $delimiter = self::buildDelimiter($rowDelimiter); + $rows = ($delimiter === '()') + ? [$text] + : preg_split("/{$delimiter}/{$flags}", $text); + } else { + $rows = [$text]; + } + + /** @var array $rows */ + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + function ($row) { + return $row !== ''; + } + )); + } + + if ($columnDelimiter !== null) { + $delimiter = self::buildDelimiter($columnDelimiter); + array_walk( + $rows, + function (&$row) use ($delimiter, $flags, $ignoreEmpty): void { + $row = ($delimiter === '()') + ? [$row] + : preg_split("/{$delimiter}/{$flags}", $row); + /** @var array $row */ + if ($ignoreEmpty === true) { + $row = array_values(array_filter( + $row, + function ($value) { + return $value !== ''; + } + )); + } + } + ); + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + function ($row) { + return $row !== [] && $row !== ['']; + } + )); + } + } + + return self::applyPadding($rows, $padding); + } + + /** + * @param mixed $padding + */ + private static function applyPadding(array $rows, $padding): array + { + $columnCount = array_reduce( + $rows, + function (int $counter, array $row): int { + return max($counter, count($row)); + }, + 0 + ); + + return array_map( + function (array $row) use ($columnCount, $padding): array { + return (count($row) < $columnCount) + ? array_merge($row, array_fill(0, $columnCount - count($row), $padding)) + : $row; + }, + $rows + ); + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to split + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + $valueSet = Functions::flattenArray($delimiter); + + if (is_array($delimiter) && count($valueSet) > 1) { + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $valueSet + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote(Functions::flattenSingleValue($delimiter)) . ')'; + } + + private static function matchFlags(bool $matchMode): string + { + return ($matchMode === true) ? 'miu' : 'mu'; + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php index b623c573..a1bdf96a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -146,6 +146,7 @@ class Xlfn . '|register[.]id' . '|textafter' . '|textbefore' + . '|textsplit' . '|valuetotext' . ')(?=\\s*[(])/i'; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php new file mode 100644 index 00000000..e0fec8b9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php @@ -0,0 +1,60 @@ + $value) { + ++$index; + $worksheet->getCell("{$column}{$index}")->setValue($value); + } + } else { + $worksheet->getCell("{$column}1")->setValue($argument); + } + } + + /** + * @dataProvider providerTEXTSPLIT + */ + public function testTextSplit(array $expectedResult, array $arguments): void + { + $text = $arguments[0]; + $columnDelimiter = $arguments[1]; + $rowDelimiter = $arguments[2]; + + $args = 'A1'; + $args .= (is_array($columnDelimiter)) ? ', ' . $this->setDelimiterArgument($columnDelimiter, 'B') : ', B1'; + $args .= (is_array($rowDelimiter)) ? ', ' . $this->setDelimiterArgument($rowDelimiter, 'C') : ', C1'; + $args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ','; + $args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ','; + $args .= (isset($arguments[5])) ? ", {$arguments[5]}" : ','; + + $worksheet = $this->getSheet(); + $worksheet->getCell('A1')->setValue($text); + $this->setDelimiterValues($worksheet, 'B', $columnDelimiter); + $this->setDelimiterValues($worksheet, 'C', $rowDelimiter); + $worksheet->getCell('H1')->setValue("=TEXTSPLIT({$args})"); + + $result = Calculation::getInstance($this->getSpreadsheet())->calculateCellValue($worksheet->getCell('H1')); + self::assertSame($expectedResult, $result); + } + + public function providerTEXTSPLIT(): array + { + return require 'tests/data/Calculation/TextData/TEXTSPLIT.php'; + } +} diff --git a/tests/data/Calculation/TextData/TEXTSPLIT.php b/tests/data/Calculation/TextData/TEXTSPLIT.php new file mode 100644 index 00000000..64016ca7 --- /dev/null +++ b/tests/data/Calculation/TextData/TEXTSPLIT.php @@ -0,0 +1,107 @@ + Date: Wed, 3 Aug 2022 12:43:38 +0200 Subject: [PATCH 069/156] Documentation markdown fix --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57560702..b292715e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ composer require phpoffice/phpspreadsheet ``` If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: -```json lines +```json { "require": { "phpoffice/phpspreadsheet": "^1.23" diff --git a/docs/index.md b/docs/index.md index ff137c26..63d71655 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,7 +53,7 @@ composer require phpoffice/phpspreadsheet --prefer-source ``` If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: -```json lines +```json { "require": { "phpoffice/phpspreadsheet": "^1.23" From f331bca470297059dbc522183846c87a33db5dba Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 4 Aug 2022 14:38:35 +0200 Subject: [PATCH 070/156] cellExists() and getCell() methods should support UTF-8 named cells --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- .../Worksheet/WorksheetNamedRangesTest.php | 18 ++++++++++++++++++ tests/data/Worksheet/namedRangeTest.xlsx | Bin 9812 -> 10267 bytes 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 017e31ed..71b131c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) +- cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) ## 1.24.1 - 2022-07-18 diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 45111f3c..e89043c8 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1265,7 +1265,7 @@ class Worksheet implements IComparable } } elseif ( !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) && - preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $coordinate) + preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate) ) { // Named range? $namedRange = $this->validateNamedRange($coordinate, true); diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php index b367583b..506e753c 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php @@ -29,6 +29,15 @@ class WorksheetNamedRangesTest extends TestCase self::assertTrue($cellExists); } + public function testCellExistsUtf8(): void + { + $namedCell = 'Χαιρετισμός'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cellExists = $worksheet->cellExists($namedCell); + self::assertTrue($cellExists); + } + public function testCellNotExists(): void { $namedCell = 'GOODBYE'; @@ -67,6 +76,15 @@ class WorksheetNamedRangesTest extends TestCase self::assertSame('Hello', $cell->getValue()); } + public function testGetCellUtf8(): void + { + $namedCell = 'Χαιρετισμός'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cell = $worksheet->getCell($namedCell); + self::assertSame('नमस्ते', $cell->getValue()); + } + public function testGetCellNotExists(): void { $namedCell = 'GOODBYE'; diff --git a/tests/data/Worksheet/namedRangeTest.xlsx b/tests/data/Worksheet/namedRangeTest.xlsx index a4383bb0b1b6839889a3f7725ade8214db118e8c..b0936a5a1ee4deebb09dd33fe779f5d6e278e4e1 100644 GIT binary patch delta 3842 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 JN&x+R`X4`Q0$Tt8 delta 3355 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 From 4724c8f7e96ccfcf6d20360ead93dda81ee3b978 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 4 Aug 2022 14:05:18 +0200 Subject: [PATCH 071/156] Initial work on the ARRAYTOTEXT() Excel Function --- CHANGELOG.md | 1 + .../Calculation/Calculation.php | 4 +- .../Calculation/TextData/Text.php | 44 +++++++++++++++++++ .../Functions/TextData/ArrayToTextTest.php | 24 ++++++++++ .../data/Calculation/TextData/ARRAYTOTEXT.php | 26 +++++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php create mode 100644 tests/data/Calculation/TextData/ARRAYTOTEXT.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 017e31ed..38c1b6b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions +- Implementation of the `ARRAYTOTEXT()` Excel Function ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 13f38f66..cf93a694 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -304,8 +304,8 @@ class Calculation ], 'ARRAYTOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Text::class, 'fromArray'], + 'argumentCount' => '1,2', ], 'ASC' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index bd533f20..83810422 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Text @@ -207,4 +208,47 @@ class Text { return ($matchMode === true) ? 'miu' : 'mu'; } + + public static function fromArray(array $array, int $format = 0): string + { + $result = []; + foreach ($array as $row) { + $cells = []; + foreach ($row as $cellValue) { + $value = ($format === 1) ? self::formatValueMode1($cellValue) : self::formatValueMode0($cellValue); + $cells[] = $value; + } + $result[] = implode(($format === 1) ? ',' : ', ', $cells); + } + + $result = implode(($format === 1) ? ';' : ', ', $result); + + return ($format === 1) ? '{' . $result . '}' : $result; + } + + /** + * @param mixed $cellValue + */ + private static function formatValueMode0($cellValue): string + { + if (is_bool($cellValue)) { + return ($cellValue) ? Calculation::$localeBoolean['TRUE'] : Calculation::$localeBoolean['FALSE']; + } + + return (string) $cellValue; + } + + /** + * @param mixed $cellValue + */ + private static function formatValueMode1($cellValue): string + { + if (is_string($cellValue) && Functions::isError($cellValue) === false) { + return Calculation::FORMULA_STRING_QUOTE . $cellValue . Calculation::FORMULA_STRING_QUOTE; + } elseif (is_bool($cellValue)) { + return ($cellValue) ? Calculation::$localeBoolean['TRUE'] : Calculation::$localeBoolean['FALSE']; + } + + return (string) $cellValue; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php new file mode 100644 index 00000000..81fa5a53 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php @@ -0,0 +1,24 @@ +getSheet(); + $worksheet->fromArray($testData, null, 'A1', true); + $worksheet->getCell('H1')->setValue("=ARRAYTOTEXT(A1:C5, {$mode})"); + + $result = $worksheet->getCell('H1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerARRAYTOTEXT(): array + { + return require 'tests/data/Calculation/TextData/ARRAYTOTEXT.php'; + } +} diff --git a/tests/data/Calculation/TextData/ARRAYTOTEXT.php b/tests/data/Calculation/TextData/ARRAYTOTEXT.php new file mode 100644 index 00000000..4bef2823 --- /dev/null +++ b/tests/data/Calculation/TextData/ARRAYTOTEXT.php @@ -0,0 +1,26 @@ + Date: Sun, 7 Aug 2022 01:28:26 +0100 Subject: [PATCH 072/156] Ensure multiplication is performed on a non-array value (#2964) * Ensure multiplication is performed on a non-array value * Simplify formula Numbers should be numbers * Provide test coverage for SUM combined with INDEX/MATCH * PHPStan --- phpstan-baseline.neon | 2 +- src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 4 +++ .../Functions/MathTrig/SumTest.php | 25 +++++++++++++++++++ .../Functions/TextData/ConcatenateTest.php | 4 +-- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 49ab167e..d6e95eae 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2377,7 +2377,7 @@ parameters: - message: "#^Result of && is always false\\.$#" - count: 10 + count: 11 path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index ab78ef18..8aab07e3 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared\JAMA; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -742,6 +743,9 @@ class Matrix $value = trim($value, '"'); $validValues &= StringHelper::convertToNumberIfFraction($value); } + if (!is_numeric($value) && is_array($value)) { + $value = Functions::flattenArray($value)[0]; + } if ($validValues) { $this->A[$i][$j] *= $value; } else { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php index 3f80b8ec..738c203e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; +use PhpOffice\PhpSpreadsheet\Spreadsheet; + class SumTest extends AllSetupTeardown { /** @@ -44,4 +46,27 @@ class SumTest extends AllSetupTeardown { return require 'tests/data/Calculation/MathTrig/SUMLITERALS.php'; } + + public function testSumWithIndexMatch(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->fromArray( + [ + ['Number', 'Formula'], + [83, '=SUM(4 * INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)))'], + ] + ); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->fromArray( + [ + ['Lookup', 'Match'], + [83, 16], + ] + ); + self::assertSame(64, $sheet1->getCell('B2')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php index 31fb94fa..53ce2435 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php @@ -41,7 +41,7 @@ class ConcatenateTest extends AllSetupTeardown $sheet1->fromArray( [ ['Number', 'Formula'], - ['52101293', '=CONCAT(INDEX(Lookup!$B$2, MATCH($A2, Lookup!$A$2, 0)))'], + [52101293, '=CONCAT(INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)))'], ] ); $sheet2 = $spreadsheet->createSheet(); @@ -49,7 +49,7 @@ class ConcatenateTest extends AllSetupTeardown $sheet2->fromArray( [ ['Lookup', 'Match'], - ['52101293', 'PHP'], + [52101293, 'PHP'], ] ); self::assertSame('PHP', $sheet1->getCell('B2')->getCalculatedValue()); From b661d31887ae68a0f65eebf87887523c7e3e24c2 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 6 Aug 2022 17:39:18 -0700 Subject: [PATCH 073/156] Limited Support for Chart Titles as Formulas (#2971) This is a start in addressing issue #2965 (and earlier issue #749). Chart Titles are usually entered as strings or Rich Text strings, and PhpSpreadsheet supports that. They can also be entered as formulas (typically a pointer to a cell with the title text), and, not only did PhpSpreadsheet not support that, it threw an exception when reading a spreadsheet that did so. This change does: - eliminate the exception - set a static chart title when it can determine it from the Xml This change does not: - fully support dynamic titles (e.g. if you change the contents of the source cell, or delete or insert cells or rows or columns) - permit the user to set the title to a formula - allow the use of formulas when writing a chart title to a spreadsheet - provide styling for titles when it has read them as a formula --- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 20 ++++++--- .../Chart/Issue2965Test.php | 42 ++++++++++++++++++ tests/data/Reader/XLSX/issue.2965.xlsx | Bin 0 -> 13496 bytes 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/Issue2965Test.php create mode 100644 tests/data/Reader/XLSX/issue.2965.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 12ee0ade..e7314d0b 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -396,12 +396,20 @@ class Chart foreach ($titleDetails as $titleDetailKey => $chartDetail) { switch ($titleDetailKey) { case 'tx': - $titleDetails = $chartDetail->rich->children($this->aNamespace); - foreach ($titleDetails as $titleKey => $titleDetail) { - switch ($titleKey) { - case 'p': - $titleDetailPart = $titleDetail->children($this->aNamespace); - $caption[] = $this->parseRichText($titleDetailPart); + if (isset($chartDetail->rich)) { + $titleDetails = $chartDetail->rich->children($this->aNamespace); + foreach ($titleDetails as $titleKey => $titleDetail) { + switch ($titleKey) { + case 'p': + $titleDetailPart = $titleDetail->children($this->aNamespace); + $caption[] = $this->parseRichText($titleDetailPart); + } + } + } elseif (isset($chartDetail->strRef->strCache)) { + foreach ($chartDetail->strRef->strCache->pt as $pt) { + if (isset($pt->v)) { + $caption[] = (string) $pt->v; + } } } diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2965Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2965Test.php new file mode 100644 index 00000000..8294d39b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2965Test.php @@ -0,0 +1,42 @@ +Sheet1!$A$1NewTitle', $data); + } + } + + public function testChartTitleFormula(): void + { + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load(self::DIRECTORY . 'issue.2965.xlsx'); + $worksheet = $spreadsheet->getActiveSheet(); + $charts = $worksheet->getChartCollection(); + self::assertCount(1, $charts); + $originalChart1 = $charts[0]; + self::assertNotNull($originalChart1); + $originalTitle1 = $originalChart1->getTitle(); + self::assertNotNull($originalTitle1); + self::assertSame('NewTitle', $originalTitle1->getCaptionText()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/issue.2965.xlsx b/tests/data/Reader/XLSX/issue.2965.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7c0672d396605e6745a7bc653d7965531cb6f9c0 GIT binary patch literal 13496 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 z z^@~4w-x(ygJ&?UG=BZew{g0!E7r0z$1H2*nJ^Fu3tv|;8SHbn)M&BMhQTh#1W$Fl_ zL3BW<+S)oB9lU$GisO)lL+~n<NrLA2eBV~_F z!lI;Y0kl5*y6z<wk1?X4k}e;mA< z@I^B)VBiCQ8a(13ORukMq0eukYijlL)TuE&EY`z=7_ip9%E8#~%pMwvFA8DTnVj*F zC$27zX9{zQ_wp!$mSLci>~m%I=UUR2Q?7{hVM?oHOWYIZOq9L`dzLwUT<#yWt>|P+ zC6-0~^Qk<~7$gX5M-eOg)e-JoD%F-%)X`5ZnOX^Ume#~aP8Wl7mgj6VaaxT|Y~;>r z;l~=*BkU3T-51wp3uprq*1gCB+p3|ut-PUUl_d#`NTw^=&w;}bCXkP;yCmYh$N z6p9UxAm85~@olbGb_F-aGeCU*XBOGfgW&OJlj|YiEfSz*!N6Ky#?IOXpq{Ml41dlS zph@n3oeBUy@`#m|-uT(6AoB1Ye?`6@n0MC9ucv}l)<*!Tv>nrZ!43N*h70lh$TpUl zk)mgm!Fs!B$C##KMKYtEH+W&5NVnG4K&L={!76Ff)H7{d6irrz$jn$*R4sV3<=(a- zI#V2d6^|7Gn5l;{#$t1dW)ObTUMz3P2gx>(*aDe_8pEd;R((HUDU52OhLpM(7)2~Q z`#tRqLCm{ou}F~@to;w!gaKHvD&g^?7*FYfP{f~_Cn!kbg!B)lGvJ-1=f&XHS)V>! zvCc7$ma&~!6262#t1fe0E05M6)x`3Y1jN2$nvtQ}-J#1^KN#kYX07YTho<-DK`%*w zdN>q1#Jf!EcS67Yu+RsNI(?omwg{68yP)sZ2h3%fzo#akQQ*96BEw?oxZ*u}%+raX z>%QRScD~ZjGzXGU0CbBuE9?64<;vYtyrf1)P%EB|Vuok~T{->faZxqv(AftT&Yt#+ zs+#g*ot$RJ+-Z4hah|ZC7WliWdc5Cz?A7O>pLKg^bbDF3lzGP0Ptb1UzGog z59`|4{Lh5}Q}*YT9>;6F2q@~FfZpT4E@QLJ%fR}S>4_*`R_Ou9HW*74>gO~Nij>71 z&PFxJ&Z;7SySWqMWPX&dY5CY7Iio8v)5=GQ-aIgYsjcQr+Vl1K`n+`j*@q(M4E~Mn z7s86GUiA8ik2-{5WE)UPFtTq0>f;B$&GD(7&Yk6-SA(z9f5@*SH$Ux5OG0};sFT(Yf-EV*Af5 z2`~s1;N<$xzdHEW(*5iC55GZp4eJ>P|46pHMtH5oeO#1hM z{TH$O8t}Db{0-PZ_Qx3iDj;85|6M`-wgm#(A_oHc4?XqT{O`f*uja*6e=+|vl*x#L U1GxB8*&+f_12U-&v_Jp*KMqwz)&Kwi literal 0 HcmV?d00001 From eb76c3c0ffc1946aa634a63161fc73ecd2b7f850 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 6 Aug 2022 17:56:30 -0700 Subject: [PATCH 074/156] Code Coverage >90% (#2973) No source code changes, just additional tests. FormulaParser appears unused, replaced by newer code in Calculation. However, it's a public interface, so probably shouldn't be deleted without first deprecating it. I have no strong feelings about whether that should happen. However, as long as it's part of the package, we may as well have some formal unit tests for it. --- phpstan-baseline.neon | 5 - .../Calculation/FormulaParser.php | 2 +- .../Calculation/FormulaParserTest.php | 151 ++++++++++++++++++ 3 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d6e95eae..9184a5cb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -515,11 +515,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Functions\\:\\:ifCondition\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/FormulaParser.php b/src/PhpSpreadsheet/Calculation/FormulaParser.php index ddf45b23..f71d96fc 100644 --- a/src/PhpSpreadsheet/Calculation/FormulaParser.php +++ b/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -61,7 +61,7 @@ class FormulaParser /** * Create a new FormulaParser. * - * @param string $formula Formula to parse + * @param ?string $formula Formula to parse */ public function __construct($formula = '') { diff --git a/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php new file mode 100644 index 00000000..4682c6b2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php @@ -0,0 +1,151 @@ +expectException(CalcException::class); + $this->expectExceptionMessage('Invalid parameter passed: formula'); + $result = new FormulaParser(null); + } + + public function testInvalidTokenId(): void + { + $this->expectException(CalcException::class); + $this->expectExceptionMessage('Token with id 1 does not exist.'); + $result = new FormulaParser('=2'); + $result->getToken(1); + } + + public function testNoFormula(): void + { + $result = new FormulaParser(''); + self::assertSame(0, $result->getTokenCount()); + } + + /** + * @dataProvider providerFormulaParser + */ + public function testFormulaParser(string $formula, array $expectedResult): void + { + $formula = "=$formula"; + $result = new FormulaParser($formula); + self::assertSame($formula, $result->getFormula()); + self::assertSame(count($expectedResult), $result->getTokenCount()); + $tokens = $result->getTokens(); + $token0 = $result->getToken(0); + self::assertSame($tokens[0], $token0); + $idx = -1; + foreach ($expectedResult as $resultArray) { + ++$idx; + self::assertSame($resultArray[0], $tokens[$idx]->getValue()); + self::assertSame($resultArray[1], $tokens[$idx]->getTokenType()); + self::assertSame($resultArray[2], $tokens[$idx]->getTokenSubType()); + } + } + + public function providerFormulaParser(): array + { + return [ + ['5%*(2+(-3))+A3', + [ + ['5', 'Operand', 'Number'], + ['%', 'OperatorPostfix', 'Nothing'], + ['*', 'OperatorInfix', 'Math'], + ['', 'Subexpression', 'Start'], + ['2', 'Operand', 'Number'], + ['+', 'OperatorInfix', 'Math'], + ['', 'Subexpression', 'Start'], + ['-', 'OperatorPrefix', 'Nothing'], + ['3', 'Operand', 'Number'], + ['', 'Subexpression', 'Stop'], + ['', 'Subexpression', 'Stop'], + ['+', 'OperatorInfix', 'Math'], + ['A3', 'Operand', 'Range'], + ], + ], + ['"hello" & "goodbye"', + [ + ['hello', 'Operand', 'Text'], + ['&', 'OperatorInfix', 'Concatenation'], + ['goodbye', 'Operand', 'Text'], + ], + ], + ['+1.23E5', + [ + ['1.23E5', 'Operand', 'Number'], + ], + ], + ['#DIV/0!', + [ + ['#DIV/0!', 'Operand', 'Error'], + ], + ], + ['"HE""LLO"', + [ + ['HE"LLO', 'Operand', 'Text'], + ], + ], + ['MINVERSE({3,1;4,2})', + [ + ['MINVERSE', 'Function', 'Start'], + ['ARRAY', 'Function', 'Start'], + ['ARRAYROW', 'Function', 'Start'], + ['3', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['1', 'Operand', 'Number'], + ['', 'Function', 'Stop'], + [',', 'Argument', 'Nothing'], + ['ARRAYROW', 'Function', 'Start'], + ['4', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['2', 'Operand', 'Number'], + ['', 'Function', 'Stop'], + ['', 'Function', 'Stop'], + ['', 'Function', 'Stop'], + ], + ], + ['[1,1]*5', + [ + ['[1,1]', 'Operand', 'Range'], + ['*', 'OperatorInfix', 'Math'], + ['5', 'Operand', 'Number'], + ], + ], + ['IF(A1>=0,2,3)', + [ + ['IF', 'Function', 'Start'], + ['A1', 'Operand', 'Range'], + ['>=', 'OperatorInfix', 'Logical'], + ['0', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['2', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['3', 'Operand', 'Number'], + ['', 'Function', 'Stop'], + ], + ], + ["'Worksheet'!A1:A3", + [ + ['Worksheet!A1:A3', 'Operand', 'Range'], + ], + ], + ["'Worksh''eet'!A1:A3", + [ + ['Worksh\'eet!A1:A3', 'Operand', 'Range'], + ], + ], + ['true', + [ + ['true', 'Operand', 'Logical'], + ], + ], + ]; + } +} From 8bde1ace4488462ee8647d37d749da1fbd9edecf Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 6 Aug 2022 18:06:36 -0700 Subject: [PATCH 075/156] Charts Support for Rounded Corners and Trendlines (#2976) Fix #2968. Fix #2815. Solution largely based on suggestions by @bridgeplayr. --- .../33_Chart_create_scatter5_trendlines.php | 273 ++++++++++++++++++ samples/templates/32readwriteAreaChart1.xlsx | Bin 12588 -> 13641 bytes .../32readwriteScatterChartTrendlines1.xlsx | Bin 0 -> 15275 bytes src/PhpSpreadsheet/Chart/Chart.php | 15 + src/PhpSpreadsheet/Chart/DataSeriesValues.php | 15 + src/PhpSpreadsheet/Chart/TrendLine.php | 126 ++++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 37 +++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 62 +++- .../Chart/RoundedCornersTest.php | 74 +++++ .../Chart/TrendLineTest.php | 97 +++++++ 10 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 samples/Chart/33_Chart_create_scatter5_trendlines.php create mode 100644 samples/templates/32readwriteScatterChartTrendlines1.xlsx create mode 100644 src/PhpSpreadsheet/Chart/TrendLine.php create mode 100644 tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php create mode 100644 tests/PhpSpreadsheetTests/Chart/TrendLineTest.php diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php new file mode 100644 index 00000000..a87f6ee1 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -0,0 +1,273 @@ +getActiveSheet(); +$dataSheet->setTitle('Data'); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$dataSheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-04-01")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-07-01")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-10-01")', 30.2, 22.2, 0.2], + ['=DATEVALUE("2022-01-01")', 40.1, 38.1, 65.1], + ['=DATEVALUE("2022-04-01")', 45.2, 44.2, 96.2], + ['=DATEVALUE("2022-07-01")', 52.2, 51.2, 55.2], + ['=DATEVALUE("2022-10-01")', 41.2, 72.2, 56.2], + ] +); + +$dataSheet->getStyle('A2:A9')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$dataSheet->getColumnDimension('A')->setAutoSize(true); +$dataSheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$B$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$C$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), +]; +// Set the X-Axis Labels +// NUMBER, not STRING +// added x-axis values for each of the 3 metrics +// added FORMATE_CODE_NUMBER +// Number of datapoints in series +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Data Marker Color fill/[fill,Border] +// Data Marker size +// Color(s) added +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$B$2:$B$9', Properties::FORMAT_CODE_NUMBER, 8, null, 'diamond', null, 5), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$C$2:$C$9', Properties::FORMAT_CODE_NUMBER, 8, null, 'square', '*accent1', 6), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$D$2:$D$9', Properties::FORMAT_CODE_NUMBER, 8, null, null, null, 7), // let Excel choose marker shape +]; +// series 1 - metric1 +// marker details +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + +// line details - dashed, smooth line (Bezier) with arrows, 40% transparent +$dataSeriesValues[0] + ->setSmoothLine(true) + ->setScatterLines(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - metric2, straight line - no special effects, connected +$dataSeriesValues[1] // square marker border color + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square marker fill color + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - metric3, markers, no line +$dataSeriesValues[2] // triangle? fill + //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + // $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('P12'); + +// create a 'Chart' worksheet, add $chart to it +$spreadsheet->createSheet(); +$chartSheet = $spreadsheet->getSheet(1); +$chartSheet->setTitle('Scatter Chart'); + +$chartSheet = $spreadsheet->getSheetByName('Scatter Chart'); +// Add the chart to the worksheet +$chartSheet->addChart($chart); + +// ------------ Demonstrate Trendlines for metric3 values in a new chart ------------ + +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), +]; +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; + +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$D$2:$D$9', Properties::FORMAT_CODE_NUMBER, 4, null, 'triangle', null, 7), +]; + +// add 3 trendlines: +// 1- linear, double-ended arrow, w=0.5, same color as marker fill; nodispRSqr, nodispEq +// 2- polynomial (order=3) no-arrow trendline, w=1.25, same color as marker fill; dispRSqr, dispEq +// 3- moving Avg (period=2) single-arrow trendline, w=1.5, same color as marker fill; no dispRSqr, no dispEq +$trendLines = [ + new TrendLine(TrendLine::TRENDLINE_LINEAR, null, null, false, false), + new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true), + new TrendLine(TrendLine::TRENDLINE_MOVING_AVG, null, 2, true), +]; +$dataSeriesValues[0]->setTrendLines($trendLines); + +// Suppress connecting lines; instead, add different Trendline algorithms to +// determine how well the data fits the algorithm (Rsquared="goodness of fit") +// Display RSqr plus the eqn just because we can. + +$dataSeriesValues[0]->setScatterLines(false); // points not connected +$dataSeriesValues[0]->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0]->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); + +// add properties to the trendLines - give each a different color +$dataSeriesValues[0]->getTrendLines()[0]->getLineColor()->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[0]->setLineStyleProperties(0.5, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_STEALTH, 5, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); + +$dataSeriesValues[0]->getTrendLines()[1]->getLineColor()->setColorProperties('accent3', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[1]->setLineStyleProperties(1.25); + +$dataSeriesValues[0]->getTrendLines()[2]->getLineColor()->setColorProperties('accent2', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[2]->setLineStyleProperties(1.5, null, null, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); + +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // m/d/yyyy + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart - trendlines for metric3 values'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart2', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + // $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet below the first chart +$chart->setTopLeftPosition('A13'); +$chart->setBottomRightPosition('P25'); + +// Add the chart to the worksheet $chartSheet +$chartSheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteAreaChart1.xlsx b/samples/templates/32readwriteAreaChart1.xlsx index d44ae5340c3f7f1f252f4d891fd525427876ff71..474affbe8cf0f731f8fcc25c306d697aad59fb86 100644 GIT binary patch delta 9985 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 diff --git a/samples/templates/32readwriteScatterChartTrendlines1.xlsx b/samples/templates/32readwriteScatterChartTrendlines1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f48366fe0a3705bfaa1c91671a26c72b054b72e1 GIT binary patch literal 15275 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 S56XuEumcXdl5_m=?*9RVK&c7< literal 0 HcmV?d00001 diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index e850f502..556b0eff 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -147,6 +147,9 @@ class Chart /** @var bool */ private $noFill = false; + /** @var bool */ + private $roundedCorners = false; + /** * Create a new Chart. * majorGridlines and minorGridlines are deprecated, moved to Axis. @@ -762,4 +765,16 @@ class Chart return $this; } + + public function getRoundedCorners(): bool + { + return $this->roundedCorners; + } + + public function setRoundedCorners(bool $roundedCorners): self + { + $this->roundedCorners = $roundedCorners; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index cb5fa742..7d29e9c4 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -88,6 +88,9 @@ class DataSeriesValues extends Properties /** @var ?Layout */ private $labelLayout; + /** @var TrendLine[] */ + private $trendLines = []; + /** * Create a new DataSeriesValues object. * @@ -577,4 +580,16 @@ class DataSeriesValues extends Properties return $this; } + + public function setTrendLines(array $trendLines): self + { + $this->trendLines = $trendLines; + + return $this; + } + + public function getTrendLines(): array + { + return $this->trendLines; + } } diff --git a/src/PhpSpreadsheet/Chart/TrendLine.php b/src/PhpSpreadsheet/Chart/TrendLine.php new file mode 100644 index 00000000..e177f819 --- /dev/null +++ b/src/PhpSpreadsheet/Chart/TrendLine.php @@ -0,0 +1,126 @@ +setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + } + + public function getTrendLineType(): string + { + return $this->trendLineType; + } + + public function setTrendLineType(string $trendLineType): self + { + $this->trendLineType = $trendLineType; + + return $this; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setOrder(int $order): self + { + $this->order = $order; + + return $this; + } + + public function getPeriod(): int + { + return $this->period; + } + + public function setPeriod(int $period): self + { + $this->period = $period; + + return $this; + } + + public function getDispRSqr(): bool + { + return $this->dispRSqr; + } + + public function setDispRSqr(bool $dispRSqr): self + { + $this->dispRSqr = $dispRSqr; + + return $this; + } + + public function getDispEq(): bool + { + return $this->dispEq; + } + + public function setDispEq(bool $dispEq): self + { + $this->dispEq = $dispEq; + + return $this; + } + + public function setTrendLineProperties(?string $trendLineType = null, ?int $order = 0, ?int $period = 0, ?bool $dispRSqr = false, ?bool $dispEq = false): self + { + if (!empty($trendLineType)) { + $this->setTrendLineType($trendLineType); + } + if ($order !== null) { + $this->setOrder($order); + } + if ($period !== null) { + $this->setPeriod($period); + } + if ($dispRSqr !== null) { + $this->setDispRSqr($dispRSqr); + } + if ($dispEq !== null) { + $this->setDispEq($dispEq); + } + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index e7314d0b..dab2b410 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -13,6 +13,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; @@ -76,6 +77,7 @@ class Chart $chartNoFill = false; $gradientArray = []; $gradientLin = null; + $roundedCorners = false; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'spPr': @@ -84,6 +86,11 @@ class Chart $chartNoFill = true; } + break; + case 'roundedCorners': + /** @var bool */ + $roundedCorners = self::getAttribute($chartElementsC->roundedCorners, 'val', 'boolean'); + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { @@ -370,6 +377,7 @@ class Chart if ($chartNoFill) { $chart->setNoFill(true); } + $chart->setRoundedCorners($roundedCorners); if (is_bool($autoTitleDeleted)) { $chart->setAutoTitleDeleted($autoTitleDeleted); } @@ -466,6 +474,7 @@ class Chart $markerBorderColor = null; $lineStyle = null; $labelLayout = null; + $trendLines = []; foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -513,6 +522,23 @@ class Chart } } + break; + case 'trendline': + $trendLine = new TrendLine(); + $this->readLineStyle($seriesDetail, $trendLine); + /** @var ?string */ + $trendLineType = self::getAttribute($seriesDetail->trendlineType, 'val', 'string'); + /** @var ?bool */ + $dispRSqr = self::getAttribute($seriesDetail->dispRSqr, 'val', 'boolean'); + /** @var ?bool */ + $dispEq = self::getAttribute($seriesDetail->dispEq, 'val', 'boolean'); + /** @var ?int */ + $order = self::getAttribute($seriesDetail->order, 'val', 'integer'); + /** @var ?int */ + $period = self::getAttribute($seriesDetail->period, 'val', 'integer'); + $trendLine->setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + $trendLines[] = $trendLine; + break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); @@ -651,6 +677,17 @@ class Chart $seriesValues[$seriesIndex]->setSmoothLine(true); } } + if (!empty($trendLines)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setTrendLines($trendLines); + } + } } } /** @phpstan-ignore-next-line */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 278b64e7..ad746a0a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -11,6 +11,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -58,7 +59,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', 'en-GB'); $objWriter->endElement(); $objWriter->startElement('c:roundedCorners'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0'); $objWriter->endElement(); $this->writeAlternateContent($objWriter); @@ -1123,6 +1124,65 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } + // Trendlines + if ($plotSeriesValues !== false) { + foreach ($plotSeriesValues->getTrendLines() as $trendLine) { + $trendLineType = $trendLine->getTrendLineType(); + $order = $trendLine->getOrder(); + $period = $trendLine->getPeriod(); + $dispRSqr = $trendLine->getDispRSqr(); + $dispEq = $trendLine->getDispEq(); + $trendLineColor = $trendLine->getLineColor(); // ChartColor + $trendLineWidth = $trendLine->getLineStyleProperty('width'); + + $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' + $objWriter->startElement('c:spPr'); + + if (!$trendLineColor->isUsable()) { + // use dataSeriesValues line color as a backup if $trendLineColor is null + $dsvLineColor = $plotSeriesValues->getLineColor(); + if ($dsvLineColor->isUsable()) { + $trendLine + ->getLineColor() + ->setColorProperties($dsvLineColor->getValue(), $dsvLineColor->getAlpha(), $dsvLineColor->getType()); + } + } // otherwise, hope Excel does the right thing + + $this->writeLineStyles($objWriter, $trendLine, false); // suppress noFill + + $objWriter->endElement(); // spPr + + $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell' + $objWriter->writeAttribute('val', $trendLineType); + $objWriter->endElement(); // trendlineType + if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) { + $objWriter->startElement('c:order'); + $objWriter->writeAttribute('val', $order); + $objWriter->endElement(); // order + } + if ($trendLineType == TrendLine::TRENDLINE_MOVING_AVG) { + $objWriter->startElement('c:period'); + $objWriter->writeAttribute('val', $period); + $objWriter->endElement(); // period + } + $objWriter->startElement('c:dispRSqr'); + $objWriter->writeAttribute('val', $dispRSqr ? '1' : '0'); + $objWriter->endElement(); + $objWriter->startElement('c:dispEq'); + $objWriter->writeAttribute('val', $dispEq ? '1' : '0'); + $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('c:trendlineLbl'); + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', 'General'); + $objWriter->writeAttribute('sourceLinked', '0'); + $objWriter->endElement(); // numFmt + $objWriter->endElement(); // trendlineLbl + } + + $objWriter->endElement(); // trendline + } + } // Category Labels $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx); diff --git a/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php b/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php new file mode 100644 index 00000000..93bbaa23 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php @@ -0,0 +1,74 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testRounded(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Data', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getRoundedCorners()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testNotRounded(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart2.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Data', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getRoundedCorners()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php new file mode 100644 index 00000000..c8550d83 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php @@ -0,0 +1,97 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testTrendLine(): void + { + $file = self::DIRECTORY . '32readwriteScatterChartTrendlines1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getSheet(1); + self::assertSame(2, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getSheet(1); + self::assertSame('Scatter Chart', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(2, $charts); + + $chart = $charts[0]; + self::assertNotNull($chart); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeriesArray); + $plotSeries = $plotSeriesArray[0]; + $valuesArray = $plotSeries->getPlotValues(); + self::assertCount(3, $valuesArray); + self::assertEmpty($valuesArray[0]->getTrendLines()); + self::assertEmpty($valuesArray[1]->getTrendLines()); + self::assertEmpty($valuesArray[2]->getTrendLines()); + + $chart = $charts[1]; + self::assertNotNull($chart); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeriesArray); + $plotSeries = $plotSeriesArray[0]; + $valuesArray = $plotSeries->getPlotValues(); + self::assertCount(1, $valuesArray); + $trendLines = $valuesArray[0]->getTrendLines(); + self::assertCount(3, $trendLines); + + $trendLine = $trendLines[0]; + self::assertSame('linear', $trendLine->getTrendLineType()); + self::assertFalse($trendLine->getDispRSqr()); + self::assertFalse($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent4', $lineColor->getValue()); + self::assertSame('stealth', $trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(0.5, $trendLine->getLineStyleProperty('width')); + + $trendLine = $trendLines[1]; + self::assertSame('poly', $trendLine->getTrendLineType()); + self::assertTrue($trendLine->getDispRSqr()); + self::assertTrue($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent3', $lineColor->getValue()); + self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(1.25, $trendLine->getLineStyleProperty('width')); + + $trendLine = $trendLines[2]; + self::assertSame('movingAvg', $trendLine->getTrendLineType()); + self::assertTrue($trendLine->getDispRSqr()); + self::assertFalse($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent2', $lineColor->getValue()); + self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(1.5, $trendLine->getLineStyleProperty('width')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} From b65ff9f20db047b10051911a4378a576c2133d13 Mon Sep 17 00:00:00 2001 From: Mikhail Oleynik Date: Sun, 7 Aug 2022 14:50:38 +0300 Subject: [PATCH 076/156] MtJpGraph support added (#2979) https://github.com/PHPOffice/PhpSpreadsheet/pull/2979 Co-authored-by: Mikhail Oleynik --- CHANGELOG.md | 2 + README.md | 12 +- phpstan.neon.dist | 2 + src/PhpSpreadsheet/Chart/Renderer/JpGraph.php | 862 +----------------- .../Chart/Renderer/JpGraphRendererBase.php | 861 +++++++++++++++++ .../Chart/Renderer/MtJpGraphRenderer.php | 38 + .../Chart/Renderer/PHP Charting Libraries.txt | 7 +- 7 files changed, 924 insertions(+), 860 deletions(-) create mode 100644 src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php create mode 100644 src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a39a2c..00167722 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions - Implementation of the `ARRAYTOTEXT()` Excel Function +- Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of + JpGraph library to render charts added. ### Changed diff --git a/README.md b/README.md index b292715e..dd712bff 100644 --- a/README.md +++ b/README.md @@ -71,16 +71,18 @@ or the appropriate PDF Writer wrapper for the library that you have chosen to in #### Chart Export -For Chart export, we support, which you will also need to install yourself - - jpgraph/jpgraph +For Chart export, we support following packages, which you will also need to install yourself using `composer require` + - [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0. + You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) + - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) and then configure PhpSpreadsheet using: ```php -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph +//or +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph ``` -You can `composer/require` the github version of jpgraph, but this was abandoned at version 4.0; or manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/) - ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 61672b28..92767872 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -11,6 +11,8 @@ parameters: - tests/ excludePaths: - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php + - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php + - src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php parallel: processTimeout: 300.0 checkMissingIterableValueType: false diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php index b276707d..0b0164b4 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php @@ -2,68 +2,20 @@ namespace PhpOffice\PhpSpreadsheet\Chart\Renderer; -use AccBarPlot; -use AccLinePlot; -use BarPlot; -use ContourPlot; -use Graph; -use GroupBarPlot; -use LinePlot; -use PhpOffice\PhpSpreadsheet\Chart\Chart; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -use PieGraph; -use PiePlot; -use PiePlot3D; -use PiePlotC; -use RadarGraph; -use RadarPlot; -use ScatterPlot; -use Spline; -use StockPlot; - /** - * Jpgraph is not maintained in Composer, and the version there - * is extremely out of date. For that reason, all unit test - * requiring Jpgraph are skipped. So, do not measure - * code coverage for this class till that is fixed. + * Jpgraph is not oficially maintained in Composer, so the version there + * could be out of date. For that reason, all unit test requiring Jpgraph + * are skipped. So, do not measure code coverage for this class till that + * is fixed. + * + * This implementation uses abandoned package + * https://packagist.org/packages/jpgraph/jpgraph * * @codeCoverageIgnore */ -class JpGraph implements IRenderer +class JpGraph extends JpGraphRendererBase { - private static $width = 640; - - private static $height = 480; - - private static $colourSet = [ - 'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1', - 'darkmagenta', 'coral', 'dodgerblue3', 'eggplant', - 'mediumblue', 'magenta', 'sandybrown', 'cyan', - 'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen', - 'goldenrod2', - ]; - - private static $markSet; - - private $chart; - - private $graph; - - private static $plotColour = 0; - - private static $plotMark = 0; - - /** - * Create a new jpgraph. - */ - public function __construct(Chart $chart) - { - self::init(); - $this->graph = null; - $this->chart = $chart; - } - - private static function init(): void + protected static function init(): void { static $loaded = false; if ($loaded) { @@ -81,802 +33,6 @@ class JpGraph implements IRenderer \JpGraph\JpGraph::module('scatter'); \JpGraph\JpGraph::module('stock'); - self::$markSet = [ - 'diamond' => MARK_DIAMOND, - 'square' => MARK_SQUARE, - 'triangle' => MARK_UTRIANGLE, - 'x' => MARK_X, - 'star' => MARK_STAR, - 'dot' => MARK_FILLEDCIRCLE, - 'dash' => MARK_DTRIANGLE, - 'circle' => MARK_CIRCLE, - 'plus' => MARK_CROSS, - ]; - $loaded = true; } - - private function formatPointMarker($seriesPlot, $markerID) - { - $plotMarkKeys = array_keys(self::$markSet); - if ($markerID === null) { - // Use default plot marker (next marker in the series) - self::$plotMark %= count(self::$markSet); - $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); - } elseif ($markerID !== 'none') { - // Use specified plot marker (if it exists) - if (isset(self::$markSet[$markerID])) { - $seriesPlot->mark->SetType(self::$markSet[$markerID]); - } else { - // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) - self::$plotMark %= count(self::$markSet); - $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); - } - } else { - // Hide plot marker - $seriesPlot->mark->Hide(); - } - $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); - $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - - return $seriesPlot; - } - - private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') - { - $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); - if ($datasetLabelFormatCode !== null) { - // Retrieve any label formatting code - $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); - } - - $testCurrentIndex = 0; - foreach ($datasetLabels as $i => $datasetLabel) { - if (is_array($datasetLabel)) { - if ($rotation == 'bar') { - $datasetLabels[$i] = implode(' ', $datasetLabel); - } else { - $datasetLabel = array_reverse($datasetLabel); - $datasetLabels[$i] = implode("\n", $datasetLabel); - } - } else { - // Format labels according to any formatting code - if ($datasetLabelFormatCode !== null) { - $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); - } - } - ++$testCurrentIndex; - } - - return $datasetLabels; - } - - private function percentageSumCalculation($groupID, $seriesCount) - { - $sumValues = []; - // Adjust our values to a percentage value across all series in the group - for ($i = 0; $i < $seriesCount; ++$i) { - if ($i == 0) { - $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - } else { - $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - foreach ($nextValues as $k => $value) { - if (isset($sumValues[$k])) { - $sumValues[$k] += $value; - } else { - $sumValues[$k] = $value; - } - } - } - } - - return $sumValues; - } - - private function percentageAdjustValues($dataValues, $sumValues) - { - foreach ($dataValues as $k => $dataValue) { - $dataValues[$k] = $dataValue / $sumValues[$k] * 100; - } - - return $dataValues; - } - - private function getCaption($captionElement) - { - // Read any caption - $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; - // Test if we have a title caption to display - if ($caption !== null) { - // If we do, it could be a plain string or an array - if (is_array($caption)) { - // Implode an array to a plain string - $caption = implode('', $caption); - } - } - - return $caption; - } - - private function renderTitle(): void - { - $title = $this->getCaption($this->chart->getTitle()); - if ($title !== null) { - $this->graph->title->Set($title); - } - } - - private function renderLegend(): void - { - $legend = $this->chart->getLegend(); - if ($legend !== null) { - $legendPosition = $legend->getPosition(); - switch ($legendPosition) { - case 'r': - $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right - $this->graph->legend->SetColumns(1); - - break; - case 'l': - $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left - $this->graph->legend->SetColumns(1); - - break; - case 't': - $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top - - break; - case 'b': - $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom - - break; - default: - $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right - $this->graph->legend->SetColumns(1); - - break; - } - } else { - $this->graph->legend->Hide(); - } - } - - private function renderCartesianPlotArea($type = 'textlin'): void - { - $this->graph = new Graph(self::$width, self::$height); - $this->graph->SetScale($type); - - $this->renderTitle(); - - // Rotate for bar rather than column chart - $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); - $reverse = $rotation == 'bar'; - - $xAxisLabel = $this->chart->getXAxisLabel(); - if ($xAxisLabel !== null) { - $title = $this->getCaption($xAxisLabel); - if ($title !== null) { - $this->graph->xaxis->SetTitle($title, 'center'); - $this->graph->xaxis->title->SetMargin(35); - if ($reverse) { - $this->graph->xaxis->title->SetAngle(90); - $this->graph->xaxis->title->SetMargin(90); - } - } - } - - $yAxisLabel = $this->chart->getYAxisLabel(); - if ($yAxisLabel !== null) { - $title = $this->getCaption($yAxisLabel); - if ($title !== null) { - $this->graph->yaxis->SetTitle($title, 'center'); - if ($reverse) { - $this->graph->yaxis->title->SetAngle(0); - $this->graph->yaxis->title->SetMargin(-55); - } - } - } - } - - private function renderPiePlotArea(): void - { - $this->graph = new PieGraph(self::$width, self::$height); - - $this->renderTitle(); - } - - private function renderRadarPlotArea(): void - { - $this->graph = new RadarGraph(self::$width, self::$height); - $this->graph->SetScale('lin'); - - $this->renderTitle(); - } - - private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void - { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - if ($grouping == 'percentStacked') { - $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); - } else { - $sumValues = []; - } - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - - if ($grouping == 'percentStacked') { - $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); - } - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - $seriesPlot = new LinePlot($dataValues); - if ($combination) { - $seriesPlot->SetBarCenter(); - } - - if ($filled) { - $seriesPlot->SetFilled(true); - $seriesPlot->SetColor('black'); - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); - } else { - // Set the appropriate plot marker - $this->formatPointMarker($seriesPlot, $marker); - } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetLegend($dataLabel); - - $seriesPlots[] = $seriesPlot; - } - - if ($grouping == 'standard') { - $groupPlot = $seriesPlots; - } else { - $groupPlot = new AccLinePlot($seriesPlots); - } - $this->graph->Add($groupPlot); - } - - private function renderPlotBar($groupID, $dimensions = '2d'): void - { - $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); - // Rotate for bar rather than column chart - if (($groupID == 0) && ($rotation == 'bar')) { - $this->graph->Set90AndMargin(); - } - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); - // Rotate for bar rather than column chart - if ($rotation == 'bar') { - $datasetLabels = array_reverse($datasetLabels); - $this->graph->yaxis->SetPos('max'); - $this->graph->yaxis->SetLabelAlign('center', 'top'); - $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); - } - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - if ($grouping == 'percentStacked') { - $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); - } else { - $sumValues = []; - } - - // Loop through each data series in turn - for ($j = 0; $j < $seriesCount; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); - if ($grouping == 'percentStacked') { - $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); - } - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - // Reverse the $dataValues order for bar rather than column chart - if ($rotation == 'bar') { - $dataValues = array_reverse($dataValues); - } - $seriesPlot = new BarPlot($dataValues); - $seriesPlot->SetColor('black'); - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); - if ($dimensions == '3d') { - $seriesPlot->SetShadow(); - } - if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { - $dataLabel = ''; - } else { - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); - } - $seriesPlot->SetLegend($dataLabel); - - $seriesPlots[] = $seriesPlot; - } - // Reverse the plot order for bar rather than column chart - if (($rotation == 'bar') && ($grouping != 'percentStacked')) { - $seriesPlots = array_reverse($seriesPlots); - } - - if ($grouping == 'clustered') { - $groupPlot = new GroupBarPlot($seriesPlots); - } elseif ($grouping == 'standard') { - $groupPlot = new GroupBarPlot($seriesPlots); - } else { - $groupPlot = new AccBarPlot($seriesPlots); - if ($dimensions == '3d') { - $groupPlot->SetShadow(); - } - } - - $this->graph->Add($groupPlot); - } - - private function renderPlotScatter($groupID, $bubble): void - { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - - foreach ($dataValuesY as $k => $dataValueY) { - $dataValuesY[$k] = $k; - } - - $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); - if ($scatterStyle == 'lineMarker') { - $seriesPlot->SetLinkPoints(); - $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); - } elseif ($scatterStyle == 'smoothMarker') { - $spline = new Spline($dataValuesY, $dataValuesX); - [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); - $lplot = new LinePlot($splineDataX, $splineDataY); - $lplot->SetColor(self::$colourSet[self::$plotColour]); - - $this->graph->Add($lplot); - } - - if ($bubble) { - $this->formatPointMarker($seriesPlot, 'dot'); - $seriesPlot->mark->SetColor('black'); - $seriesPlot->mark->SetSize($bubbleSize); - } else { - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - $this->formatPointMarker($seriesPlot, $marker); - } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetLegend($dataLabel); - - $this->graph->Add($seriesPlot); - } - } - - private function renderPlotRadar($groupID): void - { - $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - - $dataValues = []; - foreach ($dataValuesY as $k => $dataValueY) { - $dataValues[$k] = implode(' ', array_reverse($dataValueY)); - } - $tmp = array_shift($dataValues); - $dataValues[] = $tmp; - $tmp = array_shift($dataValuesX); - $dataValuesX[] = $tmp; - - $this->graph->SetTitles(array_reverse($dataValues)); - - $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); - - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - if ($radarStyle == 'filled') { - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); - } - $this->formatPointMarker($seriesPlot, $marker); - $seriesPlot->SetLegend($dataLabel); - - $this->graph->Add($seriesPlot); - } - } - - private function renderPlotContour($groupID): void - { - $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - $dataValues = []; - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - - $dataValues[$i] = $dataValuesX; - } - $seriesPlot = new ContourPlot($dataValues); - - $this->graph->Add($seriesPlot); - } - - private function renderPlotStock($groupID): void - { - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); - - $dataValues = []; - // Loop through each data series in turn and build the plot arrays - foreach ($plotOrder as $i => $v) { - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); - foreach ($dataValuesX as $j => $dataValueX) { - $dataValues[$plotOrder[$i]][$j] = $dataValueX; - } - } - if (empty($dataValues)) { - return; - } - - $dataValuesPlot = []; - // Flatten the plot arrays to a single dimensional array to work with jpgraph - $jMax = count($dataValues[0]); - for ($j = 0; $j < $jMax; ++$j) { - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesPlot[] = $dataValues[$i][$j]; - } - } - - // Set the x-axis labels - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesPlot = new StockPlot($dataValuesPlot); - $seriesPlot->SetWidth(20); - - $this->graph->Add($seriesPlot); - } - - private function renderAreaChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, true, false, $dimensions); - } - } - - private function renderLineChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, false, false, $dimensions); - } - } - - private function renderBarChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotBar($i, $dimensions); - } - } - - private function renderScatterChart($groupCount): void - { - $this->renderCartesianPlotArea('linlin'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotScatter($i, false); - } - } - - private function renderBubbleChart($groupCount): void - { - $this->renderCartesianPlotArea('linlin'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotScatter($i, true); - } - } - - private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void - { - $this->renderPiePlotArea(); - - $iLimit = ($multiplePlots) ? $groupCount : 1; - for ($groupID = 0; $groupID < $iLimit; ++$groupID) { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - $datasetLabels = []; - if ($groupID == 0) { - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - } - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - // For pie charts, we only display the first series: doughnut charts generally display all series - $jLimit = ($multiplePlots) ? $seriesCount : 1; - // Loop through each data series in turn - for ($j = 0; $j < $jLimit; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - if ($dimensions == '3d') { - $seriesPlot = new PiePlot3D($dataValues); - } else { - if ($doughnut) { - $seriesPlot = new PiePlotC($dataValues); - } else { - $seriesPlot = new PiePlot($dataValues); - } - } - - if ($multiplePlots) { - $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); - } - - if ($doughnut) { - $seriesPlot->SetMidColor('white'); - } - - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - if (count($datasetLabels) > 0) { - $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); - } - if ($dimensions != '3d') { - $seriesPlot->SetGuideLines(false); - } - if ($j == 0) { - if ($exploded) { - $seriesPlot->ExplodeAll(); - } - $seriesPlot->SetLegends($datasetLabels); - } - - $this->graph->Add($seriesPlot); - } - } - } - - private function renderRadarChart($groupCount): void - { - $this->renderRadarPlotArea(); - - for ($groupID = 0; $groupID < $groupCount; ++$groupID) { - $this->renderPlotRadar($groupID); - } - } - - private function renderStockChart($groupCount): void - { - $this->renderCartesianPlotArea('intint'); - - for ($groupID = 0; $groupID < $groupCount; ++$groupID) { - $this->renderPlotStock($groupID); - } - } - - private function renderContourChart($groupCount, $dimensions): void - { - $this->renderCartesianPlotArea('intint'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotContour($i); - } - } - - private function renderCombinationChart($groupCount, $dimensions, $outputDestination) - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $dimensions = null; - $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); - switch ($chartType) { - case 'area3DChart': - $dimensions = '3d'; - // no break - case 'areaChart': - $this->renderPlotLine($i, true, true, $dimensions); - - break; - case 'bar3DChart': - $dimensions = '3d'; - // no break - case 'barChart': - $this->renderPlotBar($i, $dimensions); - - break; - case 'line3DChart': - $dimensions = '3d'; - // no break - case 'lineChart': - $this->renderPlotLine($i, false, true, $dimensions); - - break; - case 'scatterChart': - $this->renderPlotScatter($i, false); - - break; - case 'bubbleChart': - $this->renderPlotScatter($i, true); - - break; - default: - $this->graph = null; - - return false; - } - } - - $this->renderLegend(); - - $this->graph->Stroke($outputDestination); - - return true; - } - - public function render($outputDestination) - { - self::$plotColour = 0; - - $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); - - $dimensions = null; - if ($groupCount == 1) { - $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); - } else { - $chartTypes = []; - for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); - } - $chartTypes = array_unique($chartTypes); - if (count($chartTypes) == 1) { - $chartType = array_pop($chartTypes); - } elseif (count($chartTypes) == 0) { - echo 'Chart is not yet implemented
'; - - return false; - } else { - return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); - } - } - - switch ($chartType) { - case 'area3DChart': - $dimensions = '3d'; - // no break - case 'areaChart': - $this->renderAreaChart($groupCount, $dimensions); - - break; - case 'bar3DChart': - $dimensions = '3d'; - // no break - case 'barChart': - $this->renderBarChart($groupCount, $dimensions); - - break; - case 'line3DChart': - $dimensions = '3d'; - // no break - case 'lineChart': - $this->renderLineChart($groupCount, $dimensions); - - break; - case 'pie3DChart': - $dimensions = '3d'; - // no break - case 'pieChart': - $this->renderPieChart($groupCount, $dimensions, false, false); - - break; - case 'doughnut3DChart': - $dimensions = '3d'; - // no break - case 'doughnutChart': - $this->renderPieChart($groupCount, $dimensions, true, true); - - break; - case 'scatterChart': - $this->renderScatterChart($groupCount); - - break; - case 'bubbleChart': - $this->renderBubbleChart($groupCount); - - break; - case 'radarChart': - $this->renderRadarChart($groupCount); - - break; - case 'surface3DChart': - $dimensions = '3d'; - // no break - case 'surfaceChart': - $this->renderContourChart($groupCount, $dimensions); - - break; - case 'stockChart': - $this->renderStockChart($groupCount); - - break; - default: - echo $chartType . ' is not yet implemented
'; - - return false; - } - $this->renderLegend(); - - $this->graph->Stroke($outputDestination); - - return true; - } } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php new file mode 100644 index 00000000..4d5526b8 --- /dev/null +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -0,0 +1,861 @@ +graph = null; + $this->chart = $chart; + + self::$markSet = [ + 'diamond' => MARK_DIAMOND, + 'square' => MARK_SQUARE, + 'triangle' => MARK_UTRIANGLE, + 'x' => MARK_X, + 'star' => MARK_STAR, + 'dot' => MARK_FILLEDCIRCLE, + 'dash' => MARK_DTRIANGLE, + 'circle' => MARK_CIRCLE, + 'plus' => MARK_CROSS, + ]; + } + + /** + * This method should be overriden in descendants to do real JpGraph library initialization. + */ + abstract protected static function init(): void; + + private function formatPointMarker($seriesPlot, $markerID) + { + $plotMarkKeys = array_keys(self::$markSet); + if ($markerID === null) { + // Use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } elseif ($markerID !== 'none') { + // Use specified plot marker (if it exists) + if (isset(self::$markSet[$markerID])) { + $seriesPlot->mark->SetType(self::$markSet[$markerID]); + } else { + // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } + } else { + // Hide plot marker + $seriesPlot->mark->Hide(); + } + $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); + $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + + return $seriesPlot; + } + + private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') + { + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); + if ($datasetLabelFormatCode !== null) { + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); + } + + $testCurrentIndex = 0; + foreach ($datasetLabels as $i => $datasetLabel) { + if (is_array($datasetLabel)) { + if ($rotation == 'bar') { + $datasetLabels[$i] = implode(' ', $datasetLabel); + } else { + $datasetLabel = array_reverse($datasetLabel); + $datasetLabels[$i] = implode("\n", $datasetLabel); + } + } else { + // Format labels according to any formatting code + if ($datasetLabelFormatCode !== null) { + $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); + } + } + ++$testCurrentIndex; + } + + return $datasetLabels; + } + + private function percentageSumCalculation($groupID, $seriesCount) + { + $sumValues = []; + // Adjust our values to a percentage value across all series in the group + for ($i = 0; $i < $seriesCount; ++$i) { + if ($i == 0) { + $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + } else { + $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + foreach ($nextValues as $k => $value) { + if (isset($sumValues[$k])) { + $sumValues[$k] += $value; + } else { + $sumValues[$k] = $value; + } + } + } + } + + return $sumValues; + } + + private function percentageAdjustValues($dataValues, $sumValues) + { + foreach ($dataValues as $k => $dataValue) { + $dataValues[$k] = $dataValue / $sumValues[$k] * 100; + } + + return $dataValues; + } + + private function getCaption($captionElement) + { + // Read any caption + $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; + // Test if we have a title caption to display + if ($caption !== null) { + // If we do, it could be a plain string or an array + if (is_array($caption)) { + // Implode an array to a plain string + $caption = implode('', $caption); + } + } + + return $caption; + } + + private function renderTitle(): void + { + $title = $this->getCaption($this->chart->getTitle()); + if ($title !== null) { + $this->graph->title->Set($title); + } + } + + private function renderLegend(): void + { + $legend = $this->chart->getLegend(); + if ($legend !== null) { + $legendPosition = $legend->getPosition(); + switch ($legendPosition) { + case 'r': + $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right + $this->graph->legend->SetColumns(1); + + break; + case 'l': + $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left + $this->graph->legend->SetColumns(1); + + break; + case 't': + $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top + + break; + case 'b': + $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom + + break; + default: + $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right + $this->graph->legend->SetColumns(1); + + break; + } + } else { + $this->graph->legend->Hide(); + } + } + + private function renderCartesianPlotArea($type = 'textlin'): void + { + $this->graph = new Graph(self::$width, self::$height); + $this->graph->SetScale($type); + + $this->renderTitle(); + + // Rotate for bar rather than column chart + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); + $reverse = $rotation == 'bar'; + + $xAxisLabel = $this->chart->getXAxisLabel(); + if ($xAxisLabel !== null) { + $title = $this->getCaption($xAxisLabel); + if ($title !== null) { + $this->graph->xaxis->SetTitle($title, 'center'); + $this->graph->xaxis->title->SetMargin(35); + if ($reverse) { + $this->graph->xaxis->title->SetAngle(90); + $this->graph->xaxis->title->SetMargin(90); + } + } + } + + $yAxisLabel = $this->chart->getYAxisLabel(); + if ($yAxisLabel !== null) { + $title = $this->getCaption($yAxisLabel); + if ($title !== null) { + $this->graph->yaxis->SetTitle($title, 'center'); + if ($reverse) { + $this->graph->yaxis->title->SetAngle(0); + $this->graph->yaxis->title->SetMargin(-55); + } + } + } + } + + private function renderPiePlotArea(): void + { + $this->graph = new PieGraph(self::$width, self::$height); + + $this->renderTitle(); + } + + private function renderRadarPlotArea(): void + { + $this->graph = new RadarGraph(self::$width, self::$height); + $this->graph->SetScale('lin'); + + $this->renderTitle(); + } + + private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + $seriesPlot = new LinePlot($dataValues); + if ($combination) { + $seriesPlot->SetBarCenter(); + } + + if ($filled) { + $seriesPlot->SetFilled(true); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + } else { + // Set the appropriate plot marker + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + + if ($grouping == 'standard') { + $groupPlot = $seriesPlots; + } else { + $groupPlot = new AccLinePlot($seriesPlots); + } + $this->graph->Add($groupPlot); + } + + private function renderPlotBar($groupID, $dimensions = '2d'): void + { + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); + // Rotate for bar rather than column chart + if (($groupID == 0) && ($rotation == 'bar')) { + $this->graph->Set90AndMargin(); + } + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); + // Rotate for bar rather than column chart + if ($rotation == 'bar') { + $datasetLabels = array_reverse($datasetLabels); + $this->graph->yaxis->SetPos('max'); + $this->graph->yaxis->SetLabelAlign('center', 'top'); + $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); + } + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($j = 0; $j < $seriesCount; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + // Reverse the $dataValues order for bar rather than column chart + if ($rotation == 'bar') { + $dataValues = array_reverse($dataValues); + } + $seriesPlot = new BarPlot($dataValues); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + if ($dimensions == '3d') { + $seriesPlot->SetShadow(); + } + if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { + $dataLabel = ''; + } else { + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); + } + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + // Reverse the plot order for bar rather than column chart + if (($rotation == 'bar') && ($grouping != 'percentStacked')) { + $seriesPlots = array_reverse($seriesPlots); + } + + if ($grouping == 'clustered') { + $groupPlot = new GroupBarPlot($seriesPlots); + } elseif ($grouping == 'standard') { + $groupPlot = new GroupBarPlot($seriesPlots); + } else { + $groupPlot = new AccBarPlot($seriesPlots); + if ($dimensions == '3d') { + $groupPlot->SetShadow(); + } + } + + $this->graph->Add($groupPlot); + } + + private function renderPlotScatter($groupID, $bubble): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + foreach ($dataValuesY as $k => $dataValueY) { + $dataValuesY[$k] = $k; + } + + $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); + if ($scatterStyle == 'lineMarker') { + $seriesPlot->SetLinkPoints(); + $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); + } elseif ($scatterStyle == 'smoothMarker') { + $spline = new Spline($dataValuesY, $dataValuesX); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); + $lplot = new LinePlot($splineDataX, $splineDataY); + $lplot->SetColor(self::$colourSet[self::$plotColour]); + + $this->graph->Add($lplot); + } + + if ($bubble) { + $this->formatPointMarker($seriesPlot, 'dot'); + $seriesPlot->mark->SetColor('black'); + $seriesPlot->mark->SetSize($bubbleSize); + } else { + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotRadar($groupID): void + { + $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + $dataValues = []; + foreach ($dataValuesY as $k => $dataValueY) { + $dataValues[$k] = implode(' ', array_reverse($dataValueY)); + } + $tmp = array_shift($dataValues); + $dataValues[] = $tmp; + $tmp = array_shift($dataValuesX); + $dataValuesX[] = $tmp; + + $this->graph->SetTitles(array_reverse($dataValues)); + + $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); + + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if ($radarStyle == 'filled') { + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); + } + $this->formatPointMarker($seriesPlot, $marker); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotContour($groupID): void + { + $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + + $dataValues = []; + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + $dataValues[$i] = $dataValuesX; + } + $seriesPlot = new ContourPlot($dataValues); + + $this->graph->Add($seriesPlot); + } + + private function renderPlotStock($groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); + + $dataValues = []; + // Loop through each data series in turn and build the plot arrays + foreach ($plotOrder as $i => $v) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); + foreach ($dataValuesX as $j => $dataValueX) { + $dataValues[$plotOrder[$i]][$j] = $dataValueX; + } + } + if (empty($dataValues)) { + return; + } + + $dataValuesPlot = []; + // Flatten the plot arrays to a single dimensional array to work with jpgraph + $jMax = count($dataValues[0]); + for ($j = 0; $j < $jMax; ++$j) { + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesPlot[] = $dataValues[$i][$j]; + } + } + + // Set the x-axis labels + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesPlot = new StockPlot($dataValuesPlot); + $seriesPlot->SetWidth(20); + + $this->graph->Add($seriesPlot); + } + + private function renderAreaChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, true, false, $dimensions); + } + } + + private function renderLineChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, false, false, $dimensions); + } + } + + private function renderBarChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotBar($i, $dimensions); + } + } + + private function renderScatterChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, false); + } + } + + private function renderBubbleChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, true); + } + } + + private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void + { + $this->renderPiePlotArea(); + + $iLimit = ($multiplePlots) ? $groupCount : 1; + for ($groupID = 0; $groupID < $iLimit; ++$groupID) { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + $datasetLabels = []; + if ($groupID == 0) { + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + } + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + // For pie charts, we only display the first series: doughnut charts generally display all series + $jLimit = ($multiplePlots) ? $seriesCount : 1; + // Loop through each data series in turn + for ($j = 0; $j < $jLimit; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + if ($dimensions == '3d') { + $seriesPlot = new PiePlot3D($dataValues); + } else { + if ($doughnut) { + $seriesPlot = new PiePlotC($dataValues); + } else { + $seriesPlot = new PiePlot($dataValues); + } + } + + if ($multiplePlots) { + $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); + } + + if ($doughnut) { + $seriesPlot->SetMidColor('white'); + } + + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if (count($datasetLabels) > 0) { + $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); + } + if ($dimensions != '3d') { + $seriesPlot->SetGuideLines(false); + } + if ($j == 0) { + if ($exploded) { + $seriesPlot->ExplodeAll(); + } + $seriesPlot->SetLegends($datasetLabels); + } + + $this->graph->Add($seriesPlot); + } + } + } + + private function renderRadarChart($groupCount): void + { + $this->renderRadarPlotArea(); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotRadar($groupID); + } + } + + private function renderStockChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotStock($groupID); + } + } + + private function renderContourChart($groupCount, $dimensions): void + { + $this->renderCartesianPlotArea('intint'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotContour($i); + } + } + + private function renderCombinationChart($groupCount, $dimensions, $outputDestination) + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $dimensions = null; + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderPlotLine($i, true, true, $dimensions); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderPlotBar($i, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderPlotLine($i, false, true, $dimensions); + + break; + case 'scatterChart': + $this->renderPlotScatter($i, false); + + break; + case 'bubbleChart': + $this->renderPlotScatter($i, true); + + break; + default: + $this->graph = null; + + return false; + } + } + + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } + + public function render($outputDestination) + { + self::$plotColour = 0; + + $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); + + $dimensions = null; + if ($groupCount == 1) { + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = array_pop($chartTypes); + } elseif (count($chartTypes) == 0) { + echo 'Chart is not yet implemented
'; + + return false; + } else { + return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); + } + } + + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderAreaChart($groupCount, $dimensions); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderBarChart($groupCount, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderLineChart($groupCount, $dimensions); + + break; + case 'pie3DChart': + $dimensions = '3d'; + // no break + case 'pieChart': + $this->renderPieChart($groupCount, $dimensions, false, false); + + break; + case 'doughnut3DChart': + $dimensions = '3d'; + // no break + case 'doughnutChart': + $this->renderPieChart($groupCount, $dimensions, true, true); + + break; + case 'scatterChart': + $this->renderScatterChart($groupCount); + + break; + case 'bubbleChart': + $this->renderBubbleChart($groupCount); + + break; + case 'radarChart': + $this->renderRadarChart($groupCount); + + break; + case 'surface3DChart': + $dimensions = '3d'; + // no break + case 'surfaceChart': + $this->renderContourChart($groupCount, $dimensions); + + break; + case 'stockChart': + $this->renderStockChart($groupCount); + + break; + default: + echo $chartType . ' is not yet implemented
'; + + return false; + } + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } +} diff --git a/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php new file mode 100644 index 00000000..3fef3b6e --- /dev/null +++ b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php @@ -0,0 +1,38 @@ + Date: Sun, 7 Aug 2022 13:59:26 +0200 Subject: [PATCH 077/156] Expand [PR #2964](https://github.com/PHPOffice/PhpSpreadsheet/pull/2964) to cover all arithmetic operators, not just multiplication, and both left and right side values --- phpstan-baseline.neon | 10 --- src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 69 +++++++------------ .../Functions/MathTrig/SumTest.php | 16 ++++- .../MathTrig/SUMWITHINDEXMATCH.php | 34 +++++++++ 4 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9184a5cb..596e1e10 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2285,11 +2285,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php - - - message: "#^Call to function is_string\\(\\) with float\\|int will always evaluate to false\\.$#" - count: 5 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:__construct\\(\\) has parameter \\$args with no type specified\\.$#" count: 1 @@ -2370,11 +2365,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - message: "#^Result of && is always false\\.$#" - count: 11 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 19 diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index 8aab07e3..58ab8813 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -533,14 +533,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] += $value; } else { @@ -633,14 +627,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] -= $value; } else { @@ -735,17 +723,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } - if (!is_numeric($value) && is_array($value)) { - $value = Functions::flattenArray($value)[0]; - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] *= $value; } else { @@ -796,14 +775,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { if ($value == 0) { // Trap for Divide by Zero error @@ -1083,14 +1056,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] = $this->A[$i][$j] ** $value; } else { @@ -1191,4 +1158,20 @@ class Matrix return $L->det(); } + + /** + * @param mixed $value + */ + private function validateExtractedValue($value, bool $validValues): array + { + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + if (!is_numeric($value) && is_array($value)) { + $value = Functions::flattenArray($value)[0]; + } + + return [$value, $validValues]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php index 738c203e..780b9623 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -47,7 +47,12 @@ class SumTest extends AllSetupTeardown return require 'tests/data/Calculation/MathTrig/SUMLITERALS.php'; } - public function testSumWithIndexMatch(): void + /** + * @dataProvider providerSUMWITHINDEXMATCH + * + * @param mixed $expectedResult + */ + public function testSumWithIndexMatch($expectedResult, string $formula): void { $spreadsheet = new Spreadsheet(); $sheet1 = $spreadsheet->getActiveSheet(); @@ -55,7 +60,7 @@ class SumTest extends AllSetupTeardown $sheet1->fromArray( [ ['Number', 'Formula'], - [83, '=SUM(4 * INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)))'], + [83, $formula], ] ); $sheet2 = $spreadsheet->createSheet(); @@ -66,7 +71,12 @@ class SumTest extends AllSetupTeardown [83, 16], ] ); - self::assertSame(64, $sheet1->getCell('B2')->getCalculatedValue()); + self::assertSame($expectedResult, $sheet1->getCell('B2')->getCalculatedValue()); $spreadsheet->disconnectWorksheets(); } + + public function providerSUMWITHINDEXMATCH(): array + { + return require 'tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php'; + } } diff --git a/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php b/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php new file mode 100644 index 00000000..b62cfd30 --- /dev/null +++ b/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php @@ -0,0 +1,34 @@ + Date: Sun, 7 Aug 2022 18:45:36 +0200 Subject: [PATCH 078/156] Additional for [PR #2964](https://github.com/PHPOffice/PhpSpreadsheet/pull/2964); validate value after extracting from flattened array --- src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index 58ab8813..0bbd94a7 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -1164,13 +1164,13 @@ class Matrix */ private function validateExtractedValue($value, bool $validValues): array { + if (!is_numeric($value) && is_array($value)) { + $value = Functions::flattenArray($value)[0]; + } if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { $value = trim($value, '"'); $validValues &= StringHelper::convertToNumberIfFraction($value); } - if (!is_numeric($value) && is_array($value)) { - $value = Functions::flattenArray($value)[0]; - } return [$value, $validValues]; } From b783fecb7fd02c2f86854de1cbff7afac7bdf939 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 12 Aug 2022 14:03:13 +0200 Subject: [PATCH 079/156] More return type declarations, and some additional argument typehinting --- src/PhpSpreadsheet/Cell/Cell.php | 92 +++++++------------ src/PhpSpreadsheet/Collection/Cells.php | 16 +--- .../Collection/CellsFactory.php | 3 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 44 +++------ 4 files changed, 52 insertions(+), 103 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 2005694d..bc02fbf0 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -50,14 +50,14 @@ class Cell private $dataType; /** - * Collection of cells. + * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet). * * @var Cells */ private $parent; /** - * Index to cellXf. + * Index to the cellXf reference for the styling of this cell. * * @var int */ @@ -95,9 +95,8 @@ class Cell * Create a new Cell. * * @param mixed $value - * @param string $dataType */ - public function __construct($value, $dataType, Worksheet $worksheet) + public function __construct($value, ?string $dataType, Worksheet $worksheet) { // Initialise cell value $this->value = $value; @@ -111,7 +110,7 @@ class Cell $dataType = DataType::TYPE_STRING; } $this->dataType = $dataType; - } elseif (!self::getValueBinder()->bindValue($this, $value)) { + } elseif (self::getValueBinder()->bindValue($this, $value) === false) { throw new Exception('Value could not be bound to cell.'); } } @@ -167,10 +166,8 @@ class Cell /** * Get cell value with formatting. - * - * @return string */ - public function getFormattedValue() + public function getFormattedValue(): string { return (string) NumberFormat::toFormattedString( $this->getCalculatedValue(), @@ -188,7 +185,7 @@ class Cell * * @return $this */ - public function setValue($value) + public function setValue($value): self { if (!self::getValueBinder()->bindValue($this, $value)) { throw new Exception('Value could not be bound to cell.'); @@ -205,7 +202,7 @@ class Cell * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this * method, then it is your responsibility as an end-user developer to validate that the value and * the datatype match. - * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * If you do mismatch value and datatype, then the value you enter may be changed to match the datatype * that you specify. * * @return Cell @@ -271,7 +268,7 @@ class Cell * * @return mixed */ - public function getCalculatedValue($resetLog = true) + public function getCalculatedValue(bool $resetLog = true) { if ($this->dataType === DataType::TYPE_FORMULA) { try { @@ -319,7 +316,7 @@ class Cell * * @return Cell */ - public function setCalculatedValue($originalValue) + public function setCalculatedValue($originalValue): self { if ($originalValue !== null) { $this->calculatedValue = (is_numeric($originalValue)) ? (float) $originalValue : $originalValue; @@ -345,10 +342,8 @@ class Cell /** * Get cell data type. - * - * @return string */ - public function getDataType() + public function getDataType(): string { return $this->dataType; } @@ -360,7 +355,7 @@ class Cell * * @return Cell */ - public function setDataType($dataType) + public function setDataType($dataType): self { if ($dataType == DataType::TYPE_STRING2) { $dataType = DataType::TYPE_STRING; @@ -392,10 +387,8 @@ class Cell /** * Get Data validation rules. - * - * @return DataValidation */ - public function getDataValidation() + public function getDataValidation(): DataValidation { if (!isset($this->parent)) { throw new Exception('Cannot get data validation for cell that is not bound to a worksheet'); @@ -420,10 +413,8 @@ class Cell /** * Does this cell contain valid value? - * - * @return bool */ - public function hasValidValue() + public function hasValidValue(): bool { $validator = new DataValidator(); @@ -432,10 +423,8 @@ class Cell /** * Does this cell contain a Hyperlink? - * - * @return bool */ - public function hasHyperlink() + public function hasHyperlink(): bool { if (!isset($this->parent)) { throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet'); @@ -446,10 +435,8 @@ class Cell /** * Get Hyperlink. - * - * @return Hyperlink */ - public function getHyperlink() + public function getHyperlink(): Hyperlink { if (!isset($this->parent)) { throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet'); @@ -463,7 +450,7 @@ class Cell * * @return Cell */ - public function setHyperlink(?Hyperlink $hyperlink = null) + public function setHyperlink(?Hyperlink $hyperlink = null): self { if (!isset($this->parent)) { throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet'); @@ -486,10 +473,8 @@ class Cell /** * Get parent worksheet. - * - * @return Worksheet */ - public function getWorksheet() + public function getWorksheet(): Worksheet { try { $worksheet = $this->parent->getParent(); @@ -506,27 +491,22 @@ class Cell /** * Is this cell in a merge range. - * - * @return bool */ - public function isInMergeRange() + public function isInMergeRange(): bool { return (bool) $this->getMergeRange(); } /** * Is this cell the master (top left cell) in a merge range (that holds the actual data value). - * - * @return bool */ - public function isMergeRangeValueCell() + public function isMergeRangeValueCell(): bool { if ($mergeRange = $this->getMergeRange()) { $mergeRange = Coordinate::splitRange($mergeRange); [$startCell] = $mergeRange[0]; - if ($this->getCoordinate() === $startCell) { - return true; - } + + return $this->getCoordinate() === $startCell; } return false; @@ -579,7 +559,7 @@ class Cell * * @return Cell */ - public function rebindParent(Worksheet $parent) + public function rebindParent(Worksheet $parent): self { $this->parent = $parent->getCellCollection(); @@ -590,10 +570,8 @@ class Cell * Is cell in a specific range? * * @param string $range Cell range (e.g. A1:A1) - * - * @return bool */ - public function isInRange($range) + public function isInRange(string $range): bool { [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range); @@ -614,7 +592,7 @@ class Cell * * @return int Result of comparison (always -1 or 1, never zero!) */ - public static function compareCells(self $a, self $b) + public static function compareCells(self $a, self $b): int { if ($a->getRow() < $b->getRow()) { return -1; @@ -629,10 +607,8 @@ class Cell /** * Get value binder to use. - * - * @return IValueBinder */ - public static function getValueBinder() + public static function getValueBinder(): IValueBinder { if (self::$valueBinder === null) { self::$valueBinder = new DefaultValueBinder(); @@ -655,21 +631,19 @@ class Cell public function __clone() { $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if ((is_object($value)) && ($key != 'parent')) { - $this->$key = clone $value; + foreach ($vars as $propertyName => $propertyValue) { + if ((is_object($propertyValue)) && ($propertyName !== 'parent')) { + $this->$propertyName = clone $propertyValue; } else { - $this->$key = $value; + $this->$propertyName = $propertyValue; } } } /** * Get index to cellXf. - * - * @return int */ - public function getXfIndex() + public function getXfIndex(): int { return $this->xfIndex; } @@ -677,11 +651,9 @@ class Cell /** * Set index to cellXf. * - * @param int $indexValue - * * @return Cell */ - public function setXfIndex($indexValue) + public function setXfIndex(int $indexValue): self { $this->xfIndex = $indexValue; @@ -695,7 +667,7 @@ class Cell * * @return $this */ - public function setFormulaAttributes($attributes) + public function setFormulaAttributes($attributes): self { $this->formulaAttributes = $attributes; diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 20fccf48..03ad1cd4 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -91,10 +91,8 @@ class Cells * Whether the collection holds a cell for the given coordinate. * * @param string $cellCoordinate Coordinate of the cell to check - * - * @return bool */ - public function has($cellCoordinate) + public function has($cellCoordinate): bool { return ($cellCoordinate === $this->currentCoordinate) || isset($this->index[$cellCoordinate]); } @@ -103,10 +101,8 @@ class Cells * Add or update a cell in the collection. * * @param Cell $cell Cell to update - * - * @return Cell */ - public function update(Cell $cell) + public function update(Cell $cell): Cell { return $this->add($cell->getCoordinate(), $cell); } @@ -165,10 +161,8 @@ class Cells /** * Return the column coordinate of the currently active cell object. - * - * @return string */ - public function getCurrentColumn() + public function getCurrentColumn(): string { sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); @@ -177,10 +171,8 @@ class Cells /** * Return the row coordinate of the currently active cell object. - * - * @return int */ - public function getCurrentRow() + public function getCurrentRow(): int { sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); diff --git a/src/PhpSpreadsheet/Collection/CellsFactory.php b/src/PhpSpreadsheet/Collection/CellsFactory.php index 26f18dfc..b3833bd8 100644 --- a/src/PhpSpreadsheet/Collection/CellsFactory.php +++ b/src/PhpSpreadsheet/Collection/CellsFactory.php @@ -12,9 +12,8 @@ abstract class CellsFactory * * @param Worksheet $worksheet Enable cell caching for this worksheet * - * @return Cells * */ - public static function getInstance(Worksheet $worksheet) + public static function getInstance(Worksheet $worksheet): Cells { return new Cells($worksheet, Settings::getCache()); } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index e89043c8..13cd4f61 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1336,7 +1336,7 @@ class Worksheet implements IComparable * * @return Cell Cell that was created */ - public function createNewCell($coordinate) + public function createNewCell($coordinate): Cell { [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate); $cell = new Cell(null, DataType::TYPE_NULL, $this); @@ -2459,10 +2459,8 @@ class Worksheet implements IComparable /** * Show gridlines? - * - * @return bool */ - public function getShowGridlines() + public function getShowGridlines(): bool { return $this->showGridlines; } @@ -2474,7 +2472,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowGridlines($showGridLines) + public function setShowGridlines(bool $showGridLines): self { $this->showGridlines = $showGridLines; @@ -2483,10 +2481,8 @@ class Worksheet implements IComparable /** * Print gridlines? - * - * @return bool */ - public function getPrintGridlines() + public function getPrintGridlines(): bool { return $this->printGridlines; } @@ -2498,7 +2494,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setPrintGridlines($printGridLines) + public function setPrintGridlines(bool $printGridLines): self { $this->printGridlines = $printGridLines; @@ -2507,10 +2503,8 @@ class Worksheet implements IComparable /** * Show row and column headers? - * - * @return bool */ - public function getShowRowColHeaders() + public function getShowRowColHeaders(): bool { return $this->showRowColHeaders; } @@ -2522,7 +2516,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowRowColHeaders($showRowColHeaders) + public function setShowRowColHeaders(bool $showRowColHeaders): self { $this->showRowColHeaders = $showRowColHeaders; @@ -2531,10 +2525,8 @@ class Worksheet implements IComparable /** * Show summary below? (Row/Column outlining). - * - * @return bool */ - public function getShowSummaryBelow() + public function getShowSummaryBelow(): bool { return $this->showSummaryBelow; } @@ -2546,7 +2538,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowSummaryBelow($showSummaryBelow) + public function setShowSummaryBelow(bool $showSummaryBelow): self { $this->showSummaryBelow = $showSummaryBelow; @@ -2555,10 +2547,8 @@ class Worksheet implements IComparable /** * Show summary right? (Row/Column outlining). - * - * @return bool */ - public function getShowSummaryRight() + public function getShowSummaryRight(): bool { return $this->showSummaryRight; } @@ -2570,7 +2560,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowSummaryRight($showSummaryRight) + public function setShowSummaryRight(bool $showSummaryRight): self { $this->showSummaryRight = $showSummaryRight; @@ -2594,7 +2584,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setComments(array $comments) + public function setComments(array $comments): self { $this->comments = $comments; @@ -2609,7 +2599,7 @@ class Worksheet implements IComparable * * @return $this */ - public function removeComment($cellCoordinate) + public function removeComment($cellCoordinate): self { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); @@ -2633,10 +2623,8 @@ class Worksheet implements IComparable * * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. - * - * @return Comment */ - public function getComment($cellCoordinate) + public function getComment($cellCoordinate): Comment { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); @@ -2669,10 +2657,8 @@ class Worksheet implements IComparable * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell - * - * @return Comment */ - public function getCommentByColumnAndRow($columnIndex, $row) + public function getCommentByColumnAndRow($columnIndex, $row): Comment { return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row); } From 0492ea6d8a99ed0b0d3452bd94c9ef6778ab8205 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 12 Aug 2022 18:59:28 -0700 Subject: [PATCH 080/156] Use Only mb_convert_encoding in StringHelper sanitizeUTF8 (#2994) * Test if UConverter Exists Without Autoload Fix #2982. That issue is actually closed, but it did expose a problem. Our test environments all enable php-intl, but that extension isn't a formal requirement for PhpSpreadsheet. Perhaps it ought to be. Nevertheless ... Using UConverter for string translation solved some problems for us. However, it is only available when php-intl is enabled. The code tests if it exists before using it, so no big deal ... except it seems likely that the people reporting the issue not only did not have php-intl, but they do have their own autoloader which issues an exception when the class isn't found. The test for existence of UConverter defaulted to attempting to autoload it if not found. So, on a system without php-intl but with a custom autoloader, there is a problem. Code is changed to suppress autoload when testing UConverter existence. Pending this fix, the workaround for this issue is to enable php-intl. * Minor Improvement Make mb_convert_encoding use same substitution character as UConverter, ensuring consistent results whatever the user's environment. * And Now That I Figured That Out Since mb_convert_encoding can now return the same output as UConverter, we don't need UConverter (or iconv) after all in sanitizeUTF8. --- src/PhpSpreadsheet/Shared/StringHelper.php | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 030df66d..0fe10e4d 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Shared; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use UConverter; class StringHelper { @@ -334,26 +333,13 @@ class StringHelper public static function sanitizeUTF8(string $textValue): string { $textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue); - if (class_exists(UConverter::class)) { - $returnValue = UConverter::transcode($textValue, 'UTF-8', 'UTF-8'); - if ($returnValue !== false) { - return $returnValue; - } - } - // @codeCoverageIgnoreStart - // I don't think any of the code below should ever be executed. - if (self::getIsIconvEnabled()) { - $returnValue = @iconv('UTF-8', 'UTF-8', $textValue); - if ($returnValue !== false) { - return $returnValue; - } - } - + $subst = mb_substitute_character(); // default is question mark + mb_substitute_character(65533); // Unicode substitution character // Phpstan does not think this can return false. $returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); + mb_substitute_character($subst); return $returnValue; - // @codeCoverageIgnoreEnd } /** From f34e0ead2991e07d4868553ea623f4f6f121a5cb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 12 Aug 2022 20:10:45 -0700 Subject: [PATCH 081/156] Add setName Method for Chart (#3001) Addresses a problem identified in issue #2991. Chart name is set in constructor, but there is no method to subsequently change it. This PR adds a method to do so. --- src/PhpSpreadsheet/Chart/Chart.php | 7 +++++++ tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 556b0eff..b962978d 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -188,6 +188,13 @@ class Chart return $this->name; } + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + /** * Get Worksheet. */ diff --git a/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php b/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php index 027ddc53..5c3622ad 100644 --- a/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php @@ -93,8 +93,9 @@ class ChartMethodTest extends TestCase $xAxis, // xAxis $yAxis // yAxis ); - $chart2 = new Chart('chart1'); + $chart2 = new Chart('xyz'); $chart2 + ->setName('chart1') ->setLegend($legend) ->setPlotArea($plotArea) ->setPlotVisibleOnly(true) From 5c13b179a162a87c07f10324743cc84dadadc612 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:14:25 -0700 Subject: [PATCH 082/156] Replace Dev jpgraph/jpgraph with mitoteam/jpgraph (#2997) * Replace Dev jpgraph/jpgraph with mitoteam/jpgraph PR #2979 added support for mitoteam/jpgraph as an alternative to jpgraph/jpgraph. The package jpgraph/jpgraph is abandoned in composer, and the version loaded with composer has been unusable for some time. This PR removes the dev requirement for jpgraph/jpgraph, and adds a dev requirement for mitoteam/jpgraph in its place. With a usable graph library, a number of tests and samples that had been disabled are now re-enabled. A lot of new functionality has been added to Charts recently. Some of that new code has exposed bugs in JpgraphRendererBase. I have fixed those where I could. A handful of exceptions remain; I will investigate, and hopefully fix, those over time, but I don't feel it is necessary to fix them all before installing this PR - we are already way ahead of the game with the graphs that are working. Three members had been ignoring code coverage in whole or in part because of the unavailability of a usable graph libray. Code coverage is restored in them. I am relieved to report that, although they aren't completely covered, adding them did not reduce code coverage by much - it is still over 90.4%. I took a look at JpgraphRendererBase and Phpstan. Phpstan reports 128 problems. When I added some docblocks to correct some of those, the number increased to 284. Sigh. I will investigate over time, but, for now, we will still suppress Phpstan for JpgraphRendererBase. I do not find a License file for mitoteam. However, there also wasn't one for jpgraph in the first place. Based on that and the discussion in #2996 (mitoteam will be used in exactly the same manner as mpdf), I don't think this is a problem. IANAL. * PHP 8.2 Problems Tons of "cannot create dynamic property" deprecations in jpgraph. Disable the test with most of those for now; leave the two with only a handful of messages enabled. * Correct Failures in 2 Stock Charts Down to 6 templates on which Render fails. --- composer.json | 9 +- composer.lock | 88 ++++++++++--------- phpstan.neon.dist | 1 - samples/Chart/32_Chart_read_write_HTML.php | 3 +- samples/Chart/32_Chart_read_write_PDF.php | 3 +- samples/Chart/35_Chart_render.php | 26 ++++-- src/PhpSpreadsheet/Chart/Chart.php | 4 - .../Chart/Renderer/JpGraphRendererBase.php | 22 +++-- .../Chart/Renderer/MtJpGraphRenderer.php | 2 - src/PhpSpreadsheet/Writer/Html.php | 10 --- .../PhpSpreadsheetTests/Chart/RenderTest.php | 15 ++++ .../PhpSpreadsheetTests/Helper/SampleTest.php | 9 +- 12 files changed, 110 insertions(+), 82 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Chart/RenderTest.php diff --git a/composer.json b/composer.json index 16991514..4ef1c1b4 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "jpgraph/jpgraph": "^4.0", + "mitoteam/jpgraph": "^10.1", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", @@ -91,10 +91,11 @@ "tecnickcom/tcpdf": "^6.4" }, "suggest": { + "ext-intl": "PHP Internationalization Functions", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet fully support PHP8)", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 3f82754d..bbbd0c75 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6cbb20f8d8f2daae0aeb72431cda0980", + "content-hash": "fc6928651785d4bb82d727d22f50227d", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1192,47 +1192,6 @@ ], "time": "2021-12-11T16:25:08+00:00" }, - { - "name": "jpgraph/jpgraph", - "version": "4.0.2", - "source": { - "type": "git", - "url": "https://github.com/ztec/JpGraph.git", - "reference": "e82db7da6a546d3926c24c9a346226da7aa49094" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ztec/JpGraph/zipball/e82db7da6a546d3926c24c9a346226da7aa49094", - "reference": "e82db7da6a546d3926c24c9a346226da7aa49094", - "shasum": "" - }, - "type": "library", - "autoload": { - "classmap": [ - "lib/JpGraph.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "QPL 1.0" - ], - "authors": [ - { - "name": "JpGraph team" - } - ], - "description": "jpGraph, library to make graphs and charts", - "homepage": "http://jpgraph.net/", - "keywords": [ - "chart", - "data", - "graph", - "jpgraph", - "pie" - ], - "abandoned": true, - "time": "2017-02-23T09:44:15+00:00" - }, { "name": "masterminds/html5", "version": "2.7.5", @@ -1298,6 +1257,49 @@ ], "time": "2021-07-01T14:25:37+00:00" }, + { + "name": "mitoteam/jpgraph", + "version": "10.1.3", + "source": { + "type": "git", + "url": "https://github.com/mitoteam/jpgraph.git", + "reference": "425a2a0f0c97a28fe0aca60a4384ce85880e438a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/425a2a0f0c97a28fe0aca60a4384ce85880e438a", + "reference": "425a2a0f0c97a28fe0aca60a4384ce85880e438a", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/MtJpGraph.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "QPL-1.0" + ], + "authors": [ + { + "name": "JpGraph team" + } + ], + "description": "Composer compatible version of JpGraph library with PHP 8.1 support", + "homepage": "https://github.com/mitoteam/jpgraph", + "keywords": [ + "jpgraph" + ], + "support": { + "issues": "https://github.com/mitoteam/jpgraph/issues", + "source": "https://github.com/mitoteam/jpgraph/tree/10.1.3" + }, + "time": "2022-07-05T16:46:34+00:00" + }, { "name": "mpdf/mpdf", "version": "v8.1.1", @@ -5273,5 +5275,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.2.0" } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 92767872..5cac36a1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,7 +12,6 @@ parameters: excludePaths: - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php - - src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php parallel: processTimeout: 300.0 checkMissingIterableValueType: false diff --git a/samples/Chart/32_Chart_read_write_HTML.php b/samples/Chart/32_Chart_read_write_HTML.php index 5febbf93..90d61c5d 100644 --- a/samples/Chart/32_Chart_read_write_HTML.php +++ b/samples/Chart/32_Chart_read_write_HTML.php @@ -6,7 +6,8 @@ use PhpOffice\PhpSpreadsheet\Settings; require __DIR__ . '/../Header.php'; // Change these values to select the Rendering library that you wish to use -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +//Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; $inputFileNames = __DIR__ . '/../templates/36write*.xlsx'; diff --git a/samples/Chart/32_Chart_read_write_PDF.php b/samples/Chart/32_Chart_read_write_PDF.php index ee3ad0e0..214c3d38 100644 --- a/samples/Chart/32_Chart_read_write_PDF.php +++ b/samples/Chart/32_Chart_read_write_PDF.php @@ -8,7 +8,8 @@ require __DIR__ . '/../Header.php'; IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf::class); // Change these values to select the Rendering library that you wish to use -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +//Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; $inputFileNames = __DIR__ . '/../templates/36write*.xlsx'; diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index ebab16a7..2376008c 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -5,16 +5,13 @@ use PhpOffice\PhpSpreadsheet\Settings; require __DIR__ . '/../Header.php'; -if (PHP_VERSION_ID >= 80000) { - $helper->log('Jpgraph no longer runs against PHP8'); - exit; -} - // Change these values to select the Rendering library that you wish to use -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +//Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; $inputFileNames = __DIR__ . '/../templates/32readwrite*[0-9].xlsx'; +//$inputFileNames = __DIR__ . '/../templates/32readwriteStockChart5.xlsx'; if ((isset($argc)) && ($argc > 1)) { $inputFileNames = []; @@ -24,6 +21,18 @@ if ((isset($argc)) && ($argc > 1)) { } else { $inputFileNames = glob($inputFileNames); } +if (count($inputFileNames) === 1) { + $unresolvedErrors = []; +} else { + $unresolvedErrors = [ + '32readwriteBubbleChart2.xlsx', + '32readwritePieChart3.xlsx', + '32readwritePieChart4.xlsx', + '32readwritePieChart3D1.xlsx', + '32readwritePieChartExploded1.xlsx', + '32readwritePieChartExploded3D1.xlsx', + ]; +} foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -32,6 +41,11 @@ foreach ($inputFileNames as $inputFileName) { continue; } + if (in_array($inputFileNameShort, $unresolvedErrors, true)) { + $helper->log('File ' . $inputFileNameShort . ' does not yet work with this script'); + + continue; + } $helper->log("Load Test from $inputFileType file " . $inputFileNameShort); diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index b962978d..036338c6 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -661,14 +661,10 @@ class Chart /** * Render the chart to given file (or stream). - * Unable to cover code until a usable current version of JpGraph - * is made available through Composer. * * @param string $outputDestination Name of the file render to * * @return bool true on success - * - * @codeCoverageIgnore */ public function render($outputDestination = null) { diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index 4d5526b8..f0ce1f65 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -277,7 +277,8 @@ abstract class JpGraphRendererBase implements IRenderer { $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); @@ -294,8 +295,9 @@ abstract class JpGraphRendererBase implements IRenderer // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$i]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointMarker(); if ($grouping == 'percentStacked') { $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); @@ -324,7 +326,7 @@ abstract class JpGraphRendererBase implements IRenderer // Set the appropriate plot marker $this->formatPointMarker($seriesPlot, $marker); } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($index)->getDataValue(); $seriesPlot->SetLegend($dataLabel); $seriesPlots[] = $seriesPlot; @@ -347,7 +349,8 @@ abstract class JpGraphRendererBase implements IRenderer } $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); @@ -371,7 +374,8 @@ abstract class JpGraphRendererBase implements IRenderer // Loop through each data series in turn for ($j = 0; $j < $seriesCount; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$j]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); if ($grouping == 'percentStacked') { $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); } @@ -535,6 +539,10 @@ abstract class JpGraphRendererBase implements IRenderer $dataValues = []; // Loop through each data series in turn and build the plot arrays foreach ($plotOrder as $i => $v) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v); + if ($dataValuesX === false) { + continue; + } $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); foreach ($dataValuesX as $j => $dataValueX) { $dataValues[$plotOrder[$i]][$j] = $dataValueX; @@ -549,7 +557,7 @@ abstract class JpGraphRendererBase implements IRenderer $jMax = count($dataValues[0]); for ($j = 0; $j < $jMax; ++$j) { for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesPlot[] = $dataValues[$i][$j]; + $dataValuesPlot[] = $dataValues[$i][$j] ?? null; } } diff --git a/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php index 3fef3b6e..e1f0f90a 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php +++ b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php @@ -9,8 +9,6 @@ namespace PhpOffice\PhpSpreadsheet\Chart\Renderer; * https://packagist.org/packages/mitoteam/jpgraph * * This package is up to date for August 2022 and has PHP 8.1 support. - * - * @codeCoverageIgnore */ class MtJpGraphRenderer extends JpGraphRendererBase { diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 6e51b7a8..f6c34a8a 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -551,15 +551,10 @@ class Html extends BaseWriter * Extend Row if chart is placed after nominal end of row. * This code should be exercised by sample: * Chart/32_Chart_read_write_PDF.php. - * However, that test is suppressed due to out-of-date - * Jpgraph code issuing warnings. So, don't measure - * code coverage for this function till that is fixed. * * @param int $row Row to check for charts * * @return array - * - * @codeCoverageIgnore */ private function extendRowsForCharts(Worksheet $worksheet, int $row) { @@ -725,11 +720,6 @@ class Html extends BaseWriter * Generate chart tag in cell. * This code should be exercised by sample: * Chart/32_Chart_read_write_PDF.php. - * However, that test is suppressed due to out-of-date - * Jpgraph code issuing warnings. So, don't measure - * code coverage for this function till that is fixed. - * - * @codeCoverageIgnore */ private function writeChartInCell(Worksheet $worksheet, string $coordinates): string { diff --git a/tests/PhpSpreadsheetTests/Chart/RenderTest.php b/tests/PhpSpreadsheetTests/Chart/RenderTest.php new file mode 100644 index 00000000..f0eaffee --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/RenderTest.php @@ -0,0 +1,15 @@ +render()); + } +} diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index 50a650f8..a104e8ff 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -26,10 +26,13 @@ class SampleTest extends TestCase public function providerSample(): array { $skipped = [ - 'Chart/32_Chart_read_write_PDF.php', // Unfortunately JpGraph is not up to date for latest PHP and raise many warnings - 'Chart/32_Chart_read_write_HTML.php', // idem - 'Chart/35_Chart_render.php', // idem ]; + if (PHP_VERSION_ID >= 80200) { + // Hopefully temporary. Continue to try + // 32_chart_read_write_PDF/HTML + // so as not to lose track of the problem. + $skipped[] = 'Chart/35_Chart_render.php'; + } // Unfortunately some tests are too long to run with code-coverage // analysis on GitHub Actions, so we need to exclude them From fadfb727bf14cd098f55f1d4329160399afae058 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:28:22 -0700 Subject: [PATCH 083/156] Minor Changes for Mpdf, Dompdf (#3002) See discussion in #2999. Mpdf is not acknowledging the styling that we're using to hide table rows. I have opened an issue with them, but enclosing the cells in the hidden row inside a div with appropriate css does seem to be a workaround, and that can be incorporated into PhpSpreadsheet. It's kludgey, and it isn't even valid HTML, but ... Mpdf also doesn't like the addition of the ```file:///``` prefix when using local images from Windows (sample 21). Results are better when that prefix is not added. Dompdf seemed to have problems with sample 21 images on both Windows and Unix, with or without the file prefix. It does, however, support data urls for both, so is changed to embed images. It's still not perfect - the image seems truncated to the row height - but the results are better. I will continue to research, but may proceed as-is if I don't find anything better to do. Html Writer was producing a file with mixed line endings on Windows. This didn't cause any harm, but it seems a bit sloppy. It is changed to always use PHP_EOL as a line ending. --- samples/Pdf/21a_Pdf.php | 2 + src/PhpSpreadsheet/Writer/Html.php | 58 ++++++++++++++---------- src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 7 +++ 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/samples/Pdf/21a_Pdf.php b/samples/Pdf/21a_Pdf.php index b5572afe..33b61c9f 100644 --- a/samples/Pdf/21a_Pdf.php +++ b/samples/Pdf/21a_Pdf.php @@ -12,6 +12,8 @@ $spreadsheet->getActiveSheet()->setShowGridLines(false); $helper->log('Set orientation to landscape'); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); $spreadsheet->setActiveSheetIndex(0)->setPrintGridlines(true); +// Issue 2299 - mpdf can't handle hide rows without kludge +$spreadsheet->getActiveSheet()->getRowDimension(2)->setVisible(false); function changeGridlines(string $html): string { diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index f6c34a8a..362eae00 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -54,7 +54,7 @@ class Html extends BaseWriter * * @var bool */ - private $embedImages = false; + protected $embedImages = false; /** * Use inline CSS? @@ -630,11 +630,12 @@ class Html extends BaseWriter * * @return string */ - public static function winFileToUrl($filename) + public static function winFileToUrl($filename, bool $mpdf = false) { // Windows filename if (substr($filename, 1, 2) === ':\\') { - $filename = 'file:///' . str_replace('\\', '/', $filename); + $protocol = $mpdf ? '' : 'file:///'; + $filename = $protocol . str_replace('\\', '/', $filename); } return $filename; @@ -676,9 +677,9 @@ class Html extends BaseWriter $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); $html .= PHP_EOL; - $imageData = self::winFileToUrl($filename); + $imageData = self::winFileToUrl($filename, $this->isMPdf); - if (($this->embedImages && !$this->isPdf) || substr($imageData, 0, 6) === 'zip://') { + if ($this->embedImages || substr($imageData, 0, 6) === 'zip://') { $picture = @file_get_contents($filename); if ($picture !== false) { $imageDetails = getimagesize($filename); @@ -1160,9 +1161,9 @@ class Html extends BaseWriter $html = ''; $id = $showid ? "id='sheet$sheetIndex'" : ''; if ($showid) { - $html .= "

\n"; + $html .= "
" . PHP_EOL; } else { - $html .= "
\n"; + $html .= "
" . PHP_EOL; } $this->generateTableTag($worksheet, $id, $html, $sheetIndex); @@ -1457,6 +1458,10 @@ class Html extends BaseWriter // Sheet index $sheetIndex = $worksheet->getParent()->getIndex($worksheet); $html = $this->generateRowStart($worksheet, $sheetIndex, $row); + $generateDiv = $this->isMPdf && $worksheet->getRowDimension($row + 1)->getVisible() === false; + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } // Write cells $colNum = 0; @@ -1504,6 +1509,9 @@ class Html extends BaseWriter } // Write row end + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } $html .= ' ' . PHP_EOL; // Return @@ -1834,26 +1842,26 @@ class Html extends BaseWriter } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { $htmlPage .= 'size: portrait; '; } - $htmlPage .= "}\n"; + $htmlPage .= '}' . PHP_EOL; ++$sheetId; } - $htmlPage .= <<div {margin-top: 5px;} - body>div:first-child {margin-top: 0;} - .scrpgbrk {margin-top: 1px;} -} -@media print { - .gridlinesp td {border: 1px solid black;} - .gridlinesp th {border: 1px solid black;} - .navigation {display: none;} -} - -EOF; + $htmlPage .= implode(PHP_EOL, [ + '.navigation {page-break-after: always;}', + '.scrpgbrk, div + div {page-break-before: always;}', + '@media screen {', + ' .gridlines td {border: 1px solid black;}', + ' .gridlines th {border: 1px solid black;}', + ' body>div {margin-top: 5px;}', + ' body>div:first-child {margin-top: 0;}', + ' .scrpgbrk {margin-top: 1px;}', + '}', + '@media print {', + ' .gridlinesp td {border: 1px solid black;}', + ' .gridlinesp th {border: 1px solid black;}', + ' .navigation {display: none;}', + '}', + '', + ]); $htmlPage .= $generateSurroundingHTML ? ('' . PHP_EOL) : ''; return $htmlPage; diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index bf9e28cb..cd17cccf 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -7,6 +7,13 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Dompdf extends Pdf { + /** + * embed images, or link to images. + * + * @var bool + */ + protected $embedImages = true; + /** * Gets the implementation of external PDF library that should be used. * From bb072d1ca7529b70d98f1015fe23df4b70ab5419 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 14 Aug 2022 10:57:34 -0700 Subject: [PATCH 084/156] Upgrade Dev TCPDF to 6.5 (#3006) * Upgrade Dev TCPDF to 6.5 Implementation of https://github.com/tecnickcom/TCPDF/pull/467, which is available in just-released Tcpdf 6.5, will improve look of Tcpdf rendering for PhpSpreadsheet. Fix #1164. One test had been suppressed for Tcpdf, ostensibly because it was not compatible with Php8. As it turns out, the PhpSpreadsheet code which invokes Tcpdf was (harmlessly) incorrect, so the Php8 issue was actually with PhpSpreadsheet, not Tcpdf. That code is corrected, and the test is no longer suppressed. * Update Change Log Pick up some earlier changes as well as this one, and deprecations which had been omitted from the 1.24 change log. --- CHANGELOG.md | 16 +++++++++++++++- composer.json | 4 ++-- composer.lock | 16 ++++++++++------ src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 2 +- .../Functional/StreamTest.php | 8 +------- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00167722..4e19cb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Implementation of the `ARRAYTOTEXT()` Excel Function - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines. ### Changed @@ -20,7 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Deprecated -- Nothing +- Axis getLineProperty deprecated in favor of getLineColorProperty. +- Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated. +- Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated. +- ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed). +- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT. +- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead. ### Removed @@ -30,6 +36,14 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) +- Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) +- Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960) +- Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971) +- Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950) +- Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976) +- Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) +- Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) +- Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) ## 1.24.1 - 2022-07-18 diff --git a/composer.json b/composer.json index 4ef1c1b4..cda2dc96 100644 --- a/composer.json +++ b/composer.json @@ -88,13 +88,13 @@ "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.4" + "tecnickcom/tcpdf": "6.5" }, "suggest": { "ext-intl": "PHP Internationalization Functions", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet fully support PHP8)", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer", "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" }, "autoload": { diff --git a/composer.lock b/composer.lock index bbbd0c75..f54b9c58 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fc6928651785d4bb82d727d22f50227d", + "content-hash": "05bd955232ea7ceab5b849e990f593bd", "packages": [ { "name": "ezyang/htmlpurifier", @@ -5084,16 +5084,16 @@ }, { "name": "tecnickcom/tcpdf", - "version": "6.4.4", + "version": "6.5.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "42cd0f9786af7e5db4fcedaa66f717b0d0032320" + "reference": "cc54c1503685e618b23922f53635f46e87653662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/42cd0f9786af7e5db4fcedaa66f717b0d0032320", - "reference": "42cd0f9786af7e5db4fcedaa66f717b0d0032320", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/cc54c1503685e618b23922f53635f46e87653662", + "reference": "cc54c1503685e618b23922f53635f46e87653662", "shasum": "" }, "require": { @@ -5142,13 +5142,17 @@ "pdf417", "qrcode" ], + "support": { + "issues": "https://github.com/tecnickcom/TCPDF/issues", + "source": "https://github.com/tecnickcom/TCPDF/tree/6.5.0" + }, "funding": [ { "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", "type": "custom" } ], - "time": "2021-12-31T08:39:24+00:00" + "time": "2022-08-12T07:50:54+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index d29d4764..aefc6b56 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -77,7 +77,7 @@ class Tcpdf extends Pdf $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); // Write to file - fwrite($fileHandle, $pdf->output($filename, 'S')); + fwrite($fileHandle, $pdf->output('', 'S')); parent::restoreStateAfterSave(); } diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php index a84a2490..05b87ab9 100644 --- a/tests/PhpSpreadsheetTests/Functional/StreamTest.php +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -18,15 +18,9 @@ class StreamTest extends TestCase ['Html'], ['Mpdf'], ['Dompdf'], + ['Tcpdf'], ]; - if (\PHP_VERSION_ID < 80000) { - $providerFormats = array_merge( - $providerFormats, - [['Tcpdf']] - ); - } - return $providerFormats; } From cd6629890162f447edd2c343c5b45fe0dfa8f8fe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 14 Aug 2022 23:43:52 +0200 Subject: [PATCH 085/156] Adjust `extractAllCellReferencesInRange()` method to allow a worksheet in the reference --- src/PhpSpreadsheet/Cell/Coordinate.php | 21 +++++++++++- src/PhpSpreadsheet/Worksheet/Worksheet.php | 23 +++++++++---- testing/cellReferenceTest.php | 28 ++++++++++++++++ .../CellExtractAllCellReferencesInRange.php | 33 +++++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 testing/cellReferenceTest.php diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 50678397..2fca6212 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -349,6 +350,19 @@ abstract class Coordinate */ public static function extractAllCellReferencesInRange($cellRange): array { + if (substr_count($cellRange, '!') > 1) { + throw new Exception('3-D Range References are not supported'); + } + + [$worksheet, $cellRange] = Worksheet::extractSheetTitle($cellRange, true); + $quoted = ''; + if ($worksheet > '') { + $quoted = Worksheet::nameRequiresQuotes($worksheet) ? "'" : ''; + if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") { + $worksheet = substr($worksheet, 1, -1); + } + $worksheet = str_replace("'", "''", $worksheet); + } [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange); $cells = []; @@ -364,7 +378,12 @@ abstract class Coordinate $cellList = array_merge(...$cells); - return self::sortCellReferenceArray($cellList); + return array_map( + function ($cellAddress) use ($worksheet, $quoted) { + return ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress; + }, + self::sortCellReferenceArray($cellList) + ); } private static function processRangeSetOperators(array $operators, array $cells): array diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index e89043c8..3464a321 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -32,14 +32,16 @@ use PhpOffice\PhpSpreadsheet\Style\Style; class Worksheet implements IComparable { // Break types - const BREAK_NONE = 0; - const BREAK_ROW = 1; - const BREAK_COLUMN = 2; + public const BREAK_NONE = 0; + public const BREAK_ROW = 1; + public const BREAK_COLUMN = 2; // Sheet state - const SHEETSTATE_VISIBLE = 'visible'; - const SHEETSTATE_HIDDEN = 'hidden'; - const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const SHEETSTATE_VISIBLE = 'visible'; + public const SHEETSTATE_HIDDEN = 'hidden'; + public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** * Maximum 31 characters allowed for sheet title. @@ -3051,7 +3053,11 @@ class Worksheet implements IComparable * Extract worksheet title from range. * * Example: extractSheetTitle("testSheet!A1") ==> 'A1' + * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3' * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1']; + * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3']; + * Example: extractSheetTitle("A1", true) ==> ['', 'A1']; + * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3'] * * @param string $range Range to extract title from * @param bool $returnRange Return range? (see example) @@ -3450,4 +3456,9 @@ class Worksheet implements IComparable { return $this->codeName !== null; } + + public static function nameRequiresQuotes(string $sheetName): bool + { + return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1; + } } diff --git a/testing/cellReferenceTest.php b/testing/cellReferenceTest.php new file mode 100644 index 00000000..c8d1659c --- /dev/null +++ b/testing/cellReferenceTest.php @@ -0,0 +1,28 @@ + Date: Mon, 15 Aug 2022 06:33:31 +0200 Subject: [PATCH 086/156] Fix phpstan baseline --- phpstan-baseline.neon | 5 ----- 1 file changed, 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 596e1e10..c6d769d5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1060,11 +1060,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/TextData/Text.php - - - message: "#^Elseif branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\:\\:getFormulaAttributes\\(\\) has no return type specified\\.$#" count: 1 From d7da20610323508de897e3640445e8f8b842c396 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 15 Aug 2022 07:47:27 +0200 Subject: [PATCH 087/156] Remove file accidentally committed --- testing/cellReferenceTest.php | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 testing/cellReferenceTest.php diff --git a/testing/cellReferenceTest.php b/testing/cellReferenceTest.php deleted file mode 100644 index c8d1659c..00000000 --- a/testing/cellReferenceTest.php +++ /dev/null @@ -1,28 +0,0 @@ - Date: Fri, 19 Aug 2022 11:28:51 +0200 Subject: [PATCH 088/156] Bugfix for Issue #3013, Named ranges not usable as anchors in OFFSET function --- src/PhpSpreadsheet/Calculation/Functions.php | 2 +- .../Calculation/LookupRef/Offset.php | 11 ++++++ .../Functions/LookupRef/HLookupTest.php | 39 +++++++++++++++++++ .../Functions/LookupRef/OffsetTest.php | 15 +++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index b3d6998c..172f2022 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -687,7 +687,7 @@ class Functions $worksheet2 = $defined->getWorkSheet(); if (!$defined->isFormula() && $worksheet2 !== null) { $coordinate = "'" . $worksheet2->getTitle() . "'!" . - (string) preg_replace('/^=/', '', $defined->getValue()); + (string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue())); } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index 9f3377f6..02a25581 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -99,6 +99,8 @@ class Offset private static function extractWorksheet($cellAddress, Cell $cell): array { + $cellAddress = self::assessCellAddress($cellAddress, $cell); + $sheetName = ''; if (strpos($cellAddress, '!') !== false) { [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); @@ -112,6 +114,15 @@ class Offset return [$cellAddress, $worksheet]; } + private static function assessCellAddress(string $cellAddress, Cell $cell): string + { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $cellAddress) !== false) { + $cellAddress = Functions::expandDefinedName($cellAddress, $cell); + } + + return $cellAddress; + } + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns) { $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php index 0ce3513b..084e3d9f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -91,6 +92,44 @@ class HLookupTest extends TestCase self::assertSame($expectedResult, $result); } + /** + * @dataProvider providerHLookupNamedRange + */ + public function testHLookupNamedRange(string $expectedResult, string $cellAddress): void + { + $lookupData = [ + ['Rating', 1, 2, 3, 4], + ['Level', 'Poor', 'Average', 'Good', 'Excellent'], + ]; + $formData = [ + ['Category', 'Rating', 'Level'], + ['Service', 2, '=HLOOKUP(C5,Lookup_Table,2,FALSE)'], + ['Quality', 3, '=HLOOKUP(C6,Lookup_Table,2,FALSE)'], + ['Value', 4, '=HLOOKUP(C7,Lookup_Table,2,FALSE)'], + ['Cleanliness', 3, '=HLOOKUP(C8,Lookup_Table,2,FALSE)'], + ]; + + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($lookupData, null, 'F4'); + $worksheet->fromArray($formData, null, 'B4'); + + $spreadsheet->addNamedRange(new NamedRange('Lookup_Table', $worksheet, '=$G$4:$J$5')); + + $result = $worksheet->getCell($cellAddress)->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerHLookupNamedRange(): array + { + return [ + ['Average', 'D5'], + ['Good', 'D6'], + ['Excellent', 'D7'], + ['Good', 'D8'], + ]; + } + /** * @dataProvider providerHLookupArray */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php index e0786505..22787e25 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\NamedRange; class OffsetTest extends AllSetupTeardown { @@ -46,4 +47,18 @@ class OffsetTest extends AllSetupTeardown $sheet->getCell('A5')->setValue('=OFFSET(C1, 0, 0)'); self::assertSame(5, $sheet->getCell('A5')->getCalculatedValue()); } + + public function testOffsetNamedRange(): void + { + $workSheet = $this->getSheet(); + $workSheet->setCellValue('A1', 1); + $workSheet->setCellValue('A2', 2); + + $this->getSpreadsheet()->addNamedRange(new NamedRange('demo', $workSheet, '=$A$1')); + + $workSheet->setCellValue('B1', '=demo'); + $workSheet->setCellValue('B2', '=OFFSET(demo, 1, 0)'); + + self::assertSame(2, $workSheet->getCell('B2')->getCalculatedValue()); + } } From 4e82b55f3718ced5bf8fc8b12e789482c741529a Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 19 Aug 2022 14:26:39 +0200 Subject: [PATCH 089/156] Update Change Log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e19cb2d..62fdaf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) - Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) From e67de6f300b687721b3359cdb6b0e421b26f5bdc Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 20 Aug 2022 21:27:05 +0200 Subject: [PATCH 090/156] Support for SimpleCache Interface versions 1.0, 2.0 and 3.0; to stop people moaning; even though it requires a second implementation of the Memory cache for Cells --- CHANGELOG.md | 1 + composer.json | 2 +- phpstan-baseline.neon | 5 - src/PhpSpreadsheet/Collection/Cells.php | 4 +- .../{Memory.php => Memory/SimpleCache1.php} | 7 +- .../Collection/Memory/SimpleCache3.php | 109 ++++++++++++++++++ .../Reader/Security/XmlScanner.php | 2 +- src/PhpSpreadsheet/Settings.php | 10 +- .../Collection/CellsTest.php | 10 +- 9 files changed, 137 insertions(+), 13 deletions(-) rename src/PhpSpreadsheet/Collection/{Memory.php => Memory/SimpleCache1.php} (93%) create mode 100644 src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fdaf91..da29bc8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0 - Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) ### Changed diff --git a/composer.json b/composer.json index cda2dc96..46ee56b9 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0 || ^2.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c6d769d5..7c0070d7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1100,11 +1100,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Cell/Coordinate.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Memory\\:\\:\\$cache has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Collection/Memory.php - - message: "#^Parameter \\#1 \\$namedRange of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:addNamedRange\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\NamedRange, \\$this\\(PhpOffice\\\\PhpSpreadsheet\\\\DefinedName\\) given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 03ad1cd4..82c9ae1a 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -268,7 +268,9 @@ class Cells */ private function getUniqueID() { - return Settings::getCache() instanceof Memory + $cacheType = Settings::getCache(); + + return ($cacheType instanceof Memory\SimpleCache1 || $cacheType instanceof Memory\SimpleCache3) ? random_bytes(7) . ':' : uniqid('phpspreadsheet.', true) . '.'; } diff --git a/src/PhpSpreadsheet/Collection/Memory.php b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php similarity index 93% rename from src/PhpSpreadsheet/Collection/Memory.php rename to src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php index 2690ab7d..a0eb6ec2 100644 --- a/src/PhpSpreadsheet/Collection/Memory.php +++ b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php @@ -1,6 +1,6 @@ cache = []; + + return true; + } + + /** + * @param string $key + */ + public function delete($key): bool + { + unset($this->cache[$key]); + + return true; + } + + /** + * @param iterable $keys + */ + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + /** + * @param string $key + * @param mixed $default + */ + public function get($key, $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + /** + * @param iterable $keys + * @param mixed $default + */ + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + /** + * @param string $key + */ + public function has($key): bool + { + return array_key_exists($key, $this->cache); + } + + /** + * @param string $key + * @param mixed $value + * @param null|DateInterval|int $ttl + */ + public function set($key, $value, $ttl = null): bool + { + $this->cache[$key] = $value; + + return true; + } + + /** + * @param iterable $values + * @param null|DateInterval|int $ttl + */ + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index 8155b838..40008d01 100644 --- a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -52,7 +52,7 @@ class XmlScanner public static function threadSafeLibxmlDisableEntityLoaderAvailability() { - if (PHP_MAJOR_VERSION == 7) { + if (PHP_MAJOR_VERSION === 7) { switch (PHP_MINOR_VERSION) { case 2: return PHP_RELEASE_VERSION >= 1; diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 5fbbadb6..3282a596 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Memory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\CacheInterface; +use ReflectionClass; class Settings { @@ -161,12 +162,19 @@ class Settings public static function getCache(): CacheInterface { if (!self::$cache) { - self::$cache = new Memory(); + self::$cache = self::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(); } return self::$cache; } + public static function useSimpleCacheVersion3(): bool + { + return + PHP_MAJOR_VERSION === 8 && + (new ReflectionClass(CacheInterface::class))->getMethod('get')->getReturnType() !== null; + } + /** * Set the HTTP client implementation to be used for network request. */ diff --git a/tests/PhpSpreadsheetTests/Collection/CellsTest.php b/tests/PhpSpreadsheetTests/Collection/CellsTest.php index 5731d581..9e662b92 100644 --- a/tests/PhpSpreadsheetTests/Collection/CellsTest.php +++ b/tests/PhpSpreadsheetTests/Collection/CellsTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Collection; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Collection\Memory; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -107,7 +108,10 @@ class CellsTest extends TestCase $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); $collection = $this->getMockBuilder(Cells::class) - ->setConstructorArgs([new Worksheet(), new Memory()]) + ->setConstructorArgs([ + new Worksheet(), + Settings::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(), + ]) ->onlyMethods(['has']) ->getMock(); @@ -121,7 +125,9 @@ class CellsTest extends TestCase { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $cache = $this->createMock(Memory::class); + $cache = $this->createMock( + Settings::useSimpleCacheVersion3() ? Memory\SimpleCache3::class : Memory\SimpleCache1::class + ); $cell = $this->createMock(Cell::class); $cache->method('set') ->willReturn(false); From d55978cf931ced2bc9f0d982bb6b8269fbace1f9 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 20 Aug 2022 19:58:43 -0700 Subject: [PATCH 091/156] Correct Namespaces in 11 Tests (#3020) The setup for unit testing in Github in the "Install dependencies" log reports 11 members as "does not comply with with psr-4 autoloading standard." In each case, it is because the test namespace does not match the directory; in most cases, it was caused by the member being moved from one directory to another without changing the namespace declaration. No harm results from these problems, but there's also no reason to not correct them. --- tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php | 2 +- tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php | 2 +- tests/PhpSpreadsheetTests/Chart/PieFillTest.php | 2 +- tests/PhpSpreadsheetTests/Chart/RenderTest.php | 2 +- .../PhpSpreadsheetTests/Reader/Xlsx/NamespaceIssue2109bTest.php | 2 +- tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php | 2 +- .../PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php | 2 +- tests/PhpSpreadsheetTests/Reader/Xlsx/NamespacePurlTest.php | 2 +- tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php | 2 +- .../Style/ConditionalFormatting/Wizard/DateValueWizardTest.php | 2 +- tests/PhpSpreadsheetTests/Writer/Mpdf/ImageCopyPdfTest.php | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php index f8f02f0e..3edd22c8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php @@ -1,6 +1,6 @@ Date: Wed, 24 Aug 2022 08:33:57 +0200 Subject: [PATCH 092/156] Minor changes, mainly cosmetic --- composer.lock | 1014 ++++++++--------- samples/Calculations/LookupRef/VLOOKUP.php | 27 +- samples/Chart/33_Chart_create_scatter2.php | 2 +- samples/Chart/33_Chart_create_scatter3.php | 2 +- .../33_Chart_create_scatter5_trendlines.php | 2 +- .../Calculation/Calculation.php | 8 +- .../Calculation/Engine/Logger.php | 6 +- .../Calculation/MathTrig/Random.php | 2 +- .../Calculation/Statistical/Averages.php | 2 +- .../Statistical/Distributions/ChiSquared.php | 2 +- .../Chart/Renderer/JpGraphRendererBase.php | 18 +- src/PhpSpreadsheet/Reader/Xls.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 7 +- .../Shared/JAMA/EigenvalueDecomposition.php | 6 +- src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 14 +- .../JAMA/SingularValueDecomposition.php | 6 +- src/PhpSpreadsheet/Writer/Xls/Parser.php | 2 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 2 +- 18 files changed, 560 insertions(+), 564 deletions(-) diff --git a/composer.lock b/composer.lock index f54b9c58..37e2dfa8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "05bd955232ea7ceab5b849e990f593bd", + "content-hash": "dd19bb54ddc39f5b24f564818cb46c7e", "packages": [ { "name": "ezyang/htmlpurifier", @@ -51,33 +51,39 @@ "keywords": [ "html" ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + }, "time": "2021-12-25T01:21:49+00:00" }, { "name": "maennchen/zipstream-php", - "version": "2.1.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", - "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/211e9ba1530ea5260b45d90c9ea252f56ec52729", + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729", "shasum": "" }, "require": { "myclabs/php-enum": "^1.5", - "php": ">= 7.1", + "php": "^7.4 || ^8.0", "psr/http-message": "^1.0", "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { "ext-zip": "*", - "guzzlehttp/guzzle": ">= 6.3", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": ">= 7.5" + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^4.1" }, "type": "library", "autoload": { @@ -112,13 +118,17 @@ "stream", "zip" ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.1" + }, "funding": [ { "url": "https://opencollective.com/zipstream", "type": "open_collective" } ], - "time": "2020-05-30T13:11:16+00:00" + "time": "2022-05-18T15:52:06+00:00" }, { "name": "markbaker/complex", @@ -165,6 +175,10 @@ "complex", "mathematics" ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.1" + }, "time": "2021-06-29T15:32:53+00:00" }, { @@ -217,20 +231,24 @@ "matrix", "vector" ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" + }, "time": "2021-07-01T19:01:15+00:00" }, { "name": "myclabs/php-enum", - "version": "1.8.3", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/myclabs/php-enum.git", - "reference": "b942d263c641ddb5190929ff840c68f78713e937" + "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", - "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", + "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", "shasum": "" }, "require": { @@ -246,7 +264,10 @@ "autoload": { "psr-4": { "MyCLabs\\Enum\\": "src/" - } + }, + "classmap": [ + "stubs/Stringable.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -263,6 +284,10 @@ "keywords": [ "enum" ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.4" + }, "funding": [ { "url": "https://github.com/mnapoli", @@ -273,7 +298,7 @@ "type": "tidelift" } ], - "time": "2021-07-05T08:18:36+00:00" + "time": "2022-08-04T09:53:51+00:00" }, { "name": "psr/http-client", @@ -322,6 +347,9 @@ "psr", "psr-18" ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, "time": "2020-06-29T06:28:15+00:00" }, { @@ -374,6 +402,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, "time": "2019-04-30T12:38:16+00:00" }, { @@ -424,6 +455,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -472,32 +506,38 @@ "psr-16", "simple-cache" ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, "time": "2017-10-23T01:57:42+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -535,6 +575,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -549,36 +592,36 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [ { "name": "composer/pcre", - "version": "1.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2" + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2", + "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1", + "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -604,6 +647,10 @@ "regex", "regular expression" ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.0.0" + }, "funding": [ { "url": "https://packagist.com", @@ -618,27 +665,27 @@ "type": "tidelift" } ], - "time": "2021-12-06T15:17:27+00:00" + "time": "2022-02-25T20:21:48+00:00" }, { "name": "composer/semver", - "version": "3.2.6", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "83e511e247de329283478496f7a1e114c9517506" + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/83e511e247de329283478496f7a1e114c9517506", - "reference": "83e511e247de329283478496f7a1e114c9517506", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.54", + "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", @@ -680,6 +727,11 @@ "validation", "versioning" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, "funding": [ { "url": "https://packagist.com", @@ -694,31 +746,31 @@ "type": "tidelift" } ], - "time": "2021-10-25T11:34:17+00:00" + "time": "2022-04-01T19:23:25+00:00" }, { "name": "composer/xdebug-handler", - "version": "2.0.3", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e" + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6555461e76962fd0379c444c46fd558a0fcfb65e", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "composer/pcre": "^1", - "php": "^5.3.2 || ^7.0 || ^8.0", + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "symfony/phpunit-bridge": "^6.0" }, "type": "library", "autoload": { @@ -741,6 +793,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, "funding": [ { "url": "https://packagist.com", @@ -755,7 +812,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T13:07:32+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -784,6 +841,7 @@ "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, + "default-branch": true, "type": "composer-plugin", "extra": { "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" @@ -829,20 +887,24 @@ "stylecheck", "tests" ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, "time": "2022-07-26T12:51:47+00:00" }, { "name": "doctrine/annotations", - "version": "1.13.2", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + "reference": "648b0343343565c4a056bfc8392201385e8d89f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0", "shasum": "" }, "require": { @@ -854,9 +916,10 @@ "require-dev": { "doctrine/cache": "^1.11 || ^2.0", "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" }, "type": "library", "autoload": { @@ -897,7 +960,11 @@ "docblock", "parser" ], - "time": "2021-08-05T19:00:23+00:00" + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.13.3" + }, + "time": "2022-07-02T10:48:51+00:00" }, { "name": "doctrine/instantiator", @@ -949,6 +1016,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -967,32 +1038,28 @@ }, { "name": "doctrine/lexer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" @@ -1025,6 +1092,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1039,7 +1110,7 @@ "type": "tidelift" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2022-02-28T11:07:21+00:00" }, { "name": "dompdf/dompdf", @@ -1105,56 +1176,60 @@ ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v2.0.0" + }, "time": "2022-06-21T21:14:57+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.4.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad" + "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", + "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", "shasum": "" }, "require": { "composer/semver": "^3.2", - "composer/xdebug-handler": "^2.0", - "doctrine/annotations": "^1.12", + "composer/xdebug-handler": "^3.0.3", + "doctrine/annotations": "^1.13", "ext-json": "*", "ext-tokenizer": "*", - "php": "^7.2.5 || ^8.0", - "php-cs-fixer/diff": "^2.0", - "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", - "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", - "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", - "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.23", - "symfony/process": "^4.4.20 || ^5.0 || ^6.0", - "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php81": "^1.25", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" }, "require-dev": { "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^1.5", - "mikey179/vfsstream": "^1.6.8", + "mikey179/vfsstream": "^1.6.10", "php-coveralls/php-coveralls": "^2.5.2", "php-cs-fixer/accessible-object": "^1.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^8.5.21 || ^9.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "phpunitgoodpractices/polyfill": "^1.5", "phpunitgoodpractices/traits": "^1.9.1", - "symfony/phpunit-bridge": "^5.2.4 || ^6.0", - "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" + "symfony/phpunit-bridge": "^6.0", + "symfony/yaml": "^5.4 || ^6.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1184,26 +1259,30 @@ } ], "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.10.0" + }, "funding": [ { "url": "https://github.com/keradus", "type": "github" } ], - "time": "2021-12-11T16:25:08+00:00" + "time": "2022-08-17T22:13:10+00:00" }, { "name": "masterminds/html5", - "version": "2.7.5", + "version": "2.7.6", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab" + "reference": "897eb517a343a2281f11bc5556d6548db7d93947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f640ac1bdddff06ea333a920c95bbad8872429ab", - "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", + "reference": "897eb517a343a2281f11bc5556d6548db7d93947", "shasum": "" }, "require": { @@ -1255,7 +1334,11 @@ "serializer", "xml" ], - "time": "2021-07-01T14:25:37+00:00" + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + }, + "time": "2022-08-18T16:18:26+00:00" }, { "name": "mitoteam/jpgraph", @@ -1364,6 +1447,11 @@ "php", "utf-8" ], + "support": { + "docs": "http://mpdf.github.io", + "issues": "https://github.com/mpdf/mpdf/issues", + "source": "https://github.com/mpdf/mpdf" + }, "funding": [ { "url": "https://www.paypal.me/mpdf", @@ -1419,6 +1507,10 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", @@ -1477,6 +1569,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + }, "time": "2022-05-31T20:59:12+00:00" }, { @@ -1522,6 +1618,11 @@ "pseudorandom", "random" ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, "time": "2020-10-15T08:29:30+00:00" }, { @@ -1578,6 +1679,10 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, "time": "2021-07-20T11:28:43+00:00" }, { @@ -1625,6 +1730,10 @@ } ], "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, "time": "2022-02-21T01:04:05+00:00" }, { @@ -1665,6 +1774,10 @@ ], "description": "A library to read, parse, export and make subsets of different types of font files.", "homepage": "https://github.com/PhenX/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/0.5.4" + }, "time": "2021-12-17T19:44:54+00:00" }, { @@ -1707,56 +1820,12 @@ ], "description": "A library to read, parse and export to PDF SVG files.", "homepage": "https://github.com/PhenX/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" + }, "time": "2022-03-07T12:52:04+00:00" }, - { - "name": "php-cs-fixer/diff", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "sebastian/diff v3 backport support for PHP 5.6+", - "homepage": "https://github.com/PHP-CS-Fixer", - "keywords": [ - "diff" - ], - "time": "2020-10-14T08:32:19+00:00" - }, { "name": "php-http/message-factory", "version": "v1.0.2", @@ -1805,6 +1874,10 @@ "stream", "uri" ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/master" + }, "time": "2015-12-19T14:08:53+00:00" }, { @@ -1863,219 +1936,12 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, "time": "2019-12-27T09:44:58+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2022-03-15T21:29:03+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpstan/phpstan", "version": "1.8.2", @@ -2111,6 +1977,10 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.8.2" + }, "funding": [ { "url": "https://github.com/ondrejmirtes", @@ -2177,27 +2047,31 @@ "MIT" ], "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1" + }, "time": "2022-04-20T15:24:25+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073", + "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.14", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -2244,13 +2118,17 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2022-08-20T05:26:47+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2300,6 +2178,10 @@ "filesystem", "iterator" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2359,6 +2241,10 @@ "keywords": [ "process" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2414,6 +2300,10 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2469,6 +2359,10 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2479,16 +2373,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.21", + "version": "9.5.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" + "reference": "888556852e7e9bbeeedb9656afe46118765ade34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34", + "reference": "888556852e7e9bbeeedb9656afe46118765ade34", "shasum": "" }, "require": { @@ -2503,7 +2397,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -2521,9 +2414,6 @@ "sebastian/type": "^3.0", "sebastian/version": "^3.0.2" }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { "ext-soap": "*", "ext-xdebug": "*" @@ -2563,6 +2453,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23" + }, "funding": [ { "url": "https://phpunit.de/sponsors.html", @@ -2573,7 +2467,7 @@ "type": "github" } ], - "time": "2022-06-19T12:14:25+00:00" + "time": "2022-08-22T14:01:36+00:00" }, { "name": "psr/cache", @@ -2619,24 +2513,27 @@ "psr", "psr-6" ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, "time": "2016-08-06T20:24:11+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -2663,7 +2560,11 @@ "container-interop", "psr" ], - "time": "2021-03-05T17:36:06+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/event-dispatcher", @@ -2709,6 +2610,10 @@ "psr", "psr-14" ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, "time": "2019-01-08T18:20:26+00:00" }, { @@ -2756,6 +2661,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -2805,6 +2713,10 @@ "parser", "stylesheet" ], + "support": { + "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", + "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" + }, "time": "2021-12-11T13:40:54+00:00" }, { @@ -2851,6 +2763,10 @@ ], "description": "Library for parsing CLI options", "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2903,6 +2819,10 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2954,6 +2874,10 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3024,6 +2948,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3077,6 +3005,10 @@ ], "description": "Library for calculating the complexity of PHP code units", "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3139,6 +3071,10 @@ "unidiff", "unified diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3198,6 +3134,10 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3271,6 +3211,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3331,6 +3275,10 @@ "keywords": [ "global state" ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3384,6 +3332,10 @@ ], "description": "Library for counting the lines of code in PHP source code", "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3437,6 +3389,10 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3488,6 +3444,10 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3547,6 +3507,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3598,6 +3562,10 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3650,6 +3618,10 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3699,6 +3671,10 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3767,6 +3743,10 @@ "fpdi", "pdf" ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.3.6" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", @@ -3824,20 +3804,25 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, "time": "2022-06-18T07:21:10+00:00" }, { "name": "symfony/console", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" + "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", - "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", + "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", + "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", "shasum": "" }, "require": { @@ -3906,6 +3891,9 @@ "console", "terminal" ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3920,20 +3908,20 @@ "type": "tidelift" } ], - "time": "2021-12-20T16:11:12+00:00" + "time": "2022-07-22T10:42:43+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -3970,6 +3958,9 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3984,20 +3975,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.0", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "27d39ae126352b9fa3be5e196ccf4617897be3eb" + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/27d39ae126352b9fa3be5e196ccf4617897be3eb", - "reference": "27d39ae126352b9fa3be5e196ccf4617897be3eb", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", "shasum": "" }, "require": { @@ -4052,6 +4043,9 @@ ], "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4066,20 +4060,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-05-05T16:45:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a" + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", "shasum": "" }, "require": { @@ -4128,6 +4122,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4142,20 +4139,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.0", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "731f917dc31edcffec2c6a777f3698c33bea8f01" + "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/731f917dc31edcffec2c6a777f3698c33bea8f01", - "reference": "731f917dc31edcffec2c6a777f3698c33bea8f01", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6699fb0228d1bc35b12aed6dd5e7455457609ddd", + "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd", "shasum": "" }, "require": { @@ -4189,6 +4186,9 @@ ], "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4203,20 +4203,20 @@ "type": "tidelift" } ], - "time": "2021-10-28T13:39:27+00:00" + "time": "2022-07-20T13:00:38+00:00" }, { "name": "symfony/finder", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e77046c252be48c48a40816187ed527703c8f76c" + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e77046c252be48c48a40816187ed527703c8f76c", - "reference": "e77046c252be48c48a40816187ed527703c8f76c", + "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", "shasum": "" }, "require": { @@ -4249,6 +4249,9 @@ ], "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4263,20 +4266,20 @@ "type": "tidelift" } ], - "time": "2021-12-15T11:06:13+00:00" + "time": "2022-07-29T07:37:50+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.4.0", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b0fb78576487af19c500aaddb269fd36701d4847" + "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b0fb78576487af19c500aaddb269fd36701d4847", - "reference": "b0fb78576487af19c500aaddb269fd36701d4847", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/54f14e36aa73cb8f7261d7686691fd4d75ea2690", + "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690", "shasum": "" }, "require": { @@ -4315,6 +4318,9 @@ "configuration", "options" ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4329,7 +4335,7 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-07-20T13:00:38+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4394,6 +4400,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4412,16 +4421,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535" + "reference": "433d05519ce6990bf3530fba6957499d327395c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", "shasum": "" }, "require": { @@ -4433,7 +4442,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4472,6 +4481,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4486,20 +4498,20 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -4511,7 +4523,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4553,6 +4565,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4567,20 +4582,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -4589,7 +4604,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4629,6 +4644,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4643,20 +4661,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -4665,7 +4683,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4709,6 +4727,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4723,20 +4744,20 @@ "type": "tidelift" } ], - "time": "2021-07-28T13:41:28+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "e66119f3de95efc359483f810c4c3e6436279436" + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436", - "reference": "e66119f3de95efc359483f810c4c3e6436279436", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", "shasum": "" }, "require": { @@ -4745,7 +4766,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4785,6 +4806,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4799,20 +4823,20 @@ "type": "tidelift" } ], - "time": "2021-05-21T13:25:03+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/process", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4" + "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", - "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", + "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", + "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", "shasum": "" }, "require": { @@ -4844,6 +4868,9 @@ ], "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4858,26 +4885,26 @@ "type": "tidelift" } ], - "time": "2021-12-27T21:01:00+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -4924,6 +4951,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4938,20 +4968,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-05-30T19:17:29+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.4.0", + "version": "v5.4.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "208ef96122bfed82a8f3a61458a07113a08bdcfe" + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/208ef96122bfed82a8f3a61458a07113a08bdcfe", - "reference": "208ef96122bfed82a8f3a61458a07113a08bdcfe", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", "shasum": "" }, "require": { @@ -4983,6 +5013,9 @@ ], "description": "Provides a way to profile code", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v5.4.5" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4997,20 +5030,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-02-18T16:06:09+00:00" }, { "name": "symfony/string", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d" + "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", - "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", + "url": "https://api.github.com/repos/symfony/string/zipball/5eb661e49ad389e4ae2b6e4df8d783a8a6548322", + "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322", "shasum": "" }, "require": { @@ -5066,6 +5099,9 @@ "utf-8", "utf8" ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5080,7 +5116,7 @@ "type": "tidelift" } ], - "time": "2021-12-16T21:52:00+00:00" + "time": "2022-07-24T16:15:25+00:00" }, { "name": "tecnickcom/tcpdf", @@ -5192,6 +5228,10 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, "funding": [ { "url": "https://github.com/theseer", @@ -5199,60 +5239,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], @@ -5279,5 +5265,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/samples/Calculations/LookupRef/VLOOKUP.php b/samples/Calculations/LookupRef/VLOOKUP.php index 3e7eaa71..16a911e8 100644 --- a/samples/Calculations/LookupRef/VLOOKUP.php +++ b/samples/Calculations/LookupRef/VLOOKUP.php @@ -14,8 +14,8 @@ $worksheet = $spreadsheet->getActiveSheet(); $data = [ ['ID', 'First Name', 'Last Name', 'Salary'], - [72, 'Emily', 'Smith', 64901, null, 'ID', 53, 66, 56], - [66, 'James', 'Anderson', 70855, null, 'Salary'], + [72, 'Emily', 'Smith', 64901], + [66, 'James', 'Anderson', 70855], [14, 'Mia', 'Clark', 188657], [30, 'John', 'Lewis', 97566], [53, 'Jessica', 'Walker', 58339], @@ -25,11 +25,24 @@ $data = [ $worksheet->fromArray($data, null, 'B2'); -$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); -$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); -$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); +$lookupFields = [ + ['ID', 53, 66, 56], + ['Name'], + ['Salary'], +]; + +$worksheet->fromArray($lookupFields, null, 'G3'); + +$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 2, FALSE) & " " & VLOOKUP(H3, B3:E9, 3, FALSE)'); +$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 2, FALSE) & " " & VLOOKUP(I3, B3:E9, 3, FALSE)'); +$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 2, FALSE) & " " & VLOOKUP(J3, B3:E9, 3, FALSE)'); +$worksheet->getCell('H5')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); +$worksheet->getCell('I5')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); +$worksheet->getCell('J5')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); for ($column = 'H'; $column !== 'K'; ++$column) { - $cell = $worksheet->getCell("{$column}4"); - $helper->log("{$column}4: {$cell->getValue()} => {$cell->getCalculatedValue()}"); + for ($row = 4; $row <= 5; ++$row) { + $cell = $worksheet->getCell("{$column}{$row}"); + $helper->log("{$column}{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); + } } diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index eed87119..59310620 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -120,7 +120,7 @@ $dataSeriesValues[2] // triangle border ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected - // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php index b4fc97b1..f6f9a6f4 100644 --- a/samples/Chart/33_Chart_create_scatter3.php +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -120,7 +120,7 @@ $dataSeriesValues[2] // triangle border ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected - // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index a87f6ee1..da831fa9 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -121,7 +121,7 @@ $dataSeriesValues[2] // triangle border ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected - // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index cf93a694..d9485ad9 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4756,9 +4756,8 @@ class Calculation break; } - - // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on } elseif (($token === '~') || ($token === '%')) { + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on if (($arg = $stack->pop()) === null) { return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } @@ -4865,9 +4864,8 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $cellValue; } - - // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on if ($pCellParent) { $cell->attach($pCellParent); } @@ -4977,8 +4975,8 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $token; } - // if the token is a named range or formula, evaluate it and push the result onto the stack } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) { + // if the token is a named range or formula, evaluate it and push the result onto the stack $definedName = $matches[6]; if ($cell === null || $pCellWorksheet === null) { return $this->raiseFormulaError("undefined name '$token'"); diff --git a/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/src/PhpSpreadsheet/Calculation/Engine/Logger.php index c6ee5969..256c3eff 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/Logger.php +++ b/src/PhpSpreadsheet/Calculation/Engine/Logger.php @@ -98,9 +98,9 @@ class Logger $cellReference = implode(' -> ', $this->cellStack->showStack()); if ($this->echoDebugLog) { echo $cellReference, - ($this->cellStack->count() > 0 ? ' => ' : ''), - $message, - PHP_EOL; + ($this->cellStack->count() > 0 ? ' => ' : ''), + $message, + PHP_EOL; } $this->debugLog[] = $cellReference . ($this->cellStack->count() > 0 ? ' => ' : '') . diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php index b9fcfc73..22cad2cf 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -17,7 +17,7 @@ class Random */ public static function rand() { - return (mt_rand(0, 10000000)) / 10000000; + return mt_rand(0, 10000000) / 10000000; } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php index 41b011a5..85195c88 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php @@ -203,7 +203,7 @@ class Averages extends AggregateBase $args, function ($value) { // Is it a numeric value? - return (is_numeric($value)) && (!is_string($value)); + return is_numeric($value) && (!is_string($value)); } ); } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 8574d58d..c8743364 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -101,7 +101,7 @@ class ChiSquared return 1 - self::distributionRightTail($value, $degrees); } - return (($value ** (($degrees / 2) - 1) * exp(-$value / 2))) / + return ($value ** (($degrees / 2) - 1) * exp(-$value / 2)) / ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2)); } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index f0ce1f65..d31a55b3 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -729,21 +729,21 @@ abstract class JpGraphRendererBase implements IRenderer switch ($chartType) { case 'area3DChart': $dimensions = '3d'; - // no break + // no break case 'areaChart': $this->renderPlotLine($i, true, true, $dimensions); break; case 'bar3DChart': $dimensions = '3d'; - // no break + // no break case 'barChart': $this->renderPlotBar($i, $dimensions); break; case 'line3DChart': $dimensions = '3d'; - // no break + // no break case 'lineChart': $this->renderPlotLine($i, false, true, $dimensions); @@ -799,35 +799,35 @@ abstract class JpGraphRendererBase implements IRenderer switch ($chartType) { case 'area3DChart': $dimensions = '3d'; - // no break + // no break case 'areaChart': $this->renderAreaChart($groupCount, $dimensions); break; case 'bar3DChart': $dimensions = '3d'; - // no break + // no break case 'barChart': $this->renderBarChart($groupCount, $dimensions); break; case 'line3DChart': $dimensions = '3d'; - // no break + // no break case 'lineChart': $this->renderLineChart($groupCount, $dimensions); break; case 'pie3DChart': $dimensions = '3d'; - // no break + // no break case 'pieChart': $this->renderPieChart($groupCount, $dimensions, false, false); break; case 'doughnut3DChart': $dimensions = '3d'; - // no break + // no break case 'doughnutChart': $this->renderPieChart($groupCount, $dimensions, true, true); @@ -846,7 +846,7 @@ abstract class JpGraphRendererBase implements IRenderer break; case 'surface3DChart': $dimensions = '3d'; - // no break + // no break case 'surfaceChart': $this->renderContourChart($groupCount, $dimensions); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 9f8a3ace..71496ece 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -6999,7 +6999,7 @@ class Xls extends BaseReader } break; - // Unknown cases // don't know how to deal with + // Unknown cases // don't know how to deal with default: throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula'); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 52df94e4..630fd1dd 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -479,7 +479,7 @@ class Xlsx extends BaseReader $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); break; - //Ribbon + // Ribbon case Namespaces::EXTENSIBILITY: $customUI = $relTarget; if ($customUI) { @@ -530,7 +530,7 @@ class Xlsx extends BaseReader } break; - // a vbaProject ? (: some macros) + // a vbaProject ? (: some macros) case Namespaces::VBA: $macros = $ele['Target']; @@ -1695,8 +1695,8 @@ class Xlsx extends BaseReader break; - // unparsed case 'application/vnd.ms-excel.controlproperties+xml': + // unparsed $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; break; @@ -1722,7 +1722,6 @@ class Xlsx extends BaseReader $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { if (is_object($is->r)) { - /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php index 66111b6c..1ec7f6ab 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php @@ -495,7 +495,7 @@ class EigenvalueDecomposition $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; } - // Complex pair + // Complex pair } else { $this->d[$n - 1] = $x + $p; $this->d[$n] = $x + $p; @@ -671,7 +671,7 @@ class EigenvalueDecomposition } else { $this->H[$i][$n] = -$r / ($eps * $norm); } - // Solve real equations + // Solve real equations } else { $x = $this->H[$i][$i + 1]; $y = $this->H[$i + 1][$i]; @@ -693,7 +693,7 @@ class EigenvalueDecomposition } } } - // Complex vector + // Complex vector } elseif ($q < 0) { $l = $n - 1; // Last vector component imaginary so matrix is triangular diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index 0bbd94a7..5e35d491 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -67,21 +67,21 @@ class Matrix $this->A = $args[0]; break; - //Square matrix - n x n + //Square matrix - n x n case 'integer': $this->m = $args[0]; $this->n = $args[0]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n + //Rectangular matrix - m x n case 'integer,integer': $this->m = $args[0]; $this->n = $args[1]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n initialized from packed array + //Rectangular matrix - m x n initialized from packed array case 'array,integer': $this->m = $args[1]; if ($this->m != 0) { @@ -191,7 +191,7 @@ class Matrix return $R; break; - //A($i0...$iF; $j0...$jF) + //A($i0...$iF; $j0...$jF) case 'integer,integer,integer,integer': [$i0, $iF, $j0, $jF] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -214,7 +214,7 @@ class Matrix return $R; break; - //$R = array of row indices; $C = array of column indices + //$R = array of row indices; $C = array of column indices case 'array,array': [$RL, $CL] = $args; if (count($RL) > 0) { @@ -237,7 +237,7 @@ class Matrix return $R; break; - //A($i0...$iF); $CL = array of column indices + //A($i0...$iF); $CL = array of column indices case 'integer,integer,array': [$i0, $iF, $CL] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -260,7 +260,7 @@ class Matrix return $R; break; - //$RL = array of row indices + //$RL = array of row indices case 'array,integer,integer': [$RL, $j0, $jF] = $args; if (count($RL) > 0) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php index 6c8999d0..b809bfa1 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -315,7 +315,7 @@ class SingularValueDecomposition } break; - // Split at negligible s(k). + // Split at negligible s(k). case 2: $f = $e[$k - 1]; $e[$k - 1] = 0.0; @@ -336,7 +336,7 @@ class SingularValueDecomposition } break; - // Perform one qr step. + // Perform one qr step. case 3: // Calculate the shift. $scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k])); @@ -396,7 +396,7 @@ class SingularValueDecomposition $iter = $iter + 1; break; - // Convergence. + // Convergence. case 4: // Make the singular values positive. if ($this->s[$k] <= 0.0) { diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 2f75f908..ca9b67b5 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -531,7 +531,7 @@ class Parser { return($this->convertFunction($token, $this->_func_args)); }*/ - // if it's an argument, ignore the token (the argument remains) + // if it's an argument, ignore the token (the argument remains) } elseif ($token == 'arg') { return ''; } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 37865518..8f09af69 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2836,7 +2836,7 @@ class Worksheet extends BIFFwriter $operatorType = 0x01; break; - // not OPERATOR_NOTBETWEEN 0x02 + // not OPERATOR_NOTBETWEEN 0x02 } } From d0781c3fd236b594d94335b53691b339b4fe8818 Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre Date: Thu, 25 Aug 2022 02:47:22 +0200 Subject: [PATCH 093/156] Explain that UoM = Unit of Measure (#3014) --- docs/topics/recipes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index e4313382..afa3dcc8 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1161,7 +1161,7 @@ A column's width can be set using the following code: $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(12); ``` -If you want to set a column width using a different unit of measure, +If you want to set a column width using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the width value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1258,7 +1258,7 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. -If you want to set a row height using a different unit of measure, +If you want to set a row height using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the height value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1670,7 +1670,7 @@ $spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(12); Excel measures column width in its own proprietary units, based on the number of characters that will be displayed in the default font. -If you want to set the default column width using a different unit of measure, +If you want to set the default column width using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the width value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1693,7 +1693,7 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. -If you want to set a row height using a different unit of measure, +If you want to set a row height using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the height value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), From e97428ba6708615fd12b411c9ef2a079bcb3d252 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 24 Aug 2022 18:00:37 -0700 Subject: [PATCH 094/156] Html Writer - Do Not Generate background-color When Fill is None (#3016) * Html Writer - Do Not Generate background-color When Fill is None For PR #3002, I noted that there was a problem with Dompdf truncating images. I raised an issue with them (https://github.com/dompdf/dompdf/issues/2980), and they agree that there is a bug; however, they also suggested a workaround, namely omitting background-color from any cells which the image overlays. That did not at first appear to be a solution which could be generalized for PhpSpreasheet. However, investigating further, I saw that Html Writer is generating background-color for all cells, even though most of them use the default Fill type None (which suggests that background-color should not be specified after all). So this PR changes HTML Writer to generate background-color only when the user has actually set Fill type to something other than None. This is not a complete workaround for the Dompdf problem - we will still see truncation if the image overlays a cell which does specify a Fill type - however, it is almost certainly good enough for most use cases. In addition to that change, I made the generated Html a little smaller and the code a little more efficient by combining the TD and TH styles for each cell into a single declaration and calling createCssStyle only once. * Revamp One Test Look for both td.style and th.style instead of just td.style in test. --- src/PhpSpreadsheet/Writer/Html.php | 11 ++++++----- .../Writer/Html/VisibilityTest.php | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 362eae00..68e200f5 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -940,8 +940,8 @@ class Html extends BaseWriter // Calculate cell style hashes foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { - $css['td.style' . $index] = $this->createCSSStyle($style); - $css['th.style' . $index] = $this->createCSSStyle($style); + $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style); + //$css['th.style' . $index] = $this->createCSSStyle($style); } // Fetch sheets @@ -1094,9 +1094,10 @@ class Html extends BaseWriter $css = []; // Create CSS - $value = $fill->getFillType() == Fill::FILL_NONE ? - 'white' : '#' . $fill->getStartColor()->getRGB(); - $css['background-color'] = $value; + if ($fill->getFillType() !== Fill::FILL_NONE) { + $value = '#' . $fill->getStartColor()->getRGB(); + $css['background-color'] = $value; + } return $css; } diff --git a/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php index c5d4da68..1c1ffb06 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php @@ -99,11 +99,11 @@ class VisibilityTest extends Functional\AbstractFunctional self::assertEquals(1, $rowsrch); $rowsrch = preg_match('/^\\s*table[.]sheet0 tr[.]row1 [{] height:25pt [}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style1 [{].*text-decoration:line-through;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style1, th[.]style1 [{].*text-decoration:line-through;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style2 [{].*text-decoration:underline line-through;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style2, th[.]style2 [{].*text-decoration:underline line-through;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style3 [{].*text-decoration:underline;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style3, th[.]style3 [{].*text-decoration:underline;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); $this->writeAndReload($spreadsheet, 'Html'); From 3861f7e37e9453a9babc30d09ed08b361bb219bb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 24 Aug 2022 19:31:55 -0700 Subject: [PATCH 095/156] Charts - Add Support for Date Axis (#3018) * Charts - Add Support for Date Axis Fix #2967. Fix #2969 (which had already been fixed prior to opening the issue, but had added urgency for Date Axes). Add ability to set axis type to date axis, in addition to original possiblities of value axis and category axis. * Update 33_Chart_create_line_dateaxis.php No idea why php-cs-fixer is complaining. It didn't do so when I first uploaded. I can't duplicate problem on my own system. Not enough detail in error message for me to act. Grasping at straws, I have moved the function definition (which is the only use of braces in the entire script) from the end of the script to the beginning. * Update 33_Chart_create_line_dateaxis.php Some comments were mis-aligned. This may be related to the reasons behind PR #3025, which didn't take care of this because this script had not yet been merged. --- .../Chart/33_Chart_create_line_dateaxis.php | 375 ++++++++++++++++++ .../33_Chart_create_scatter5_trendlines.php | 1 + .../32readwriteLineDateAxisChart1.xlsx | Bin 0 -> 12114 bytes src/PhpSpreadsheet/Chart/Axis.php | 26 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 22 +- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 42 +- .../Chart/AxisPropertiesTest.php | 10 +- .../Chart/Charts32CatAxValAxTest.php | 8 +- .../Chart/Charts32XmlTest.php | 41 ++ 9 files changed, 500 insertions(+), 25 deletions(-) create mode 100644 samples/Chart/33_Chart_create_line_dateaxis.php create mode 100644 samples/templates/32readwriteLineDateAxisChart1.xlsx diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php new file mode 100644 index 00000000..1a47e5aa --- /dev/null +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -0,0 +1,375 @@ +getActiveSheet(); +$dataSheet->setTitle('Data'); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +// Dates changed not to fall on exact quarter start +$dataSheet->fromArray( + [ + ['', 'date', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE(B2)', '2021-01-10', 12.1, 15.1, 21.1], + ['=DATEVALUE(B3)', '2021-04-21', 56.2, 73.2, 86.2], + ['=DATEVALUE(B4)', '2021-07-31', 52.2, 61.2, 69.2], + ['=DATEVALUE(B5)', '2021-10-11', 30.2, 22.2, 0.2], + ['=DATEVALUE(B6)', '2022-01-21', 40.1, 38.1, 65.1], + ['=DATEVALUE(B7)', '2022-04-11', 45.2, 44.2, 96.2], + ['=DATEVALUE(B8)', '2022-07-01', 52.2, 51.2, 55.2], + ['=DATEVALUE(B9)', '2022-10-31', 41.2, 72.2, 56.2], + ] +); + +$dataSheet->getStyle('A2:A9')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$dataSheet->getColumnDimension('A')->setAutoSize(true); +$dataSheet->getColumnDimension('B')->setAutoSize(true); +$dataSheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$C$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$E$1', null, 1), +]; +// Set the X-Axis Labels +// NUMBER, not STRING +// added x-axis values for each of the 3 metrics +// added FORMATE_CODE_NUMBER +// Number of datapoints in series +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Data Marker Color fill/[fill,Border] +// Data Marker size +// Color(s) added +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$C$2:$C$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'diamond', + null, + 5 + ), + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$D$2:$D$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'square', + '*accent1', + 6 + ), + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$E$2:$E$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + null, + null, + 7 + ), // let Excel choose marker shape +]; +// series 1 - metric1 +// marker details +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + +// line details - dashed, smooth line (Bezier) with arrows, 40% transparent +$dataSeriesValues[0] + ->setSmoothLine(true) + ->setScatterLines(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - metric2, straight line - no special effects, connected +$dataSeriesValues[1] // square marker border color + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square marker fill color + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - metric3, markers, no line +$dataSeriesValues[2] // triangle? fill + //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); +$yAxis = new Axis(); +$yAxis->setMajorGridlines(new GridLines()); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('P12'); + +// create a 'Chart' worksheet, add $chart to it +$spreadsheet->createSheet(); +$chartSheet = $spreadsheet->getSheet(1); +$chartSheet->setTitle('Scatter+Line Chart'); + +$chartSheet = $spreadsheet->getSheetByName('Scatter+Line Chart'); +// Add the chart to the worksheet +$chartSheet->addChart($chart); + +// ------- Demonstrate Date Xaxis in Line Chart, not possible using Scatter Chart ------------ + +// Set the Labels (Column header) for each data series we want to plot +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$E$1', null, 1), +]; + +// Set the X-Axis Labels - dates, N.B. 01/10/2021 === Jan 10, NOT Oct 1 !! +// x-axis values are the Excel numeric representation of the date - so set +// formatCode=General for the xAxis VALUES, but we want the labels to be +// DISPLAYED as 'yyyy-mm-dd' That is, read a number, display a date. +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; + +// X axis (date) settings +$xAxisLabel = new Title('Date'); +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // yyyy-mm-dd + +// Set the Data values for each data series we want to plot +$dataSeriesValues = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$E$2:$E$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'triangle', + null, + 7 + ), +]; + +// series - metric3, markers, no line +$dataSeriesValues[0] + ->setScatterlines(false); // disable connecting lines +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); + +// Build the dataseries +// must now use LineChart instead of ScatterChart, since ScatterChart does not +// support "dateAx" axis type. +$series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + 'standard', // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + // DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title = new Title('Test Line-Chart with Date Axis - metric3 values'); + +// X axis (date) settings +$xAxisLabel = new Title('Game Date'); +$xAxis = new Axis(); +// date axis values are Excel numbers, not yyyy-mm-dd Date strings +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + +$xAxis->setAxisType('dateAx'); // dateAx available ONLY for LINECHART, not SCATTERCHART + +// measure the time span in Quarters, of data. +$dateMinMax = dateRange(8, $spreadsheet); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q +// change xAxis tick marks to match Qtr boundaries + +$nQtrs = sprintf('%3.2f', (($dateMinMax['max'] - $dateMinMax['min']) / 30.5) / 4); +$tickMarkInterval = ($nQtrs > 20) ? 6 : 3; // tick marks every ? months + +$xAxis->setAxisOptionsProperties( + Properties::AXIS_LABELS_NEXT_TO, // axis_label pos + null, // horizontalCrossesValue + null, // horizontalCrosses + null, // axisOrientation + 'in', // major_tick_mark + null, // minor_tick_mark + $dateMinMax['min'], // minimum calculate this from the earliest data: 'Data!$A$2' + $dateMinMax['max'], // maximum calculate this from the last data: 'Data!$A$'.($nrows+1) + $tickMarkInterval, // majorUnit determines tickmarks & Gridlines ? + null, // minorUnit + null, // textRotation + null, // hidden + 'days', // baseTimeUnit + 'months', // majorTimeUnit, + 'months', // minorTimeUnit +); + +$yAxisLabel = new Title('Value ($k)'); +$yAxis = new Axis(); +$yAxis->setMajorGridlines(new GridLines()); +$xAxis->setMajorGridlines(new GridLines()); +$minorGridLines = new GridLines(); +$minorGridLines->activateObject(); +$xAxis->setMinorGridlines($minorGridLines); + +// Create the chart +$chart = new Chart( + 'chart2', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet below the first chart +$chart->setTopLeftPosition('A13'); +$chart->setBottomRightPosition('P25'); +$chart->setRoundedCorners('true'); // Rounded corners in Chart Outline + +// Add the chart to the worksheet $chartSheet +$chartSheet->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); +$spreadsheet->disconnectWorksheets(); + +function dateRange(int $nrows, Spreadsheet $wrkbk): array +{ + $dataSheet = $wrkbk->getSheetByName('Data'); + + // start the xaxis at the beginning of the quarter of the first date + $startDateStr = $dataSheet->getCell('B2')->getValue(); // yyyy-mm-dd date string + $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj + + // get date of first day of the quarter of the start date + $startMonth = $startDate->format('n'); // suppress leading zero + $startYr = $startDate->format('Y'); + $qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0); + $qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3)); + $qtrStartStr = "$startYr-$qtrStartMonth-01"; + $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr); + + // end the xaxis at the end of the quarter of the last date + $lastDateStr = $dataSheet->getCellByColumnAndRow(2, $nrows + 1)->getValue(); + $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); + $lastMonth = $lastDate->format('n'); + $lastYr = $lastDate->format('Y'); + $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); + $qtrEndMonth = 3 + (($qtr - 1) * 3); + $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); + $qtrEndMonth = sprintf('%02d', $qtrEndMonth); + $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; + $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); + + $minMaxDates = ['min' => $ExcelQtrStartDateVal, 'max' => $ExcelQtrEndDateVal]; + + return $minMaxDates; +} diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index da831fa9..a640f735 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -263,6 +263,7 @@ $chart->setBottomRightPosition('P25'); // Add the chart to the worksheet $chartSheet $chartSheet->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); // Save Excel 2007 file $filename = $helper->getFilename(__FILE__); diff --git a/samples/templates/32readwriteLineDateAxisChart1.xlsx b/samples/templates/32readwriteLineDateAxisChart1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..42470303aa766a74253901e16ead0683db938d13 GIT binary patch literal 12114 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 null, 'textRotation' => null, 'hidden' => null, + 'majorTimeUnit' => self::TIME_UNIT_YEARS, + 'minorTimeUnit' => self::TIME_UNIT_MONTHS, + 'baseTimeUnit' => self::TIME_UNIT_DAYS, ]; /** @@ -74,6 +85,7 @@ class Axis extends Properties private const NUMERIC_FORMAT = [ Properties::FORMAT_CODE_NUMBER, Properties::FORMAT_CODE_DATE, + Properties::FORMAT_CODE_DATE_ISO8601, ]; /** @@ -115,12 +127,12 @@ class Axis extends Properties public function getAxisIsNumericFormat(): bool { - return (bool) $this->axisNumber['numeric']; + return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; } public function setAxisOption(string $key, ?string $value): void { - if (!empty($value)) { + if ($value !== null && $value !== '') { $this->axisOptions[$key] = $value; } } @@ -140,7 +152,10 @@ class Axis extends Properties ?string $majorUnit = null, ?string $minorUnit = null, ?string $textRotation = null, - ?string $hidden = null + ?string $hidden = null, + ?string $baseTimeUnit = null, + ?string $majorTimeUnit = null, + ?string $minorTimeUnit = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -154,6 +169,9 @@ class Axis extends Properties $this->setAxisOption('minor_unit', $minorUnit); $this->setAxisOption('textRotation', $textRotation); $this->setAxisOption('hidden', $hidden); + $this->setAxisOption('baseTimeUnit', $baseTimeUnit); + $this->setAxisOption('majorTimeUnit', $majorTimeUnit); + $this->setAxisOption('minorTimeUnit', $minorTimeUnit); } /** @@ -185,7 +203,7 @@ class Axis extends Properties public function setAxisType(string $type): self { - if ($type === 'catAx' || $type === 'valAx') { + if ($type === self::AXIS_TYPE_CATEGORY || $type === self::AXIS_TYPE_VALUE || $type === self::AXIS_TYPE_DATE) { $this->axisType = $type; } else { $this->axisType = ''; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index dab2b410..76316db9 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -139,12 +139,13 @@ class Chart $plotAreaLayout = $this->chartLayoutDetails($chartDetail); break; - case 'catAx': + case Axis::AXIS_TYPE_CATEGORY: + case Axis::AXIS_TYPE_DATE: $catAxRead = true; if (isset($chartDetail->title)) { $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } - $xAxis->setAxisType('catAx'); + $xAxis->setAxisType($chartDetailKey); $this->readEffects($chartDetail, $xAxis); if (isset($chartDetail->spPr)) { $sppr = $chartDetail->spPr->children($this->aNamespace); @@ -173,13 +174,7 @@ class Chart $this->setAxisProperties($chartDetail, $xAxis); break; - case 'dateAx': - if (isset($chartDetail->title)) { - $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); - } - - break; - case 'valAx': + case Axis::AXIS_TYPE_VALUE: $whichAxis = null; $axPos = null; if (isset($chartDetail->axPos)) { @@ -1375,6 +1370,15 @@ class Chart if (isset($chartDetail->minorUnit)) { $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); } + if (isset($chartDetail->baseTimeUnit)) { + $whichAxis->setAxisOption('baseTimeUnit', (string) self::getAttribute($chartDetail->baseTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->majorTimeUnit)) { + $whichAxis->setAxisOption('majorTimeUnit', (string) self::getAttribute($chartDetail->majorTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->minorTimeUnit)) { + $whichAxis->setAxisOption('minorTimeUnit', (string) self::getAttribute($chartDetail->minorTimeUnit, 'val', 'string')); + } if (isset($chartDetail->txPr)) { $children = $chartDetail->txPr->children($this->aNamespace); if (isset($children->bodyPr)) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index ad746a0a..48f7d255 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -490,13 +490,14 @@ class Chart extends WriterPart private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc - // In that case, xAxis is NOT a category. - if ($yAxis->getAxisType() !== '') { - $objWriter->startElement('c:' . $yAxis->getAxisType()); + // In that case, xAxis may contain values like the yAxis, or it may be a date axis (LINECHART). + $axisType = $yAxis->getAxisType(); + if ($axisType !== '') { + $objWriter->startElement("c:$axisType"); } elseif ($yAxis->getAxisIsNumericFormat()) { - $objWriter->startElement('c:valAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); } else { - $objWriter->startElement('c:catAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_CATEGORY); } $majorGridlines = $yAxis->getMajorGridlines(); $minorGridlines = $yAxis->getMinorGridlines(); @@ -654,7 +655,8 @@ class Chart extends WriterPart } $objWriter->startElement('c:auto'); - $objWriter->writeAttribute('val', '1'); + // LineChart with dateAx wants '0' + $objWriter->writeAttribute('val', ($axisType === Axis::AXIS_TYPE_DATE) ? '0' : '1'); $objWriter->endElement(); $objWriter->startElement('c:lblAlgn'); @@ -665,6 +667,30 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); + if ($axisType === Axis::AXIS_TYPE_DATE) { + $property = 'baseTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'majorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'minorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + } + if ($isMultiLevelSeries) { $objWriter->startElement('c:noMultiLvlLbl'); $objWriter->writeAttribute('val', '0'); @@ -683,7 +709,7 @@ class Chart extends WriterPart */ private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis): void { - $objWriter->startElement('c:valAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); $majorGridlines = $xAxis->getMajorGridlines(); $minorGridlines = $xAxis->getMinorGridlines(); @@ -1079,7 +1105,7 @@ class Chart extends WriterPart $objWriter->endElement(); // a:ln } } - $nofill = $groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && !$plotSeriesValues->getScatterLines()); + $nofill = $groupType === DataSeries::TYPE_STOCKCHART || (($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) && !$plotSeriesValues->getScatterLines()); if ($callLineStyles) { $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill); $this->writeEffects($objWriter, $plotSeriesValues); diff --git a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php index 91df25cb..0bfc2966 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php @@ -109,7 +109,9 @@ class AxisPropertiesTest extends AbstractFunctional '8', //minimum '68', //maximum '20', //majorUnit - '5' //minorUnit + '5', //minorUnit + '6', //textRotation + '0', //hidden ); self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis->getAxisOptionsProperty('axis_labels')); self::assertNull($xAxis->getAxisOptionsProperty('horizontal_crosses_value')); @@ -121,6 +123,8 @@ class AxisPropertiesTest extends AbstractFunctional self::assertSame('68', $xAxis->getAxisOptionsProperty('maximum')); self::assertSame('20', $xAxis->getAxisOptionsProperty('major_unit')); self::assertSame('5', $xAxis->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis->getAxisOptionsProperty('hidden')); $yAxis = new Axis(); $yAxis->setFillParameters('accent1', 30, 'schemeClr'); @@ -158,6 +162,8 @@ class AxisPropertiesTest extends AbstractFunctional self::assertSame('68', $xAxis2->getAxisOptionsProperty('maximum')); self::assertSame('20', $xAxis2->getAxisOptionsProperty('major_unit')); self::assertSame('5', $xAxis2->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis2->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis2->getAxisOptionsProperty('hidden')); $yAxis2 = $chart->getChartAxisY(); self::assertSame('accent1', $yAxis2->getFillProperty('value')); @@ -198,6 +204,8 @@ class AxisPropertiesTest extends AbstractFunctional self::assertSame('68', $xAxis3->getAxisOptionsProperty('maximum')); self::assertSame('20', $xAxis3->getAxisOptionsProperty('major_unit')); self::assertSame('5', $xAxis3->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis3->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis3->getAxisOptionsProperty('hidden')); $yAxis3 = $chart2->getChartAxisY(); self::assertSame('accent1', $yAxis3->getFillProperty('value')); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php index 268ee094..1f046af9 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php @@ -23,6 +23,8 @@ class Charts32CatAxValAxTest extends TestCase /** @var string */ private $outputFileName = ''; + private const FORMAT_CODE_DATE_ISO8601_SLASH = 'yyyy/mm/dd'; // not automatically treated as numeric + protected function tearDown(): void { if ($this->outputFileName !== '') { @@ -48,7 +50,7 @@ class Charts32CatAxValAxTest extends TestCase ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], ] ); - $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); + $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(self::FORMAT_CODE_DATE_ISO8601_SLASH); $worksheet->getColumnDimension('A')->setAutoSize(true); $worksheet->setSelectedCells('A1'); @@ -91,9 +93,9 @@ class Charts32CatAxValAxTest extends TestCase $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); if (is_bool($numeric)) { - $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, $numeric); + $xAxis->setAxisNumberProperties(self::FORMAT_CODE_DATE_ISO8601_SLASH, $numeric); } else { - $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + $xAxis->setAxisNumberProperties(self::FORMAT_CODE_DATE_ISO8601_SLASH); } // Build the dataseries diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php index 6a4673fd..4cc62360 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php @@ -177,4 +177,45 @@ class Charts32XmlTest extends TestCase ) ); } + + public function testDateAx(): void + { + $file = self::DIRECTORY . '32readwriteLineDateAxisChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(2, $charts); + $chart = $charts[1]; + self::assertNotNull($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + } } From 131708409b7a48949838e13bf528f1dca7947fc7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 25 Aug 2022 23:22:59 -0700 Subject: [PATCH 096/156] Correct Very Minor Error / Php8.2 Deprecation (#3021) When Reader/Xlsx/WorkbookView was split off from Reader/Xlsx.php, one statement accidentally brought `!empty($this->loadSheetsOnly)` with it. That property does not exist in WorkbookView, so the test is useless (it is always empty); and, in fact, the caller passes its own version of loadSheetsOnly as a parameter, so it isn't needed even it did exist. In Php8.2, this might be a deprecation, although it hasn't shown up in the GitHub 8.2 tests. Fix it anyhow. --- src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php b/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php index 9d61e3d3..4743afbf 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php @@ -29,7 +29,7 @@ class WorkbookView $this->spreadsheet->setActiveSheetIndex(0); $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView; - if (($readDataOnly !== true || !empty($this->loadSheetsOnly)) && !empty($workbookView)) { + if ($readDataOnly !== true && !empty($workbookView)) { $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView)); // active sheet index $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index From f7a35349289532a2ee3912f2464d9722826152a8 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 27 Aug 2022 16:23:55 +0200 Subject: [PATCH 097/156] Implementation of the `VALUETOTEXT()` Excel Function --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 4 +-- .../Calculation/Engineering/ConvertUOM.php | 1 + .../Calculation/TextData/Format.php | 34 +++++++++++++++++++ .../Functions/TextData/ValueToTextTest.php | 28 +++++++++++++++ .../data/Calculation/TextData/VALUETOTEXT.php | 27 +++++++++++++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php create mode 100644 tests/data/Calculation/TextData/VALUETOTEXT.php diff --git a/CHANGELOG.md b/CHANGELOG.md index da29bc8f..dc17d852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions -- Implementation of the `ARRAYTOTEXT()` Excel Function +- Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. - Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines. diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index d9485ad9..b5a26f62 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2659,8 +2659,8 @@ class Calculation ], 'VALUETOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Format::class, 'valueToText'], + 'argumentCount' => '1,2', ], 'VAR' => [ 'category' => Category::CATEGORY_STATISTICAL, diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php index 677fb0fb..b7c298db 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php @@ -106,6 +106,7 @@ class ConvertUOM 'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false], + // Magnetism 'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true], 'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true], // Temperature diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index bec11496..03e75d1d 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -4,11 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; @@ -208,6 +210,38 @@ class Format return (float) $value; } + /** + * TEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $format + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function valueToText($value, $format = false) + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + $format = (bool) $format; + + if (is_object($value) && $value instanceof RichText) { + $value = $value->getPlainText(); + } + if (is_string($value)) { + $value = ($format === true) ? Calculation::wrapResult($value) : $value; + $value = str_replace("\n", '', $value); + } elseif (is_bool($value)) { + $value = Calculation::$localeBoolean[$value === true ? 'TRUE' : 'FALSE']; + } + + return (string) $value; + } + /** * @param mixed $decimalSeparator */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php new file mode 100644 index 00000000..574d4f56 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php @@ -0,0 +1,28 @@ +getSheet(); + $this->setCell('A1', $value); + $sheet->getCell('B1')->setValue("=VALUETOTEXT(A1, {$format})"); + + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerVALUE(): array + { + return require 'tests/data/Calculation/TextData/VALUETOTEXT.php'; + } +} diff --git a/tests/data/Calculation/TextData/VALUETOTEXT.php b/tests/data/Calculation/TextData/VALUETOTEXT.php new file mode 100644 index 00000000..13df19ab --- /dev/null +++ b/tests/data/Calculation/TextData/VALUETOTEXT.php @@ -0,0 +1,27 @@ +createTextRun('Hello'); +$richText1->createText(' World'); + +$richText2 = new RichText(); +$richText2->createTextRun('Hello'); +$richText2->createText("\nWorld"); + +return [ + ['1', 1, 0], + ['1.23', 1.23, 0], + ['-123.456', -123.456, 0], + ['TRUE', true, 0], + ['FALSE', false, 0], + ['Hello World', 'Hello World', 0], + ['HelloWorld', "Hello\nWorld", 0], + ['"Hello World"', 'Hello World', 1], + ['"HelloWorld"', "Hello\nWorld", 1], + ['Hello World', $richText1, 0], + ['HelloWorld', $richText2, 0], + ['"Hello World"', $richText1, 1], + ['"HelloWorld"', $richText2, 1], +]; From 389ca80e00e833031e1c6ff2adff5c9de08668c8 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 Aug 2022 22:59:35 -0700 Subject: [PATCH 098/156] Additional Properties for Trendlines (#3028) Fix #3011. Some properties for Trendlines were omitted in the original request for this feature. Also, the trendlines sample spreadsheet included two charts. The rendering script 35_Chart_render handles this, but overlays the first output file with the second. It is changed to produce files with different names. --- .../33_Chart_create_scatter5_trendlines.php | 2 +- samples/Chart/35_Chart_render.php | 3 + .../32readwriteScatterChartTrendlines1.xlsx | Bin 15275 -> 15334 bytes src/PhpSpreadsheet/Chart/TrendLine.php | 108 +++++++++++++++++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 20 +++- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 24 ++++ .../Chart/TrendLineTest.php | 12 ++ 7 files changed, 163 insertions(+), 6 deletions(-) diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index a640f735..5beb82cd 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -193,7 +193,7 @@ $dataSeriesValues = [ // 3- moving Avg (period=2) single-arrow trendline, w=1.5, same color as marker fill; no dispRSqr, no dispEq $trendLines = [ new TrendLine(TrendLine::TRENDLINE_LINEAR, null, null, false, false), - new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true), + new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true, 20.0, 28.0, 44104.5, 'metric3 polynomial fit'), new TrendLine(TrendLine::TRENDLINE_MOVING_AVG, null, 2, true), ]; $dataSeriesValues[0]->setTrendLines($trendLines); diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 2376008c..891cd27c 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -73,6 +73,9 @@ foreach ($inputFileNames as $inputFileName) { $helper->log(' ' . $chartName . ' - ' . $caption); $jpegFile = $helper->getFilename('35-' . $inputFileNameShort, 'png'); + if ($i !== 0) { + $jpegFile = substr($jpegFile, 0, -3) . "$i.png"; + } if (file_exists($jpegFile)) { unlink($jpegFile); } diff --git a/samples/templates/32readwriteScatterChartTrendlines1.xlsx b/samples/templates/32readwriteScatterChartTrendlines1.xlsx index f48366fe0a3705bfaa1c91671a26c72b054b72e1..c8411d4daf83f0d6095c3d8721efaa54423bc5cb 100644 GIT binary patch delta 3291 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 Z00000#FIliJ^_-Gf;%GyU^f5&004C^C3^q> delta 3230 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 Q0auf~J0k`qHvj+t0D=4oYybcN diff --git a/src/PhpSpreadsheet/Chart/TrendLine.php b/src/PhpSpreadsheet/Chart/TrendLine.php index e177f819..75a5896c 100644 --- a/src/PhpSpreadsheet/Chart/TrendLine.php +++ b/src/PhpSpreadsheet/Chart/TrendLine.php @@ -34,13 +34,44 @@ class TrendLine extends Properties /** @var bool */ private $dispEq = false; + /** @var string */ + private $name = ''; + + /** @var float */ + private $backward = 0.0; + + /** @var float */ + private $forward = 0.0; + + /** @var float */ + private $intercept = 0.0; + /** * Create a new TrendLine object. */ - public function __construct(string $trendLineType = '', ?int $order = null, ?int $period = null, bool $dispRSqr = false, bool $dispEq = false) - { + public function __construct( + string $trendLineType = '', + ?int $order = null, + ?int $period = null, + bool $dispRSqr = false, + bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ) { parent::__construct(); - $this->setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + $this->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); } public function getTrendLineType(): string @@ -103,8 +134,65 @@ class TrendLine extends Properties return $this; } - public function setTrendLineProperties(?string $trendLineType = null, ?int $order = 0, ?int $period = 0, ?bool $dispRSqr = false, ?bool $dispEq = false): self + public function getName(): string { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getBackward(): float + { + return $this->backward; + } + + public function setBackward(float $backward): self + { + $this->backward = $backward; + + return $this; + } + + public function getForward(): float + { + return $this->forward; + } + + public function setForward(float $forward): self + { + $this->forward = $forward; + + return $this; + } + + public function getIntercept(): float + { + return $this->intercept; + } + + public function setIntercept(float $intercept): self + { + $this->intercept = $intercept; + + return $this; + } + + public function setTrendLineProperties( + ?string $trendLineType = null, + ?int $order = 0, + ?int $period = 0, + ?bool $dispRSqr = false, + ?bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ): self { if (!empty($trendLineType)) { $this->setTrendLineType($trendLineType); } @@ -120,6 +208,18 @@ class TrendLine extends Properties if ($dispEq !== null) { $this->setDispEq($dispEq); } + if ($backward !== null) { + $this->setBackward($backward); + } + if ($forward !== null) { + $this->setForward($forward); + } + if ($intercept !== null) { + $this->setIntercept($intercept); + } + if ($name !== null) { + $this->setName($name); + } return $this; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 76316db9..9eb30256 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -531,7 +531,25 @@ class Chart $order = self::getAttribute($seriesDetail->order, 'val', 'integer'); /** @var ?int */ $period = self::getAttribute($seriesDetail->period, 'val', 'integer'); - $trendLine->setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + /** @var ?float */ + $forward = self::getAttribute($seriesDetail->forward, 'val', 'float'); + /** @var ?float */ + $backward = self::getAttribute($seriesDetail->backward, 'val', 'float'); + /** @var ?float */ + $intercept = self::getAttribute($seriesDetail->intercept, 'val', 'float'); + /** @var ?string */ + $name = (string) $seriesDetail->name; + $trendLine->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); $trendLines[] = $trendLine; break; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 48f7d255..8dab9529 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -1158,10 +1158,19 @@ class Chart extends WriterPart $period = $trendLine->getPeriod(); $dispRSqr = $trendLine->getDispRSqr(); $dispEq = $trendLine->getDispEq(); + $forward = $trendLine->getForward(); + $backward = $trendLine->getBackward(); + $intercept = $trendLine->getIntercept(); + $name = $trendLine->getName(); $trendLineColor = $trendLine->getLineColor(); // ChartColor $trendLineWidth = $trendLine->getLineStyleProperty('width'); $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' + if ($name !== '') { + $objWriter->startElement('c:name'); + $objWriter->writeRawData($name); + $objWriter->endElement(); // c:name + } $objWriter->startElement('c:spPr'); if (!$trendLineColor->isUsable()) { @@ -1181,6 +1190,21 @@ class Chart extends WriterPart $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell' $objWriter->writeAttribute('val', $trendLineType); $objWriter->endElement(); // trendlineType + if ($backward !== 0.0) { + $objWriter->startElement('c:backward'); + $objWriter->writeAttribute('val', "$backward"); + $objWriter->endElement(); // c:backward + } + if ($forward !== 0.0) { + $objWriter->startElement('c:forward'); + $objWriter->writeAttribute('val', "$forward"); + $objWriter->endElement(); // c:forward + } + if ($intercept !== 0.0) { + $objWriter->startElement('c:intercept'); + $objWriter->writeAttribute('val', "$intercept"); + $objWriter->endElement(); // c:intercept + } if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) { $objWriter->startElement('c:order'); $objWriter->writeAttribute('val', $order); diff --git a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php index c8550d83..954a7336 100644 --- a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php +++ b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php @@ -73,6 +73,10 @@ class TrendLineTest extends AbstractFunctional self::assertSame('accent4', $lineColor->getValue()); self::assertSame('stealth', $trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); self::assertEquals(0.5, $trendLine->getLineStyleProperty('width')); + self::assertSame('', $trendLine->getName()); + self::assertSame(0.0, $trendLine->getBackward()); + self::assertSame(0.0, $trendLine->getForward()); + self::assertSame(0.0, $trendLine->getIntercept()); $trendLine = $trendLines[1]; self::assertSame('poly', $trendLine->getTrendLineType()); @@ -82,6 +86,10 @@ class TrendLineTest extends AbstractFunctional self::assertSame('accent3', $lineColor->getValue()); self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); self::assertEquals(1.25, $trendLine->getLineStyleProperty('width')); + self::assertSame('metric3 polynomial', $trendLine->getName()); + self::assertSame(20.0, $trendLine->getBackward()); + self::assertSame(28.0, $trendLine->getForward()); + self::assertSame(14400.5, $trendLine->getIntercept()); $trendLine = $trendLines[2]; self::assertSame('movingAvg', $trendLine->getTrendLineType()); @@ -91,6 +99,10 @@ class TrendLineTest extends AbstractFunctional self::assertSame('accent2', $lineColor->getValue()); self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); self::assertEquals(1.5, $trendLine->getLineStyleProperty('width')); + self::assertSame('', $trendLine->getName()); + self::assertSame(0.0, $trendLine->getBackward()); + self::assertSame(0.0, $trendLine->getForward()); + self::assertSame(0.0, $trendLine->getIntercept()); $reloadedSpreadsheet->disconnectWorksheets(); } From ca90379dc421c6a05059890961cbc0cde5af40ac Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 Aug 2022 23:15:16 -0700 Subject: [PATCH 099/156] 2 Minor Phpstan-related Fixes (#3030) For one of the Phpstan upgrades, some message text had changed so drastically that the only practical solution at the time was to move the messages from phpstan-baseline.neon to phpstan.neon.dist. This was not ideal, but it allowed us time to move on and study the errors, which I have now done. At one point, Parser is expecting a variable to be an array, and that was not clear from the code. If not an array, the code will error out (which was Phpstan's concern); I have changed it to throw an exception instead. This satisfies Phpstan, and I can get the message out of neon.dist (without needing to restore it to baseline). Unsurprisingly, the exception was never thrown in the existing test suite, although I added a couple of tests to exercise that code. In Helper/Dimension, Phpstan flagged a statement inappropriately. I suppressed the message using an annotation and filed a bug report https://github.com/phpstan/phpstan/issues/7563. A fix for the problem was merged yesterday, which is good, but it puts us in a tenuous position. The annotation is needed now, but, when the fix is inevitably pushed to the version we use, the no-longer-needed annotation will trigger a different message. Recode so that neither the current nor the future versions will issue a message, eliminating the annotation in the process. --- phpstan.neon.dist | 5 -- src/PhpSpreadsheet/Helper/Dimension.php | 14 ++++- src/PhpSpreadsheet/Writer/Xls/Parser.php | 7 ++- .../Writer/Xls/ParserTest.php | 56 +++++++++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5cac36a1..64b325c6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -21,8 +21,3 @@ parameters: # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' - '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' - - # Some issues in Xls/Parser between 1.6.3 and 1.7.7 - - - message: "#^Offset '(left|right|value)' does not exist on (non-empty-array\\|string|array\\|null)\\.$#" - path: src/PhpSpreadsheet/Writer/Xls/Parser.php diff --git a/src/PhpSpreadsheet/Helper/Dimension.php b/src/PhpSpreadsheet/Helper/Dimension.php index 425c9a61..ff07ce5b 100644 --- a/src/PhpSpreadsheet/Helper/Dimension.php +++ b/src/PhpSpreadsheet/Helper/Dimension.php @@ -55,10 +55,20 @@ class Dimension */ protected $unit; + /** + * Phpstan bug has been fixed; this function allows us to + * pass Phpstan whether fixed or not. + * + * @param mixed $value + */ + private static function stanBugFixed($value): array + { + return is_array($value) ? $value : [null, null]; + } + public function __construct(string $dimension) { - // @phpstan-ignore-next-line - [$size, $unit] = sscanf($dimension, '%[1234567890.]%s'); + [$size, $unit] = self::stanBugFixed(sscanf($dimension, '%[1234567890.]%s')); $unit = strtolower(trim($unit ?? '')); $size = (float) $size; diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index ca9b67b5..ca407d2a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -78,7 +78,7 @@ class Parser /** * The parse tree to be generated. * - * @var string + * @var array|string */ public $parseTree; @@ -1445,6 +1445,9 @@ class Parser if (empty($tree)) { // If it's the first call use parseTree $tree = $this->parseTree; } + if (!is_array($tree) || !isset($tree['left'], $tree['right'], $tree['value'])) { + throw new WriterException('Unexpected non-array'); + } if (is_array($tree['left'])) { $converted_tree = $this->toReversePolish($tree['left']); @@ -1475,7 +1478,7 @@ class Parser $left_tree = ''; } - // add it's left subtree and return. + // add its left subtree and return. return $left_tree . $this->convertFunction($tree['value'], $tree['right']); } $converted_tree = $this->convert($tree['value']); diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php new file mode 100644 index 00000000..38fd29d3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php @@ -0,0 +1,56 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + public function testNonArray(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unexpected non-array'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(); + } + + public function testMissingIndex(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unexpected non-array'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(['left' => 0]); + } + + public function testParseError(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unknown token +'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => '+']); + } + + public function testGoodParse(): void + { + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + self::assertSame('1e01001e02001e0300', bin2hex($parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => 3]))); + } +} From 026f699a6503f352d8c359dc3e867bc14f8f5e3b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 29 Aug 2022 17:14:03 +0200 Subject: [PATCH 100/156] Minor documentation updates --- docs/topics/calculation-engine.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/topics/calculation-engine.md b/docs/topics/calculation-engine.md index 4fd300e8..7dc838f7 100644 --- a/docs/topics/calculation-engine.md +++ b/docs/topics/calculation-engine.md @@ -22,6 +22,13 @@ with PhpSpreadsheet, it evaluates to the value "64": ![09-command-line-calculation.png](./images/09-command-line-calculation.png) +When writing a formula to a cell, formulae should always be set as they would appear in an English version of Microsoft Office Excel, and PhpSpreadsheet handles all formulae internally in this format. This means that the following rules hold: + + - Decimal separator is `.` (period) + - Function argument separator is `,` (comma) + - Matrix row separator is `;` (semicolon) + - English function names must be used + Another nice feature of PhpSpreadsheet's formula parser, is that it can automatically adjust a formula when inserting/removing rows/columns. Here's an example: @@ -43,6 +50,11 @@ inserted 2 new rows), changed to "SUM(E4:E11)". Also, the inserted cells duplicate style information of the previous cell, just like Excel's behaviour. Note that you can both insert rows and columns. +If you want to "anchor" a specific cell for a formula, then you prefix the column and/or the row with a `$` symbol, exactly as you would in MS Excel itself. +So if a formula contains "SUM(E$4:E9)", and you insert 2 new rows after row 1, the formula will be adjusted to read "SUM(E$4:E11)", with the `$` fixing row 4 as the start of the range. + + + ## Calculation Cache Once the Calculation engine has evaluated the formula in a cell, the result From 9eb5e7e976d1fa59742008ce39c704a334355ac4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:15:50 -0700 Subject: [PATCH 101/156] Phpstan Baseline < 4000 Lines Part 1 (#3023) A lot of easily fixed problems throughout Writer/Xlsx/*, mostly supplying int rather than string as input to WriteAttribute/WriteElement. There are, in fact, so many opportunities, that I will split it over 2 or 3 PRs. But this first one will get Phpstan baseline down to the goal on its own. Some of the other problems are also easily fixed. In particular, the docBlocks in Style/ConditionalFormatting/ConditionalDataBar do not allow for null values, and should. --- phpstan-baseline.neon | 91 ------------------- .../ConditionalDataBar.php | 21 ++--- src/PhpSpreadsheet/Writer/Xlsx/Comments.php | 6 +- src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 6 +- src/PhpSpreadsheet/Writer/Xlsx/Workbook.php | 14 +-- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 78 ++++++++-------- .../ConditionalFormattingDataBarXlsxTest.php | 24 +++-- 7 files changed, 71 insertions(+), 169 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7c0070d7..f78c4fae 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3810,31 +3810,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx.php - - - message: "#^Parameter \\#1 \\$string of function substr expects string, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Comments.php - - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Comments.php - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#" count: 1 @@ -3999,74 +3979,3 @@ parameters: message: "#^Result of \\|\\| is always true\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Writer/Xlsx/Workbook.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^If condition is always true\\.$#" - count: 6 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeAttributeIf\\(\\) has parameter \\$condition with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeDataBarElements\\(\\) has parameter \\$dataBar with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeElementIf\\(\\) has parameter \\$condition with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 15 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 3 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<1, max\\> given\\.$#" - count: 9 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#3 \\$stringTable of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeSheetData\\(\\) expects array\\, array\\\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#4 \\$val of static method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeAttributeIf\\(\\) expects string, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php index 54513670..f7a2eee1 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -11,10 +11,10 @@ class ConditionalDataBar /** children */ - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $minimumConditionalFormatValueObject; - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $maximumConditionalFormatValueObject; /** @var string */ @@ -22,7 +22,7 @@ class ConditionalDataBar /** */ - /** @var ConditionalFormattingRuleExtension */ + /** @var ?ConditionalFormattingRuleExtension */ private $conditionalFormattingRuleExt; /** @@ -43,10 +43,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMinimumConditionalFormatValueObject() + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->minimumConditionalFormatValueObject; } @@ -58,10 +55,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMaximumConditionalFormatValueObject() + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->maximumConditionalFormatValueObject; } @@ -85,10 +79,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormattingRuleExtension - */ - public function getConditionalFormattingRuleExt() + public function getConditionalFormattingRuleExt(): ?ConditionalFormattingRuleExtension { return $this->conditionalFormattingRuleExt; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php index ea0f1faa..5045e8f3 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php @@ -165,7 +165,7 @@ class Comments extends WriterPart // Metadata [$column, $row] = Coordinate::indexesFromString($cellReference); $id = 1024 + $column + $row; - $id = substr($id, 0, 4); + $id = substr("$id", 0, 4); // v:shape $objWriter->startElement('v:shape'); @@ -223,10 +223,10 @@ class Comments extends WriterPart $objWriter->writeElement('x:AutoFill', 'False'); // x:Row - $objWriter->writeElement('x:Row', ($row - 1)); + $objWriter->writeElement('x:Row', (string) ($row - 1)); // x:Column - $objWriter->writeElement('x:Column', ($column - 1)); + $objWriter->writeElement('x:Column', (string) ($column - 1)); $objWriter->endElement(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 43ce442f..cb8758c2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -56,7 +56,7 @@ class DocProps extends WriterPart // Variant $objWriter->startElement('vt:variant'); - $objWriter->writeElement('vt:i4', $spreadsheet->getSheetCount()); + $objWriter->writeElement('vt:i4', (string) $spreadsheet->getSheetCount()); $objWriter->endElement(); $objWriter->endElement(); @@ -68,7 +68,7 @@ class DocProps extends WriterPart // Vector $objWriter->startElement('vt:vector'); - $objWriter->writeAttribute('size', $spreadsheet->getSheetCount()); + $objWriter->writeAttribute('size', (string) $spreadsheet->getSheetCount()); $objWriter->writeAttribute('baseType', 'lpstr'); $sheetCount = $spreadsheet->getSheetCount(); @@ -207,7 +207,7 @@ class DocProps extends WriterPart $objWriter->startElement('property'); $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'); - $objWriter->writeAttribute('pid', $key + 2); + $objWriter->writeAttribute('pid', (string) ($key + 2)); $objWriter->writeAttribute('name', $customProperty); switch ($propertyType) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index f9d7197d..7d08388d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -104,14 +104,14 @@ class Workbook extends WriterPart // workbookView $objWriter->startElement('workbookView'); - $objWriter->writeAttribute('activeTab', $spreadsheet->getActiveSheetIndex()); + $objWriter->writeAttribute('activeTab', (string) $spreadsheet->getActiveSheetIndex()); $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false')); - $objWriter->writeAttribute('firstSheet', $spreadsheet->getFirstSheetIndex()); + $objWriter->writeAttribute('firstSheet', (string) $spreadsheet->getFirstSheetIndex()); $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false')); $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false')); $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false')); $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false')); - $objWriter->writeAttribute('tabRatio', $spreadsheet->getTabRatio()); + $objWriter->writeAttribute('tabRatio', (string) $spreadsheet->getTabRatio()); $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility()); $objWriter->endElement(); @@ -157,9 +157,9 @@ class Workbook extends WriterPart $objWriter->writeAttribute('calcId', '999999'); $objWriter->writeAttribute('calcMode', 'auto'); // fullCalcOnLoad isn't needed if we've recalculating for the save - $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? 1 : 0); - $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? 0 : 1); - $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? 0 : 1); + $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? '1' : '0'); + $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? '0' : '1'); + $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? '0' : '1'); $objWriter->endElement(); } @@ -200,7 +200,7 @@ class Workbook extends WriterPart // Write sheet $objWriter->startElement('sheet'); $objWriter->writeAttribute('name', $worksheetName); - $objWriter->writeAttribute('sheetId', $worksheetId); + $objWriter->writeAttribute('sheetId', (string) $worksheetId); if ($sheetState !== 'visible' && $sheetState != '') { $objWriter->writeAttribute('state', $sheetState); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 4b0cb632..5680281f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -28,7 +28,7 @@ class Worksheet extends WriterPart * * @return string XML Output */ - public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = null, $includeCharts = false) + public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false) { // Create XML writer $objWriter = null; @@ -149,7 +149,7 @@ class Worksheet extends WriterPart } $autoFilterRange = $worksheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { - $objWriter->writeAttribute('filterMode', 1); + $objWriter->writeAttribute('filterMode', '1'); if (!$worksheet->getAutoFilter()->getEvaluated()) { $worksheet->getAutoFilter()->showHideRows(); } @@ -158,7 +158,7 @@ class Worksheet extends WriterPart // tabColor if ($worksheet->isTabColorSet()) { $objWriter->startElement('tabColor'); - $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB()); + $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB() ?? ''); $objWriter->endElement(); } @@ -218,7 +218,7 @@ class Worksheet extends WriterPart // Show zeros (Excel also writes this attribute only if set to false) if ($worksheet->getSheetView()->getShowZeros() === false) { - $objWriter->writeAttribute('showZeros', 0); + $objWriter->writeAttribute('showZeros', '0'); } // View Layout Type @@ -252,7 +252,7 @@ class Worksheet extends WriterPart // Pane $pane = ''; if ($worksheet->getFreezePane()) { - [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane() ?? ''); + [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane()); $xSplit = Coordinate::columnIndexFromString($xSplit); --$xSplit; --$ySplit; @@ -261,7 +261,7 @@ class Worksheet extends WriterPart $pane = 'topRight'; $objWriter->startElement('pane'); if ($xSplit > 0) { - $objWriter->writeAttribute('xSplit', $xSplit); + $objWriter->writeAttribute('xSplit', "$xSplit"); } if ($ySplit > 0) { $objWriter->writeAttribute('ySplit', $ySplit); @@ -334,7 +334,7 @@ class Worksheet extends WriterPart $outlineLevelRow = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow); + $objWriter->writeAttribute('outlineLevelRow', (string) (int) $outlineLevelRow); // Outline level - column $outlineLevelCol = 0; @@ -343,7 +343,7 @@ class Worksheet extends WriterPart $outlineLevelCol = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol); + $objWriter->writeAttribute('outlineLevelCol', (string) (int) $outlineLevelCol); $objWriter->endElement(); } @@ -363,8 +363,8 @@ class Worksheet extends WriterPart foreach ($worksheet->getColumnDimensions() as $colDimension) { // col $objWriter->startElement('col'); - $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); - $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('min', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('max', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); if ($colDimension->getWidth() < 0) { // No width set, apply default of 10 @@ -396,11 +396,11 @@ class Worksheet extends WriterPart // Outline level if ($colDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel()); + $objWriter->writeAttribute('outlineLevel', (string) $colDimension->getOutlineLevel()); } // Style - $objWriter->writeAttribute('style', $colDimension->getXfIndex()); + $objWriter->writeAttribute('style', (string) $colDimension->getXfIndex()); $objWriter->endElement(); } @@ -423,7 +423,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm()); $objWriter->writeAttribute('hashValue', $protection->getPassword()); $objWriter->writeAttribute('saltValue', $protection->getSalt()); - $objWriter->writeAttribute('spinCount', $protection->getSpinCount()); + $objWriter->writeAttribute('spinCount', (string) $protection->getSpinCount()); } elseif ($protection->getPassword() !== '') { $objWriter->writeAttribute('password', $protection->getPassword()); } @@ -447,7 +447,7 @@ class Worksheet extends WriterPart $objWriter->endElement(); } - private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeAttribute($attr, $val); @@ -461,7 +461,7 @@ class Worksheet extends WriterPart } } - private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeElementIf(XMLWriter $objWriter, bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeElement($attr, $val); @@ -568,7 +568,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute($attrKey, $val); } $minCfvo = $dataBar->getMinimumConditionalFormatValueObject(); - if ($minCfvo) { + if ($minCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $minCfvo->getType()); if ($minCfvo->getCellFormula()) { @@ -578,7 +578,7 @@ class Worksheet extends WriterPart } $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject(); - if ($maxCfvo) { + if ($maxCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $maxCfvo->getType()); if ($maxCfvo->getCellFormula()) { @@ -600,9 +600,8 @@ class Worksheet extends WriterPart $objWriter->endElement(); //end conditionalFormatting } - private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void + private static function writeDataBarElements(XMLWriter $objWriter, ?ConditionalDataBar $dataBar): void { - /** @var ConditionalDataBar $dataBar */ if ($dataBar) { $objWriter->startElement('dataBar'); self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0'); @@ -669,7 +668,7 @@ class Worksheet extends WriterPart 'dxfId', (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) ); - $objWriter->writeAttribute('priority', $id++); + $objWriter->writeAttribute('priority', (string) $id++); self::writeAttributeif( $objWriter, @@ -724,7 +723,7 @@ class Worksheet extends WriterPart if (!empty($dataValidationCollection)) { $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); $objWriter->startElement('dataValidations'); - $objWriter->writeAttribute('count', count($dataValidationCollection)); + $objWriter->writeAttribute('count', (string) count($dataValidationCollection)); foreach ($dataValidationCollection as $coordinate => $dv) { $objWriter->startElement('dataValidation'); @@ -922,11 +921,11 @@ class Worksheet extends WriterPart $rules = $column->getRules(); if (count($rules) > 0) { $objWriter->startElement('filterColumn'); - $objWriter->writeAttribute('colId', $worksheet->getAutoFilter()->getColumnOffset($columnID)); + $objWriter->writeAttribute('colId', (string) $worksheet->getAutoFilter()->getColumnOffset($columnID)); $objWriter->startElement($column->getFilterType()); if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) { - $objWriter->writeAttribute('and', 1); + $objWriter->writeAttribute('and', '1'); } foreach ($rules as $rule) { @@ -936,7 +935,7 @@ class Worksheet extends WriterPart ($rule->getValue() === '') ) { // Filter rule for Blanks - $objWriter->writeAttribute('blank', 1); + $objWriter->writeAttribute('blank', '1'); } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) { // Dynamic Filter Rule $objWriter->writeAttribute('type', $rule->getGrouping()); @@ -1019,24 +1018,24 @@ class Worksheet extends WriterPart { // pageSetup $objWriter->startElement('pageSetup'); - $objWriter->writeAttribute('paperSize', $worksheet->getPageSetup()->getPaperSize()); + $objWriter->writeAttribute('paperSize', (string) $worksheet->getPageSetup()->getPaperSize()); $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation()); if ($worksheet->getPageSetup()->getScale() !== null) { - $objWriter->writeAttribute('scale', $worksheet->getPageSetup()->getScale()); + $objWriter->writeAttribute('scale', (string) $worksheet->getPageSetup()->getScale()); } if ($worksheet->getPageSetup()->getFitToHeight() !== null) { - $objWriter->writeAttribute('fitToHeight', $worksheet->getPageSetup()->getFitToHeight()); + $objWriter->writeAttribute('fitToHeight', (string) $worksheet->getPageSetup()->getFitToHeight()); } else { $objWriter->writeAttribute('fitToHeight', '0'); } if ($worksheet->getPageSetup()->getFitToWidth() !== null) { - $objWriter->writeAttribute('fitToWidth', $worksheet->getPageSetup()->getFitToWidth()); + $objWriter->writeAttribute('fitToWidth', (string) $worksheet->getPageSetup()->getFitToWidth()); } else { $objWriter->writeAttribute('fitToWidth', '0'); } if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) { - $objWriter->writeAttribute('firstPageNumber', $worksheet->getPageSetup()->getFirstPageNumber()); + $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder()); @@ -1089,8 +1088,8 @@ class Worksheet extends WriterPart // rowBreaks if (!empty($aRowBreaks)) { $objWriter->startElement('rowBreaks'); - $objWriter->writeAttribute('count', count($aRowBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks)); + $objWriter->writeAttribute('count', (string) count($aRowBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aRowBreaks)); foreach ($aRowBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); @@ -1107,14 +1106,14 @@ class Worksheet extends WriterPart // Second, write column breaks if (!empty($aColumnBreaks)) { $objWriter->startElement('colBreaks'); - $objWriter->writeAttribute('count', count($aColumnBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks)); + $objWriter->writeAttribute('count', (string) count($aColumnBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks)); foreach ($aColumnBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); $objWriter->startElement('brk'); - $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1); + $objWriter->writeAttribute('id', (string) (Coordinate::columnIndexFromString($coords[0]) - 1)); $objWriter->writeAttribute('man', '1'); $objWriter->endElement(); } @@ -1166,7 +1165,7 @@ class Worksheet extends WriterPart if ($writeCurrentRow) { // Start a new row $objWriter->startElement('row'); - $objWriter->writeAttribute('r', $currentRow); + $objWriter->writeAttribute('r', "$currentRow"); $objWriter->writeAttribute('spans', '1:' . $colCount); // Row dimensions @@ -1187,12 +1186,12 @@ class Worksheet extends WriterPart // Outline level if ($rowDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); + $objWriter->writeAttribute('outlineLevel', (string) $rowDimension->getOutlineLevel()); } // Style if ($rowDimension->getXfIndex() !== null) { - $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); + $objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex()); $objWriter->writeAttribute('customFormat', '1'); } @@ -1263,7 +1262,7 @@ class Worksheet extends WriterPart $cellValue = $cellValue . '.0'; } } - $objWriter->writeElement('v', $cellValue); + $objWriter->writeElement('v', "$cellValue"); } private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void @@ -1332,7 +1331,7 @@ class Worksheet extends WriterPart // Sheet styles $xfi = $pCell->getXfIndex(); - self::writeAttributeIf($objWriter, $xfi, 's', $xfi); + self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi"); // If cell value is supplied, write cell value $cellValue = $pCell->getValue(); @@ -1449,7 +1448,6 @@ class Worksheet extends WriterPart /** @var Conditional $conditional */ foreach ($conditionalStyles as $conditional) { $dataBar = $conditional->getDataBar(); - // @phpstan-ignore-next-line if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) { $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php index 4b051dd0..0ec25d8e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php @@ -86,8 +86,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase $dataBar = $conditionalRule->getDataBar(); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals(Color::COLOR_GREEN, $dataBar->getColor()); @@ -109,8 +109,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); @@ -118,6 +118,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar->getConditionalFormattingRuleExt()); //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{72C64AE0-5CD9-164F-83D1-AB720F263E79}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('A3:A23', $rule1ext->getSqref()); @@ -165,8 +166,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('num', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('num', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals('-5', $dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -175,6 +176,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar->getConditionalFormattingRuleExt()); //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{98904F60-57F0-DF47-B480-691B20D325E3}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('B3:B23', $rule1ext->getSqref()); @@ -224,8 +226,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEmpty($dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -235,6 +237,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{453C04BA-7ABD-8548-8A17-D9CFD2BDABE9}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('C3:C23', $rule1ext->getSqref()); @@ -286,8 +289,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotNull($dataBar); self::assertTrue($dataBar->getShowValue()); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('formula', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('formula', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals('3+2', $dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -297,6 +300,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{6C1E066A-E240-3D4A-98F8-8CC218B0DFD2}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('D3:D23', $rule1ext->getSqref()); From 4f8aa806bc8b95e9d2633e91aeb0a822f12eb89a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:34:20 -0700 Subject: [PATCH 102/156] Phpstan Baseline < 4000 Lines Part 2 Html (#3037) Continue to reduce the size of Phpstan Baseline by fixing problems reported for Writer/Html. --- phpstan-baseline.neon | 310 ----------------------------- phpstan.neon.dist | 2 +- src/PhpSpreadsheet/Writer/Html.php | 102 +++++----- 3 files changed, 54 insertions(+), 360 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f78c4fae..cfaed694 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3155,316 +3155,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Worksheet/Worksheet.php - - - message: "#^Call to function array_key_exists\\(\\) with int and array\\{none\\: 'none', dashDot\\: '1px dashed', dashDotDot\\: '1px dotted', dashed\\: '1px dashed', dotted\\: '1px dotted', double\\: '3px double', hair\\: '1px solid', medium\\: '2px solid', \\.\\.\\.\\} will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 'mime' on array\\|false\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 0 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot call method getSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot call method getSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$candidateSpannedRow with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$sheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateHTMLFooter\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has parameter \\$desc with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has parameter \\$val with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$cellAddress with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$columnNumber with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cellType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cssClass with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValue\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValue\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValueRich\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValueRich\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowIncludeCharts\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowIncludeCharts\\(\\) has parameter \\$coordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$colSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$rowSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cellType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$colNum with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$colSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$coordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cssClass with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$rowSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetPrep\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has parameter \\$rowMin with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has parameter \\$sheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$tbodyStart with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$theadEnd with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$theadStart with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableFooter\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$id with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTagInline\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTagInline\\(\\) has parameter \\$id with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$borderStyle of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapBorderStyle\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$font of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:createCSSStyleFont\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$hAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapHAlign\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$vAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapVAlign\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int\\<0, max\\>\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#3 \\$use_include_path of function fopen expects bool, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - message: "#^Negated boolean expression is always false\\.$#" count: 1 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 64b325c6..30bd6c2f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -16,7 +16,7 @@ parameters: processTimeout: 300.0 checkMissingIterableValueType: false ignoreErrors: - - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy) expects (GdImage|resource), GdImage\|resource given\.$~' + - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy|imagepng) expects (GdImage|resource), GdImage\|resource given\.$~' - '~^Parameter \#2 \$src_im(age)? of function imagecopy expects (GdImage|resource), GdImage\|resource given\.$~' # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 68e200f5..6a400673 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -287,7 +287,7 @@ class Html extends BaseWriter /** * Map border style. * - * @param int $borderStyle Sheet index + * @param int|string $borderStyle Sheet index * * @return string */ @@ -354,7 +354,7 @@ class Html extends BaseWriter return $this; } - private static function generateMeta($val, $desc) + private static function generateMeta(?string $val, string $desc): string { return $val ? (' ' . PHP_EOL) @@ -398,7 +398,7 @@ class Html extends BaseWriter return $html; } - private function generateSheetPrep() + private function generateSheetPrep(): array { // Ensure that Spans have been calculated? $this->calculateSpans(); @@ -413,7 +413,7 @@ class Html extends BaseWriter return $sheets; } - private function generateSheetStarts($sheet, $rowMin) + private function generateSheetStarts(Worksheet $sheet, int $rowMin): array { // calculate start of , $tbodyStart = $rowMin; @@ -432,7 +432,7 @@ class Html extends BaseWriter return [$theadStart, $theadEnd, $tbodyStart]; } - private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart) + private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array { // ? $startTag = ($row == $theadStart) ? (' ' . PHP_EOL) : ''; @@ -682,7 +682,7 @@ class Html extends BaseWriter if ($this->embedImages || substr($imageData, 0, 6) === 'zip://') { $picture = @file_get_contents($filename); if ($picture !== false) { - $imageDetails = getimagesize($filename); + $imageDetails = getimagesize($filename) ?: []; // base64 encode the binary data $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; @@ -697,12 +697,10 @@ class Html extends BaseWriter $imageResource = $drawing->getImageResource(); if ($imageResource) { ob_start(); // Let's start output buffering. - // @phpstan-ignore-next-line imagepng($imageResource); // This will normally output the image, but because of ob_start(), it won't. - $contents = ob_get_contents(); // Instead, output above is saved to $contents + $contents = (string) ob_get_contents(); // Instead, output above is saved to $contents ob_end_clean(); // End the output buffer. - /** @phpstan-ignore-next-line */ $dataUri = 'data:image/png;base64,' . base64_encode($contents); // Because of the nature of tables, width is more important than height. @@ -738,21 +736,18 @@ class Html extends BaseWriter } $html .= PHP_EOL; - $imageDetails = getimagesize($chartFileName); + $imageDetails = getimagesize($chartFileName) ?: []; $filedesc = $chart->getTitle(); $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; - if ($fp = fopen($chartFileName, 'rb', 0)) { - $picture = fread($fp, filesize($chartFileName)); - fclose($fp); - /** @phpstan-ignore-next-line */ + $picture = file_get_contents($chartFileName); + if ($picture !== false) { $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; $html .= '' . $filedesc . '' . PHP_EOL; - - unlink($chartFileName); } + unlink($chartFileName); } } } @@ -993,8 +988,8 @@ class Html extends BaseWriter $css = []; // Create CSS - $css['vertical-align'] = $this->mapVAlign($alignment->getVertical()); - $textAlign = $this->mapHAlign($alignment->getHorizontal()); + $css['vertical-align'] = $this->mapVAlign($alignment->getVertical() ?? ''); + $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? ''); if ($textAlign) { $css['text-align'] = $textAlign; if (in_array($textAlign, ['left', 'right'])) { @@ -1070,10 +1065,8 @@ class Html extends BaseWriter * Create CSS style. * * @param Border $border Border - * - * @return string */ - private function createCSSStyleBorder(Border $border) + private function createCSSStyleBorder(Border $border): string { // Create CSS - add !important to non-none border styles for merged cells $borderStyle = $this->mapBorderStyle($border->getBorderStyle()); @@ -1095,7 +1088,8 @@ class Html extends BaseWriter // Create CSS if ($fill->getFillType() !== Fill::FILL_NONE) { - $value = '#' . $fill->getStartColor()->getRGB(); + $value = $fill->getFillType() == Fill::FILL_NONE ? + 'white' : '#' . $fill->getStartColor()->getRGB(); $css['background-color'] = $value; } @@ -1105,7 +1099,7 @@ class Html extends BaseWriter /** * Generate HTML footer. */ - public function generateHTMLFooter() + public function generateHTMLFooter(): string { // Construct HTML $html = ''; @@ -1115,7 +1109,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTagInline(Worksheet $worksheet, $id) + private function generateTableTagInline(Worksheet $worksheet, string $id): string { $style = isset($this->cssStyles['table']) ? $this->assembleCSS($this->cssStyles['table']) : ''; @@ -1135,7 +1129,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void + private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void { if (!$this->useInlineCss) { $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; @@ -1188,7 +1182,7 @@ class Html extends BaseWriter /** * Generate table footer. */ - private function generateTableFooter() + private function generateTableFooter(): string { return ' ' . PHP_EOL . '
' . PHP_EOL; } @@ -1234,7 +1228,7 @@ class Html extends BaseWriter return $html; } - private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $row, $columnNumber) + private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array { $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : ''; $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1); @@ -1260,22 +1254,24 @@ class Html extends BaseWriter return [$cell, $cssClass, $coordinate]; } - private function generateRowCellDataValueRich($cell, &$cellData): void + private function generateRowCellDataValueRich(Cell $cell, string &$cellData): void { // Loop through rich text elements $elements = $cell->getValue()->getRichTextElements(); foreach ($elements as $element) { // Rich text start? if ($element instanceof Run) { - $cellData .= ''; - $cellEnd = ''; - if ($element->getFont()->getSuperscript()) { - $cellData .= ''; - $cellEnd = ''; - } elseif ($element->getFont()->getSubscript()) { - $cellData .= ''; - $cellEnd = ''; + if ($element->getFont() !== null) { + $cellData .= ''; + + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + $cellEnd = ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + $cellEnd = ''; + } } // Convert UTF8 data to PCDATA @@ -1293,7 +1289,7 @@ class Html extends BaseWriter } } - private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void + private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, ?string &$cellData): void { if ($cell->getValue() instanceof RichText) { $this->generateRowCellDataValueRich($cell, $cellData); @@ -1319,7 +1315,11 @@ class Html extends BaseWriter } } - private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType) + /** + * @param null|Cell|string $cell + * @param array|string $cssClass + */ + private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, string $cellType): string { $cellData = ' '; if ($cell instanceof Cell) { @@ -1339,10 +1339,10 @@ class Html extends BaseWriter $cellData = nl2br($cellData); // Extend CSS class? - if (!$this->useInlineCss) { + if (!$this->useInlineCss && is_string($cssClass)) { $cssClass .= ' style' . $cell->getXfIndex(); $cssClass .= ' ' . $cell->getDataType(); - } else { + } elseif (is_array($cssClass)) { if ($cellType == 'th') { if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); @@ -1372,12 +1372,12 @@ class Html extends BaseWriter return $cellData; } - private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate) + private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string { return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : ''; } - private function generateRowSpans($html, $rowSpan, $colSpan) + private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string { $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : ''; $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : ''; @@ -1385,7 +1385,10 @@ class Html extends BaseWriter return $html; } - private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row): void + /** + * @param array|string $cssClass + */ + private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void { // Image? $htmlx = $this->writeImageInCell($worksheet, $coordinate); @@ -1393,7 +1396,7 @@ class Html extends BaseWriter $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start $html .= ' <' . $cellType; - if (!$this->useInlineCss && !$this->isPdf) { + if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) { $html .= ' class="' . $cssClass . '"'; if ($htmlx) { $html .= " style='position: relative;'"; @@ -1403,9 +1406,11 @@ class Html extends BaseWriter // We must explicitly write the width of the element because TCPDF // does not recognize e.g. if ($this->useInlineCss) { - $xcssClass = $cssClass; + $xcssClass = is_array($cssClass) ? $cssClass : []; } else { - $html .= ' class="' . $cssClass . '"'; + if (is_string($cssClass)) { + $html .= ' class="' . $cssClass . '"'; + } $xcssClass = []; } $width = 0; @@ -1416,8 +1421,7 @@ class Html extends BaseWriter $width += $this->columnWidths[$sheetIndex][$i]; } } - $xcssClass['width'] = $width . 'pt'; - + $xcssClass['width'] = (string) $width . 'pt'; // We must also explicitly write the height of the element because TCPDF // does not recognize e.g. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) { @@ -1736,7 +1740,7 @@ class Html extends BaseWriter $this->spansAreCalculated = true; } - private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void + private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void { // Identify which rows should be omitted in HTML. These are the rows where all the cells // participate in a merge and the where base cells are somewhere above. From ad2d3df2f7ab48ca9aa7b92cfc2cdc38a4e78cb9 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 3 Sep 2022 12:58:23 +0200 Subject: [PATCH 103/156] Update Excel function samples for Database functions --- samples/Calculations/Database/DAVERAGE.php | 18 +-- samples/Calculations/Database/DCOUNT.php | 33 ++--- samples/Calculations/Database/DCOUNTA.php | 58 +++++++++ samples/Calculations/Database/DGET.php | 24 ++-- samples/Calculations/Database/DMAX.php | 16 +-- samples/Calculations/Database/DMIN.php | 16 +-- samples/Calculations/Database/DPRODUCT.php | 24 ++-- samples/Calculations/Database/DSTDEV.php | 18 +-- samples/Calculations/Database/DSTDEVP.php | 19 +-- samples/Calculations/Database/DSUM.php | 58 +++++++++ samples/Calculations/Database/DVAR.php | 19 +-- samples/Calculations/Database/DVARP.php | 18 +-- src/PhpSpreadsheet/Helper/Sample.php | 28 +++++ src/PhpSpreadsheet/Helper/TextGrid.php | 139 +++++++++++++++++++++ 14 files changed, 401 insertions(+), 87 deletions(-) create mode 100644 samples/Calculations/Database/DCOUNTA.php create mode 100644 samples/Calculations/Database/DSUM.php create mode 100644 src/PhpSpreadsheet/Helper/TextGrid.php diff --git a/samples/Calculations/Database/DAVERAGE.php b/samples/Calculations/Database/DAVERAGE.php index 92d84014..fa388c8b 100644 --- a/samples/Calculations/Database/DAVERAGE.php +++ b/samples/Calculations/Database/DAVERAGE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the average of selected database entries.'); +$category = 'Database'; +$functionName = 'DAVERAGE'; +$description = 'Returns the average of selected database entries that match criteria'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DAVERAGE(A4:E10,3,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DAVERAGE() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DAVERAGE() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DCOUNT.php b/samples/Calculations/Database/DCOUNT.php index d869a4bc..68d6be18 100644 --- a/samples/Calculations/Database/DCOUNT.php +++ b/samples/Calculations/Database/DCOUNT.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Counts the cells that contain numbers in a database.'); + +$category = 'Database'; +$functionName = 'DCOUNT'; +$description = 'Counts the cells that contain numbers in a set of database records that match criteria'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -14,9 +19,9 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ['Apple', 18, 20, 14, 105.00], ['Pear', 12, 12, 10, 96.00], ['Cherry', 13, 14, 9, 105.00], - ['Apple', 14, 15, 10, 75.00], - ['Pear', 9, 8, 8, 76.80], - ['Apple', 8, 9, 6, 45.00], + ['Apple', 14, 'N/A', 10, 75.00], + ['Pear', 9, 8, 8, 77.00], + ['Apple', 12, 11, 6, 45.00], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], ['="=Apple"', '>10', null, null, null, '<16'], @@ -26,30 +31,28 @@ $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], $worksheet->fromArray($criteria, null, 'A1'); $worksheet->fromArray($database, null, 'A4'); -$worksheet->setCellValue('A12', 'The Number of Apple trees over 10\' in height'); -$worksheet->setCellValue('B12', '=DCOUNT(A4:E10,"Yield",A1:B2)'); +$worksheet->setCellValue('A12', 'The Number of Apple trees between 10\' and 16\' in height whose age is known'); +$worksheet->setCellValue('B12', '=DCOUNT(A4:E10,"Age",A1:F2)'); -$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard'); +$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard with a numeric value in column 3 ("Age")'); $worksheet->setCellValue('B13', '=DCOUNT(A4:E10,3,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); -var_dump($criteriaData); +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DCOUNT() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DCOUNT() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DCOUNTA.php b/samples/Calculations/Database/DCOUNTA.php new file mode 100644 index 00000000..3b4cc16e --- /dev/null +++ b/samples/Calculations/Database/DCOUNTA.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], + ['Apple', 18, 20, 14, 105.00], + ['Pear', 12, 12, 10, 96.00], + ['Cherry', 13, 14, 9, 105.00], + ['Apple', 14, 'N/A', 10, 75.00], + ['Pear', 9, 8, 8, 77.00], + ['Apple', 12, 11, 6, 45.00], +]; +$criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], + ['="=Apple"', '>10', null, null, null, '<16'], + ['="=Pear"', null, null, null, null, null], +]; + +$worksheet->fromArray($criteria, null, 'A1'); +$worksheet->fromArray($database, null, 'A4'); + +$worksheet->setCellValue('A12', 'The Number of Apple trees between 10\' and 16\' in height'); +$worksheet->setCellValue('B12', '=DCOUNTA(A4:E10,"Age",A1:F2)'); + +$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard'); +$worksheet->setCellValue('B13', '=DCOUNTA(A4:E10,3,A1:A3)'); + +$helper->log('Database'); + +$databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); +$helper->displayGrid($databaseData); + +// Test the formulae +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); + +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DGET.php b/samples/Calculations/Database/DGET.php index 9f543c91..1b41b777 100644 --- a/samples/Calculations/Database/DGET.php +++ b/samples/Calculations/Database/DGET.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Extracts a single value from a column of a list or database that matches conditions that you specify.'); +$category = 'Database'; +$functionName = 'DGET'; +$description = 'Extracts a single value from a column of a list or database that matches criteria that you specify'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -21,7 +25,7 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], ['="=Apple"', '>10', null, null, null, '<16'], - ['="=Pear"', null, null, null, null, null], + ['="=Pear"', '>12', null, null, null, null], ]; $worksheet->fromArray($criteria, null, 'A1'); @@ -30,23 +34,25 @@ $worksheet->fromArray($database, null, 'A4'); $worksheet->setCellValue('A12', 'The height of the Apple tree between 10\' and 16\' tall'); $worksheet->setCellValue('B12', '=DGET(A4:E10,"Height",A1:F2)'); +$worksheet->setCellValue('A13', 'The height of the Apple tree (will return an Excel error, because there is more than one apple tree)'); +$worksheet->setCellValue('B13', '=DGET(A4:E10,"Height",A1:A2)'); + $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$helper->log('ALL'); +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DMAX.php b/samples/Calculations/Database/DMAX.php index c48928d4..0998904b 100644 --- a/samples/Calculations/Database/DMAX.php +++ b/samples/Calculations/Database/DMAX.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the maximum value from selected database entries.'); +$category = 'Database'; +$functionName = 'DMAX'; +$description = 'Returns the maximum value from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,20 +40,18 @@ $worksheet->setCellValue('B13', '=DMAX(A4:E10,3,A1:A2)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $helper->log('ALL'); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DMIN.php b/samples/Calculations/Database/DMIN.php index 7bcaa206..c0e0a8d8 100644 --- a/samples/Calculations/Database/DMIN.php +++ b/samples/Calculations/Database/DMIN.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the minimum value from selected database entries.'); +$category = 'Database'; +$functionName = 'DMIN'; +$description = 'Returns the minimum value from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,20 +40,18 @@ $worksheet->setCellValue('B13', '=DMIN(A4:E10,3,A1:A2)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $helper->log('ALL'); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMIN() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMIN() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DPRODUCT.php b/samples/Calculations/Database/DPRODUCT.php index 7c14ded6..fa666ffe 100644 --- a/samples/Calculations/Database/DPRODUCT.php +++ b/samples/Calculations/Database/DPRODUCT.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Multiplies the values in a column of a list or database that match conditions that you specify.'); +$category = 'Database'; +$functionName = 'DPRODUCT'; +$description = 'Multiplies the values in a column of a list or database that match conditions that you specify'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -16,7 +20,7 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ['Pear', 12, 12, 10, 96.00], ['Cherry', 13, 14, 9, 105.00], ['Apple', 14, 15, 10, 75.00], - ['Pear', 9, 8, 8, 76.80], + ['Pear', 9, 8, 8, 77.00], ['Apple', 8, 9, 6, 45.00], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], @@ -30,23 +34,25 @@ $worksheet->fromArray($database, null, 'A4'); $worksheet->setCellValue('A12', 'The product of the yields of all Apple trees over 10\' in the orchard'); $worksheet->setCellValue('B12', '=DPRODUCT(A4:E10,"Yield",A1:B2)'); +$worksheet->setCellValue('A13', 'The product of the yields of all Apple trees in the orchard'); +$worksheet->setCellValue('B13', '=DPRODUCT(A4:E10,"Yield",A1:A2)'); + $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$helper->log('ALL'); +$criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSTDEV.php b/samples/Calculations/Database/DSTDEV.php index 7f09fa59..b6499741 100644 --- a/samples/Calculations/Database/DSTDEV.php +++ b/samples/Calculations/Database/DSTDEV.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Estimates the standard deviation based on a sample of selected database entries.'); +$category = 'Database'; +$functionName = 'DSTDEV'; +$description = 'Estimates the standard deviation based on a sample of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DSTDEV(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DSTDEV() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DSTDEV() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSTDEVP.php b/samples/Calculations/Database/DSTDEVP.php index 9e999a80..d8bcec4b 100644 --- a/samples/Calculations/Database/DSTDEVP.php +++ b/samples/Calculations/Database/DSTDEVP.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Calculates the standard deviation based on the entire population of selected database entries.'); + +$category = 'Database'; +$functionName = 'DSTDEVP'; +$description = 'Calculates the standard deviation based on the entire population of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -35,21 +40,19 @@ $worksheet->setCellValue('B13', '=DSTDEVP(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DSTDEVP() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DSTDEVP() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSUM.php b/samples/Calculations/Database/DSUM.php new file mode 100644 index 00000000..d2d773c9 --- /dev/null +++ b/samples/Calculations/Database/DSUM.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], + ['Apple', 18, 20, 14, 105.00], + ['Pear', 12, 12, 10, 96.00], + ['Cherry', 13, 14, 9, 105.00], + ['Apple', 14, 15, 10, 75.00], + ['Pear', 9, 8, 8, 76.80], + ['Apple', 8, 9, 6, 45.00], +]; +$criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], + ['="=Apple"', '>10', null, null, null, '<16'], + ['="=Pear"', null, null, null, null, null], +]; + +$worksheet->fromArray($criteria, null, 'A1'); +$worksheet->fromArray($database, null, 'A4'); + +$worksheet->setCellValue('A12', 'The total profit from apple trees'); +$worksheet->setCellValue('B12', '=DSUM(A4:E10,"Profit",A1:A2)'); + +$worksheet->setCellValue('A13', 'Total profit from apple trees with a height between 10 and 16 feet, and all pear trees'); +$worksheet->setCellValue('B13', '=DSUM(A4:E10,"Profit",A1:F3)'); + +$helper->log('Database'); + +$databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); +$helper->displayGrid($databaseData); + +// Test the formulae +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); + +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:F3', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DVAR.php b/samples/Calculations/Database/DVAR.php index 2a5f8749..4165d076 100644 --- a/samples/Calculations/Database/DVAR.php +++ b/samples/Calculations/Database/DVAR.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Estimates variance based on a sample from selected database entries.'); + +$category = 'Database'; +$functionName = 'DVAR'; +$description = 'Estimates variance based on a sample from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -35,21 +40,19 @@ $worksheet->setCellValue('B13', '=DVAR(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DVAR() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DVAR() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DVARP.php b/samples/Calculations/Database/DVARP.php index 4f57113b..5a17415e 100644 --- a/samples/Calculations/Database/DVARP.php +++ b/samples/Calculations/Database/DVARP.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Calculates variance based on the entire population of selected database entries,'); +$category = 'Database'; +$functionName = 'DVARP'; +$description = 'Calculates variance based on the entire population of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DVARP(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DVARP() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DVARP() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 8ce37003..aeb2bea6 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Helper; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\IWriter; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -182,6 +183,33 @@ class Sample echo date('H:i:s ') . $message . $eol; } + public function titles(string $category, string $functionName, ?string $description = null): void + { + $this->log(sprintf('%s Functions:', $category)); + $description === null + ? $this->log(sprintf('%s()', rtrim($functionName, '()'))) + : $this->log(sprintf('%s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + } + + public function displayGrid(array $matrix): void + { + $renderer = new TextGrid($matrix, $this->isCli()); + echo $renderer->render(); + } + + public function logCalculationResult( + Worksheet $worksheet, + string $functionName, + string $formulaCell, + ?string $descriptionCell = null + ): void { + if ($descriptionCell !== null) { + $this->log($worksheet->getCell($descriptionCell)->getValue()); + } + $this->log($worksheet->getCell($formulaCell)->getValue()); + $this->log(sprintf('%s() Result is ', $functionName) . $worksheet->getCell($formulaCell)->getCalculatedValue()); + } + /** * Log ending notes. */ diff --git a/src/PhpSpreadsheet/Helper/TextGrid.php b/src/PhpSpreadsheet/Helper/TextGrid.php new file mode 100644 index 00000000..acb9ae60 --- /dev/null +++ b/src/PhpSpreadsheet/Helper/TextGrid.php @@ -0,0 +1,139 @@ +rows = array_keys($matrix); + $this->columns = array_keys($matrix[$this->rows[0]]); + + $matrix = array_values($matrix); + array_walk( + $matrix, + function (&$row): void { + $row = array_values($row); + } + ); + + $this->matrix = $matrix; + $this->isCli = $isCli; + } + + public function render(): string + { + $this->gridDisplay = $this->isCli ? '' : ''; + + $maxRow = max($this->rows); + $maxRowLength = strlen((string) $maxRow) + 1; + $columnWidths = $this->getColumnWidths($this->matrix); + + $this->renderColumnHeader($maxRowLength, $columnWidths); + $this->renderRows($maxRowLength, $columnWidths); + $this->renderFooter($maxRowLength, $columnWidths); + + $this->gridDisplay .= $this->isCli ? '' : ''; + + return $this->gridDisplay; + } + + private function renderRows(int $maxRowLength, array $columnWidths): void + { + foreach ($this->matrix as $row => $rowData) { + $this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' '; + $this->renderCells($rowData, $columnWidths); + $this->gridDisplay .= '|' . PHP_EOL; + } + } + + private function renderCells(array $rowData, array $columnWidths): void + { + foreach ($rowData as $column => $cell) { + $cell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell); + $this->gridDisplay .= '| '; + $this->gridDisplay .= str_pad($cell, $columnWidths[$column] + 1, ' '); + } + } + + private function renderColumnHeader(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1); + } + $this->gridDisplay .= '+' . PHP_EOL; + + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' '); + } + $this->gridDisplay .= '|' . PHP_EOL; + + $this->renderFooter($maxRowLength, $columnWidths); + } + + private function renderFooter(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-'; + $this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-'); + } + $this->gridDisplay .= '+' . PHP_EOL; + } + + private function getColumnWidths(array $matrix): array + { + $columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix); + $columnWidths = []; + for ($column = 0; $column < $columnCount; ++$column) { + $columnWidths[] = $this->getColumnWidth(array_column($this->matrix, $column)); + } + + return $columnWidths; + } + + private function getColumnWidth(array $columnData): int + { + $columnWidth = 0; + $columnData = array_values($columnData); + + foreach ($columnData as $columnValue) { + if (is_string($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue)); + } elseif (is_bool($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue ? 'TRUE' : 'FALSE')); + } + + $columnWidth = max($columnWidth, strlen((string) $columnWidth)); + } + + return $columnWidth; + } +} From 5f33ec0eeafed69bc19e1d0c25b1995650964979 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:00:01 -0700 Subject: [PATCH 104/156] Phpstan Baseline < 4000 Lines Part 3 (#3041) The last of these changes for now. No remaining Phpstan complaints in any Writer/Xlsx. Number of lines remaining in Phpstan baseline is now below 3500. --- phpstan-baseline.neon | 195 ------------------ src/PhpSpreadsheet/Writer/Xlsx.php | 8 +- .../Writer/Xlsx/DefinedNames.php | 2 +- src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 7 +- .../Writer/Xlsx/StringTable.php | 54 +++-- src/PhpSpreadsheet/Writer/Xlsx/Style.php | 118 ++++++----- 6 files changed, 111 insertions(+), 273 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cfaed694..53fa9057 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3474,198 +3474,3 @@ parameters: message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xls\\\\Xf\\:\\:\\$diag is never read, only written\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xls/Xf.php - - - - message: "#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Parameter \\#1 \\$path of function basename expects string, array\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Parameter \\#1 \\$path of function dirname expects string, array\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Possibly invalid array key type array\\|string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\:\\:\\$pathNames has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$type with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Parameter \\#2 \\$id of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeRelationship\\(\\) expects int, string given\\.$#" - count: 5 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Parameter \\#4 \\$target of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeRelationship\\(\\) expects string, array\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Cannot call method getBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Instanceof between \\*NEVER\\* and PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\RichText will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Instanceof between string and PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\RichText will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#1 \\$text of method PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\RichText\\:\\:createTextRun\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getStyle\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Conditional\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Comparison operation \"\\<\" between int\\ and 0 is always true\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$borders of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeBorder\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Borders, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Borders\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$fill of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeFill\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Fill, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Fill\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$font of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeFont\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$numberFormat of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeNumFmt\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 22 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<1, max\\> given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 5aca5117..c9446e70 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -349,12 +349,15 @@ class Xlsx extends BaseWriter //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) if ($this->spreadSheet->hasRibbon()) { $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); + $tmpRibbonTarget = is_string($tmpRibbonTarget) ? $tmpRibbonTarget : ''; $zipContent[$tmpRibbonTarget] = $this->spreadSheet->getRibbonXMLData('data'); if ($this->spreadSheet->hasRibbonBinObjects()) { $tmpRootPath = dirname($tmpRibbonTarget) . '/'; $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write - foreach ($ribbonBinObjects as $aPath => $aContent) { - $zipContent[$tmpRootPath . $aPath] = $aContent; + if (is_array($ribbonBinObjects)) { + foreach ($ribbonBinObjects as $aPath => $aContent) { + $zipContent[$tmpRootPath . $aPath] = $aContent; + } } //the rels for files $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet); @@ -684,6 +687,7 @@ class Xlsx extends BaseWriter return $this; } + /** @var array */ private $pathNames = []; private function addZipFile(string $path, string $content): void diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index b8285fcb..b3338c07 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -118,7 +118,7 @@ class DefinedNames $range[1] = Coordinate::absoluteCoordinate($range[1]); $range = implode(':', $range); - $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle() ?? '') . '\'!' . $range); + $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range); $this->objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 238fb5bf..99fa2d34 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -67,12 +67,13 @@ class Rels extends WriterPart 'xl/workbook.xml' ); // a custom UI in workbook ? + $target = $spreadsheet->getRibbonXMLData('target'); if ($spreadsheet->hasRibbon()) { $this->writeRelationShip( $objWriter, 5, 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility', - $spreadsheet->getRibbonXMLData('target') + is_string($target) ? $target : '' ); } @@ -284,7 +285,7 @@ class Rels extends WriterPart return $objWriter->getData(); } - private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, $relationship, $type): void + private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, string $relationship, string $type): void { $unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData(); if (!isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()][$relationship])) { @@ -448,7 +449,7 @@ class Rels extends WriterPart /** * Write Override content type. * - * @param int $id Relationship ID. rId will be prepended! + * @param int|string $id Relationship ID. rId will be prepended! * @param string $type Relationship type * @param string $target Relationship target * @param string $targetMode Relationship target mode diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index 8b293bc1..078f940a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -66,7 +66,7 @@ class StringTable extends WriterPart /** * Write string table to XML format. * - * @param string[] $stringTable + * @param (string|RichText)[] $stringTable * * @return string XML Output */ @@ -86,13 +86,13 @@ class StringTable extends WriterPart // String table $objWriter->startElement('sst'); $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); - $objWriter->writeAttribute('uniqueCount', count($stringTable)); + $objWriter->writeAttribute('uniqueCount', (string) count($stringTable)); // Loop through string table foreach ($stringTable as $textElement) { $objWriter->startElement('si'); - if (!$textElement instanceof RichText) { + if (!($textElement instanceof RichText)) { $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement); $objWriter->startElement('t'); if ($textToWrite !== trim($textToWrite)) { @@ -100,7 +100,7 @@ class StringTable extends WriterPart } $objWriter->writeRawData($textToWrite); $objWriter->endElement(); - } elseif ($textElement instanceof RichText) { + } else { $this->writeRichText($objWriter, $textElement); } @@ -130,14 +130,16 @@ class StringTable extends WriterPart $objWriter->startElement($prefix . 'r'); // rPr - if ($element instanceof Run) { + if ($element instanceof Run && $element->getFont() !== null) { // rPr $objWriter->startElement($prefix . 'rPr'); // rFont - $objWriter->startElement($prefix . 'rFont'); - $objWriter->writeAttribute('val', $element->getFont()->getName()); - $objWriter->endElement(); + if ($element->getFont()->getName() !== null) { + $objWriter->startElement($prefix . 'rFont'); + $objWriter->writeAttribute('val', $element->getFont()->getName()); + $objWriter->endElement(); + } // Bold $objWriter->startElement($prefix . 'b'); @@ -166,19 +168,25 @@ class StringTable extends WriterPart $objWriter->endElement(); // Color - $objWriter->startElement($prefix . 'color'); - $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); - $objWriter->endElement(); + if ($element->getFont()->getColor()->getARGB() !== null) { + $objWriter->startElement($prefix . 'color'); + $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); + $objWriter->endElement(); + } // Size - $objWriter->startElement($prefix . 'sz'); - $objWriter->writeAttribute('val', $element->getFont()->getSize()); - $objWriter->endElement(); + if ($element->getFont()->getSize() !== null) { + $objWriter->startElement($prefix . 'sz'); + $objWriter->writeAttribute('val', (string) $element->getFont()->getSize()); + $objWriter->endElement(); + } // Underline - $objWriter->startElement($prefix . 'u'); - $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); - $objWriter->endElement(); + if ($element->getFont()->getUnderline() !== null) { + $objWriter->startElement($prefix . 'u'); + $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); + $objWriter->endElement(); + } $objWriter->endElement(); } @@ -201,10 +209,10 @@ class StringTable extends WriterPart */ public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = ''): void { - if (!$richText instanceof RichText) { + if (!($richText instanceof RichText)) { $textRun = $richText; $richText = new RichText(); - $run = $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun ?? ''); $run->setFont(null); } @@ -226,9 +234,9 @@ class StringTable extends WriterPart } // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? '1' : '0')); // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? '1' : '0')); // Underline $underlineType = $element->getFont()->getUnderline(); switch ($underlineType) { @@ -241,7 +249,9 @@ class StringTable extends WriterPart break; } - $objWriter->writeAttribute('u', $underlineType); + if ($underlineType !== null) { + $objWriter->writeAttribute('u', $underlineType); + } // Strikethrough $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); // Superscript/subscript diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php index cb2e3850..b24b7533 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -40,7 +40,7 @@ class Style extends WriterPart // numFmts $objWriter->startElement('numFmts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getNumFmtHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getNumFmtHashTable()->count()); // numFmt for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) { @@ -51,54 +51,63 @@ class Style extends WriterPart // fonts $objWriter->startElement('fonts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFontHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFontHashTable()->count()); // font for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) { - $this->writeFont($objWriter, $this->getParentWriter()->getFontHashTable()->getByIndex($i)); + $thisfont = $this->getParentWriter()->getFontHashTable()->getByIndex($i); + if ($thisfont !== null) { + $this->writeFont($objWriter, $thisfont); + } } $objWriter->endElement(); // fills $objWriter->startElement('fills'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFillHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFillHashTable()->count()); // fill for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) { - $this->writeFill($objWriter, $this->getParentWriter()->getFillHashTable()->getByIndex($i)); + $thisfill = $this->getParentWriter()->getFillHashTable()->getByIndex($i); + if ($thisfill !== null) { + $this->writeFill($objWriter, $thisfill); + } } $objWriter->endElement(); // borders $objWriter->startElement('borders'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getBordersHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getBordersHashTable()->count()); // border for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) { - $this->writeBorder($objWriter, $this->getParentWriter()->getBordersHashTable()->getByIndex($i)); + $thisborder = $this->getParentWriter()->getBordersHashTable()->getByIndex($i); + if ($thisborder !== null) { + $this->writeBorder($objWriter, $thisborder); + } } $objWriter->endElement(); // cellStyleXfs $objWriter->startElement('cellStyleXfs'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('numFmtId', 0); - $objWriter->writeAttribute('fontId', 0); - $objWriter->writeAttribute('fillId', 0); - $objWriter->writeAttribute('borderId', 0); + $objWriter->writeAttribute('numFmtId', '0'); + $objWriter->writeAttribute('fontId', '0'); + $objWriter->writeAttribute('fillId', '0'); + $objWriter->writeAttribute('borderId', '0'); $objWriter->endElement(); $objWriter->endElement(); // cellXfs $objWriter->startElement('cellXfs'); - $objWriter->writeAttribute('count', count($spreadsheet->getCellXfCollection())); + $objWriter->writeAttribute('count', (string) count($spreadsheet->getCellXfCollection())); // xf foreach ($spreadsheet->getCellXfCollection() as $cellXf) { @@ -109,24 +118,27 @@ class Style extends WriterPart // cellStyles $objWriter->startElement('cellStyles'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // cellStyle $objWriter->startElement('cellStyle'); $objWriter->writeAttribute('name', 'Normal'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('builtinId', 0); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('builtinId', '0'); $objWriter->endElement(); $objWriter->endElement(); // dxfs $objWriter->startElement('dxfs'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getStylesConditionalHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getStylesConditionalHashTable()->count()); // dxf for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) { - $this->writeCellStyleDxf($objWriter, $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i)->getStyle()); + $thisstyle = $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i); + if ($thisstyle !== null) { + $this->writeCellStyleDxf($objWriter, $thisstyle->getStyle()); + } } $objWriter->endElement(); @@ -171,17 +183,19 @@ class Style extends WriterPart // gradientFill $objWriter->startElement('gradientFill'); - $objWriter->writeAttribute('type', $fill->getFillType()); - $objWriter->writeAttribute('degree', $fill->getRotation()); + $objWriter->writeAttribute('type', (string) $fill->getFillType()); + $objWriter->writeAttribute('degree', (string) $fill->getRotation()); // stop $objWriter->startElement('stop'); $objWriter->writeAttribute('position', '0'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getStartColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -190,9 +204,11 @@ class Style extends WriterPart $objWriter->writeAttribute('position', '1'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getEndColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -220,7 +236,7 @@ class Style extends WriterPart // patternFill $objWriter->startElement('patternFill'); - $objWriter->writeAttribute('patternType', $fill->getFillType()); + $objWriter->writeAttribute('patternType', (string) $fill->getFillType()); if (self::writePatternColors($fill)) { // fgColor @@ -360,20 +376,20 @@ class Style extends WriterPart { // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('fontId', (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('fontId', (string) (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); if ($style->getQuotePrefix()) { - $objWriter->writeAttribute('quotePrefix', 1); + $objWriter->writeAttribute('quotePrefix', '1'); } if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { - $objWriter->writeAttribute('numFmtId', (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); + $objWriter->writeAttribute('numFmtId', (string) (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); } else { - $objWriter->writeAttribute('numFmtId', (int) $style->getNumberFormat()->getBuiltInFormatCode()); + $objWriter->writeAttribute('numFmtId', (string) (int) $style->getNumberFormat()->getBuiltInFormatCode()); } - $objWriter->writeAttribute('fillId', (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); - $objWriter->writeAttribute('borderId', (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); + $objWriter->writeAttribute('fillId', (string) (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); + $objWriter->writeAttribute('borderId', (string) (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); // Apply styles? $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $style->getFont()->getHashCode()) ? '1' : '0'); @@ -387,25 +403,25 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $objWriter->writeAttribute('horizontal', (string) $style->getAlignment()->getHorizontal()); + $objWriter->writeAttribute('vertical', (string) $style->getAlignment()->getVertical()); $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); $objWriter->writeAttribute('wrapText', ($style->getAlignment()->getWrapText() ? 'true' : 'false')); $objWriter->writeAttribute('shrinkToFit', ($style->getAlignment()->getShrinkToFit() ? 'true' : 'false')); if ($style->getAlignment()->getIndent() > 0) { - $objWriter->writeAttribute('indent', $style->getAlignment()->getIndent()); + $objWriter->writeAttribute('indent', (string) $style->getAlignment()->getIndent()); } if ($style->getAlignment()->getReadOrder() > 0) { - $objWriter->writeAttribute('readingOrder', $style->getAlignment()->getReadOrder()); + $objWriter->writeAttribute('readingOrder', (string) $style->getAlignment()->getReadOrder()); } $objWriter->endElement(); @@ -454,10 +470,10 @@ class Style extends WriterPart $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); } $objWriter->endElement(); @@ -465,7 +481,7 @@ class Style extends WriterPart $this->writeBorder($objWriter, $style->getBorders()); // protection - if (($style->getProtection()->getLocked() !== null) || ($style->getProtection()->getHidden() !== null)) { + if ((!empty($style->getProtection()->getLocked())) || (!empty($style->getProtection()->getHidden()))) { if ( $style->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT || $style->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT @@ -503,11 +519,13 @@ class Style extends WriterPart $objWriter->writeAttribute('style', $border->getBorderStyle()); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); - $objWriter->endElement(); + if ($border->getColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); + $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); + } } } @@ -516,15 +534,15 @@ class Style extends WriterPart * * @param int $id Number Format identifier */ - private function writeNumFmt(XMLWriter $objWriter, NumberFormat $numberFormat, $id = 0): void + private function writeNumFmt(XMLWriter $objWriter, ?NumberFormat $numberFormat, $id = 0): void { // Translate formatcode - $formatCode = $numberFormat->getFormatCode(); + $formatCode = ($numberFormat === null) ? null : $numberFormat->getFormatCode(); // numFmt if ($formatCode !== null) { $objWriter->startElement('numFmt'); - $objWriter->writeAttribute('numFmtId', ($id + 164)); + $objWriter->writeAttribute('numFmtId', (string) ($id + 164)); $objWriter->writeAttribute('formatCode', $formatCode); $objWriter->endElement(); } From 57a72037b5c8976e28e305ce72d45bc59369250a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 4 Sep 2022 09:45:29 -0700 Subject: [PATCH 105/156] Phpstan and Xlsx Reader (#3044) Eliminate most Phpstan messages in Xlsx Reader. In combination with similar changes to Xlsx Writer, baseline will shrink to just over 3,000 lines. --- phpstan-baseline.neon | 450 ------------------ src/PhpSpreadsheet/Reader/Xlsx.php | 82 ++-- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 14 +- .../Reader/Xlsx/BaseParserClass.php | 5 +- .../Reader/Xlsx/ColumnAndRowAttributes.php | 12 +- .../Reader/Xlsx/ConditionalStyles.php | 22 +- .../Reader/Xlsx/DataValidations.php | 4 +- src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php | 2 + src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php | 17 +- .../Reader/Xlsx/SheetViewOptions.php | 11 +- 10 files changed, 109 insertions(+), 510 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 53fa9057..cc2eaa3d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1660,456 +1660,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot access offset 0 on array\\\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot access property \\$r on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method addChart\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Comparison operation \"\\>\" between SimpleXMLElement\\|null and 0 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToBoolean\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToBoolean\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToError\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToError\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$calculatedValue with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$castBaseType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$cellDataType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$r with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$sharedFormulas with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToString\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToString\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has parameter \\$add with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has parameter \\$base with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getArrayItem\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getArrayItem\\(\\) has parameter \\$array with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getArrayItem\\(\\) has parameter \\$key with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getFromZipArchive\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readFormControlProperties\\(\\) has parameter \\$dir with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readFormControlProperties\\(\\) has parameter \\$docSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readFormControlProperties\\(\\) has parameter \\$fileWorksheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readPrinterSettings\\(\\) has parameter \\$dir with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readPrinterSettings\\(\\) has parameter \\$docSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readPrinterSettings\\(\\) has parameter \\$fileWorksheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:stripWhiteSpaceFromStyleString\\(\\) has parameter \\$string with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:toCSSArray\\(\\) has parameter \\$style with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Negated boolean expression is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$fontSizeInPoints of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:fontSizeToPixels\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$sizeInCm of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:centimeterSizeToPixels\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$sizeInInch of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:inchSizeToPixels\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$worksheetName of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getSheetByName\\(\\) expects string, array\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:readAutoFilter\\(\\) has parameter \\$autoFilterRange with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:readAutoFilter\\(\\) has parameter \\$xmlSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Parameter \\#1 \\$operator of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\\\Rule\\:\\:setRule\\(\\) expects string, null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\BaseParserClass\\:\\:boolean\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\BaseParserClass\\:\\:boolean\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredColumn\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredColumn\\(\\) has parameter \\$columnCoordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredRow\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredRow\\(\\) has parameter \\$rowCoordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnAttributes\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnRangeAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnRangeAttributes\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readRowAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readRowAttributes\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readConditionalStyles\\(\\) has parameter \\$xmlSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarExtLstOfConditionalRule\\(\\) has parameter \\$cfRule with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarExtLstOfConditionalRule\\(\\) has parameter \\$conditionalFormattingRuleExtensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarOfConditionalRule\\(\\) has parameter \\$cfRule with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarOfConditionalRule\\(\\) has parameter \\$conditionalFormattingRuleExtensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readStyleRules\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readStyleRules\\(\\) has parameter \\$cfRules with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readStyleRules\\(\\) has parameter \\$extLst with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:setConditionalStyles\\(\\) has parameter \\$xmlExtLst with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:\\$dxfs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\DataValidations\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\DataValidations\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Hyperlinks\\:\\:\\$hyperlinks has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Hyperlinks\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:load\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:pageSetup\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\SheetViewOptions\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\SheetViewOptions\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 630fd1dd..244baddd 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -31,6 +31,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Font; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Color; +use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; @@ -293,7 +294,7 @@ class Xlsx extends BaseReader return $worksheetInfo; } - private static function castToBoolean($c) + private static function castToBoolean(SimpleXMLElement $c): bool { $value = isset($c->v) ? (string) $c->v : null; if ($value == '0') { @@ -305,17 +306,21 @@ class Xlsx extends BaseReader return (bool) $c->v; } - private static function castToError($c) + private static function castToError(SimpleXMLElement $c): ?string { return isset($c->v) ? (string) $c->v : null; } - private static function castToString($c) + private static function castToString(SimpleXMLElement $c): ?string { return isset($c->v) ? (string) $c->v : null; } - private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType): void + /** + * @param mixed $value + * @param mixed $calculatedValue + */ + private function castToFormula(SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void { $attr = $c->f->attributes(); $cellDataType = 'f'; @@ -389,7 +394,7 @@ class Xlsx extends BaseReader $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); } - return $contents; + return ($contents === false) ? '' : $contents; } /** @@ -1143,7 +1148,7 @@ class Xlsx extends BaseReader } // Header/footer images - if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) { + if ($xmlSheet && $xmlSheet->legacyDrawingHF) { if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS); $vmlRelationship = ''; @@ -1550,7 +1555,7 @@ class Xlsx extends BaseReader break; case '_xlnm.Print_Area': - $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; $newRangeSets = []; foreach ($rangeSets as $rangeSet) { [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); @@ -1605,7 +1610,7 @@ class Xlsx extends BaseReader if (strpos((string) $definedName, '!') !== false) { $range[0] = str_replace("''", "'", $range[0]); $range[0] = str_replace("'", '', $range[0]); - if ($worksheet = $excel->getSheetByName($range[0])) { + if ($worksheet = $excel->getSheetByName($range[0])) { // @phpstan-ignore-line $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); } else { $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope)); @@ -1626,7 +1631,7 @@ class Xlsx extends BaseReader // Need to split on a comma or a space if not in quotes, and extract the first part. $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $definedRange); // Extract sheet name - [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); + [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); // @phpstan-ignore-line $extractedSheetName = trim($extractedSheetName, "'"); // Locate sheet @@ -1673,7 +1678,7 @@ class Xlsx extends BaseReader if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { - $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); + $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); // @phpstan-ignore-line $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); // For oneCellAnchor or absoluteAnchor positioned charts, // toCoordinate is not in the data. Does it need to be calculated? @@ -1721,28 +1726,29 @@ class Xlsx extends BaseReader if (isset($is->t)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { - if (is_object($is->r)) { + if (isset($is->r) && is_object($is->r)) { /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); } else { $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + $objFont = $objText->getFont() ?? new StyleFont(); if (isset($run->rPr->rFont)) { $attr = $run->rPr->rFont->attributes(); if (isset($attr['val'])) { - $objText->getFont()->setName((string) $attr['val']); + $objFont->setName((string) $attr['val']); } } if (isset($run->rPr->sz)) { $attr = $run->rPr->sz->attributes(); if (isset($attr['val'])) { - $objText->getFont()->setSize((float) $attr['val']); + $objFont->setSize((float) $attr['val']); } } if (isset($run->rPr->color)) { - $objText->getFont()->setColor(new Color($this->styleReader->readColor($run->rPr->color))); + $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color))); } if (isset($run->rPr->b)) { $attr = $run->rPr->b->attributes(); @@ -1750,7 +1756,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setBold(true); + $objFont->setBold(true); } } if (isset($run->rPr->i)) { @@ -1759,7 +1765,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setItalic(true); + $objFont->setItalic(true); } } if (isset($run->rPr->vertAlign)) { @@ -1767,19 +1773,19 @@ class Xlsx extends BaseReader if (isset($attr['val'])) { $vertAlign = strtolower((string) $attr['val']); if ($vertAlign == 'superscript') { - $objText->getFont()->setSuperscript(true); + $objFont->setSuperscript(true); } if ($vertAlign == 'subscript') { - $objText->getFont()->setSubscript(true); + $objFont->setSubscript(true); } } } if (isset($run->rPr->u)) { $attr = $run->rPr->u->attributes(); if (!isset($attr['val'])) { - $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); + $objFont->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); } else { - $objText->getFont()->setUnderline((string) $attr['val']); + $objFont->setUnderline((string) $attr['val']); } } if (isset($run->rPr->strike)) { @@ -1788,7 +1794,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setStrikethrough(true); + $objFont->setStrikethrough(true); } } } @@ -1841,17 +1847,30 @@ class Xlsx extends BaseReader } } + /** + * @param null|array|bool|SimpleXMLElement $array + * @param int|string $key + * + * @return mixed + */ private static function getArrayItem($array, $key = 0) { - return $array[$key] ?? null; + return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); } + /** + * @param null|SimpleXMLElement|string $base + * @param null|SimpleXMLElement|string $add + */ private static function dirAdd($base, $add): string { + $base = (string) $base; + $add = (string) $add; + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } - private static function toCSSArray($style): array + private static function toCSSArray(string $style): array { $style = self::stripWhiteSpaceFromStyleString($style); @@ -1865,15 +1884,15 @@ class Xlsx extends BaseReader } if (strpos($item[1], 'pt') !== false) { $item[1] = str_replace('pt', '', $item[1]); - $item[1] = Font::fontSizeToPixels($item[1]); + $item[1] = (string) Font::fontSizeToPixels((int) $item[1]); } if (strpos($item[1], 'in') !== false) { $item[1] = str_replace('in', '', $item[1]); - $item[1] = Font::inchSizeToPixels($item[1]); + $item[1] = (string) Font::inchSizeToPixels((int) $item[1]); } if (strpos($item[1], 'cm') !== false) { $item[1] = str_replace('cm', '', $item[1]); - $item[1] = Font::centimeterSizeToPixels($item[1]); + $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]); } $style[$item[0]] = $item[1]; @@ -1882,11 +1901,14 @@ class Xlsx extends BaseReader return $style; } - public static function stripWhiteSpaceFromStyleString($string): string + public static function stripWhiteSpaceFromStyleString(string $string): string { return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); } + /** + * @param mixed $value + */ private static function boolean($value): bool { if (is_object($value)) { @@ -1955,7 +1977,7 @@ class Xlsx extends BaseReader return $returnValue; } - private function readFormControlProperties(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -1982,7 +2004,7 @@ class Xlsx extends BaseReader unset($unparsedCtrlProps); } - private function readPrinterSettings(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -2070,7 +2092,7 @@ class Xlsx extends BaseReader if ($xmlSheet && $xmlSheet->autoFilter) { // In older files, autofilter structure is defined in the worksheet file (new AutoFilter($docSheet, $xmlSheet))->load(); - } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) { + } elseif ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { // But for Office365, MS decided to make it all just a bit more complicated $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 374da9f6..623c6691 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class AutoFilter { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -28,7 +30,7 @@ class AutoFilter } } - private function readAutoFilter($autoFilterRange, $xmlSheet): void + private function readAutoFilter(string $autoFilterRange, SimpleXMLElement $xmlSheet): void { $autoFilter = $this->worksheet->getAutoFilter(); $autoFilter->setRange($autoFilterRange); @@ -39,15 +41,15 @@ class AutoFilter if ($filterColumn->filters) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); $filters = $filterColumn->filters; - if ((isset($filters['blank'])) && ($filters['blank'] == 1)) { + if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Standard filters are always an OR join, so no join rule needs to be set // Entries can be either filter elements foreach ($filters->filter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Or Date Group elements @@ -69,7 +71,7 @@ class AutoFilter foreach ($filters->dateGroupItem as $dateGroupItem) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', [ 'year' => (string) $dateGroupItem['year'], 'month' => (string) $dateGroupItem['month'], @@ -110,7 +112,7 @@ class AutoFilter foreach ($filterColumn->dynamicFilter as $filterRule) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', (string) $filterRule['val'], (string) $filterRule['type'] )->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php b/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php index 1679f01f..2f146458 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; class BaseParserClass { - protected static function boolean($value) + /** + * @param mixed $value + */ + protected static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php index 2a1e2afd..34705733 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -10,8 +10,10 @@ use SimpleXMLElement; class ColumnAndRowAttributes extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -120,7 +122,7 @@ class ColumnAndRowAttributes extends BaseParserClass } } - private function isFilteredColumn(IReadFilter $readFilter, $columnCoordinate, array $rowsAttributes) + private function isFilteredColumn(IReadFilter $readFilter, string $columnCoordinate, array $rowsAttributes): bool { foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -131,7 +133,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readColumnAttributes(SimpleXMLElement $worksheetCols, $readDataOnly) + private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array { $columnAttributes = []; @@ -151,7 +153,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function readColumnRangeAttributes(SimpleXMLElement $column, $readDataOnly) + private function readColumnRangeAttributes(SimpleXMLElement $column, bool $readDataOnly): array { $columnAttributes = []; @@ -172,7 +174,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function isFilteredRow(IReadFilter $readFilter, $rowCoordinate, array $columnsAttributes) + private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool { foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -183,7 +185,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readRowAttributes(SimpleXMLElement $worksheetRow, $readDataOnly) + private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly): array { $rowAttributes = []; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index c631a0fe..7d947bac 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -10,11 +10,14 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueO use PhpOffice\PhpSpreadsheet\Style\Style as Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; +use stdClass; class ConditionalStyles { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; /** @@ -22,6 +25,7 @@ class ConditionalStyles */ private $ns; + /** @var array */ private $dxfs; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs = []) @@ -146,7 +150,7 @@ class ConditionalStyles return $cfStyle; } - private function readConditionalStyles($xmlSheet): array + private function readConditionalStyles(SimpleXMLElement $xmlSheet): array { $conditionals = []; foreach ($xmlSheet->conditionalFormatting as $conditional) { @@ -162,7 +166,7 @@ class ConditionalStyles return $conditionals; } - private function setConditionalStyles(Worksheet $worksheet, array $conditionals, $xmlExtLst): void + private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void { foreach ($conditionals as $cellRangeReference => $cfRules) { ksort($cfRules); @@ -176,7 +180,7 @@ class ConditionalStyles } } - private function readStyleRules($cfRules, $extLst) + private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array { $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); $conditionalStyles = []; @@ -213,7 +217,7 @@ class ConditionalStyles if (isset($cfRule->dataBar)) { $objConditional->setDataBar( - $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) + $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) // @phpstan-ignore-line ); } else { $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); @@ -225,7 +229,10 @@ class ConditionalStyles return $conditionalStyles; } - private function readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions): ConditionalDataBar + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarOfConditionalRule($cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar { $dataBar = new ConditionalDataBar(); //dataBar attribute @@ -257,7 +264,10 @@ class ConditionalStyles return $dataBar; } - private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, $conditionalFormattingRuleExtensions): void + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, array $conditionalFormattingRuleExtensions): void { if (isset($cfRule->extLst)) { $ns = $cfRule->extLst->getNamespaces(true); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index b699cb57..dac76230 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class DataValidations { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -22,7 +24,7 @@ class DataValidations { foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { // Uppercase coordinate - $range = strtoupper($dataValidation['sqref']); + $range = strtoupper((string) $dataValidation['sqref']); $rangeSet = explode(' ', $range); foreach ($rangeSet as $range) { $stRange = $this->worksheet->shrinkRangeToFit($range); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php index 84884996..7d48c796 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class Hyperlinks { + /** @var Worksheet */ private $worksheet; + /** @var array */ private $hyperlinks = []; public function __construct(Worksheet $workSheet) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php index 56f18f98..08decd6e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class PageSetup extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -18,16 +20,17 @@ class PageSetup extends BaseParserClass $this->worksheetXml = $worksheetXml; } - public function load(array $unparsedLoadedData) + public function load(array $unparsedLoadedData): array { - if (!$this->worksheetXml) { + $worksheetXml = $this->worksheetXml; + if ($worksheetXml === null) { return $unparsedLoadedData; } - $this->margins($this->worksheetXml, $this->worksheet); - $unparsedLoadedData = $this->pageSetup($this->worksheetXml, $this->worksheet, $unparsedLoadedData); - $this->headerFooter($this->worksheetXml, $this->worksheet); - $this->pageBreaks($this->worksheetXml, $this->worksheet); + $this->margins($worksheetXml, $this->worksheet); + $unparsedLoadedData = $this->pageSetup($worksheetXml, $this->worksheet, $unparsedLoadedData); + $this->headerFooter($worksheetXml, $this->worksheet); + $this->pageBreaks($worksheetXml, $this->worksheet); return $unparsedLoadedData; } @@ -45,7 +48,7 @@ class PageSetup extends BaseParserClass } } - private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData) + private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array { if ($xmlSheet->pageSetup) { $docPageSetup = $worksheet->getPageSetup(); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php index a302cc56..9c02da9f 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -7,8 +7,10 @@ use SimpleXMLElement; class SheetViewOptions extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -24,10 +26,11 @@ class SheetViewOptions extends BaseParserClass } if (isset($this->worksheetXml->sheetPr)) { - $this->tabColor($this->worksheetXml->sheetPr, $styleReader); - $this->codeName($this->worksheetXml->sheetPr); - $this->outlines($this->worksheetXml->sheetPr); - $this->pageSetup($this->worksheetXml->sheetPr); + $sheetPr = $this->worksheetXml->sheetPr; + $this->tabColor($sheetPr, $styleReader); + $this->codeName($sheetPr); + $this->outlines($sheetPr); + $this->pageSetup($sheetPr); } if (isset($this->worksheetXml->sheetFormatPr)) { From adbda6391257041f4925c0f497bf0c17af0bfd67 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 4 Sep 2022 10:43:31 -0700 Subject: [PATCH 106/156] Phpstan and Xlsx Reader (#3043) Eliminate most Phpstan messages in Xlsx Reader. In combination with similar changes to Xlsx Writer, baseline will shrink to just over 3,000 lines. --- samples/Chart/33_Chart_create_scatter2.php | 6 +- samples/Chart/33_Chart_create_scatter3.php | 6 +- .../33_Chart_create_scatter5_trendlines.php | 18 +-- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 10 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 4 +- src/PhpSpreadsheet/Shared/Font.php | 2 +- src/PhpSpreadsheet/Shared/StringHelper.php | 16 ++- src/PhpSpreadsheet/Style/Font.php | 8 +- src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 3 - src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 3 - .../Calculation/FormulaParserTest.php | 2 +- .../Chart/AxisGlowTest.php | 6 +- .../Chart/AxisShadowTest.php | 6 +- .../Chart/GridlinesShadowGlowTest.php | 6 +- .../Chart/Issue2506Test.php | 2 +- .../Chart/MultiplierTest.php | 4 +- .../Chart/ShadowPresetsTest.php | 7 +- .../Reader/Xlsx/AutoFilter2Test.php | 13 +- .../Reader/Xlsx/RibbonTest.php | 10 +- tests/PhpSpreadsheetTests/RichTextTest.php | 4 +- .../SpreadsheetCoverageTest.php | 135 +++++++++--------- .../Worksheet/ColumnCellIteratorTest.php | 3 + .../Worksheet/RowCellIteratorTest.php | 3 + 23 files changed, 146 insertions(+), 131 deletions(-) diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index 59310620..cbc3ec1a 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -1,6 +1,6 @@ setScatterLines(false); // points not connected // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers -$xAxis = new Axis(); +$xAxis = new ChartAxis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); $xAxis->setAxisOption('textRotation', '45'); -$yAxis = new Axis(); +$yAxis = new ChartAxis(); $yAxis->setLineStyleProperties( 2.5, // width in points Properties::LINE_STYLE_COMPOUND_SIMPLE, diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php index f6f9a6f4..899fca79 100644 --- a/samples/Chart/33_Chart_create_scatter3.php +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -1,6 +1,6 @@ setScatterLines(false); // points not connected // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers -$xAxis = new Axis(); +$xAxis = new ChartAxis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); $xAxis->setAxisOption('textRotation', '45'); $xAxis->setAxisOption('hidden', '1'); -$yAxis = new Axis(); +$yAxis = new ChartAxis(); $yAxis->setLineStyleProperties( 2.5, // width in points Properties::LINE_STYLE_COMPOUND_SIMPLE, diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index 5beb82cd..dcee3e14 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -1,6 +1,6 @@ getMarkerFillColor() - ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0] ->getMarkerBorderColor() - ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); // line details - dashed, smooth line (Bezier) with arrows, 40% transparent $dataSeriesValues[0] @@ -105,24 +105,24 @@ $dataSeriesValues[1] // square marker border color ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[1] // square marker fill color ->getMarkerFillColor() - ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1] ->setScatterLines(true) ->setSmoothLine(false) - ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1]->setLineWidth(2.0); // series 3 - metric3, markers, no line $dataSeriesValues[2] // triangle? fill //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle ->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[2] // triangle border ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers -$xAxis = new Axis(); +$xAxis = new ChartAxis(); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); // Build the dataseries @@ -204,7 +204,7 @@ $dataSeriesValues[0]->setTrendLines($trendLines); $dataSeriesValues[0]->setScatterLines(false); // points not connected $dataSeriesValues[0]->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0]->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); @@ -218,7 +218,7 @@ $dataSeriesValues[0]->getTrendLines()[1]->setLineStyleProperties(1.25); $dataSeriesValues[0]->getTrendLines()[2]->getLineColor()->setColorProperties('accent2', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[0]->getTrendLines()[2]->setLineStyleProperties(1.5, null, null, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); -$xAxis = new Axis(); +$xAxis = new ChartAxis(); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // m/d/yyyy // Build the dataseries diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 7d29e9c4..cd166b23 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -324,13 +324,13 @@ class DataSeriesValues extends Properties if (is_array($this->fillColor)) { $array = []; foreach ($this->fillColor as $chartColor) { - $array[] = self::chartColorToString($chartColor); + $array[] = $this->chartColorToString($chartColor); } return $array; } - return self::chartColorToString($this->fillColor); + return $this->chartColorToString($this->fillColor); } /** @@ -348,13 +348,13 @@ class DataSeriesValues extends Properties if ($fillString instanceof ChartColor) { $this->fillColor[] = $fillString; } else { - $this->fillColor[] = self::stringToChartColor($fillString); + $this->fillColor[] = $this->stringToChartColor($fillString); } } } elseif ($color instanceof ChartColor) { $this->fillColor = $color; - } elseif (is_string($color)) { - $this->fillColor = self::stringToChartColor($color); + } else { + $this->fillColor = $this->stringToChartColor($color); } return $this; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 9eb30256..507c8f5b 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -311,7 +311,7 @@ class Chart break; case 'stockChart': $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); - $plotAttributes = $this->readChartAttributes($plotAreaLayout); + $plotAttributes = $this->readChartAttributes($chartDetail); break; } @@ -1068,7 +1068,7 @@ class Chart } /** - * @param null|Layout|SimpleXMLElement $chartDetail + * @param ?SimpleXMLElement $chartDetail */ private function readChartAttributes($chartDetail): array { diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 33796b4c..e90c679b 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -349,7 +349,7 @@ class Font // Special case if there are one or more newline characters ("\n") $cellText = $cellText ?? ''; - if (strpos($cellText, "\n") !== false) { + if (strpos(/** @scrutinizer ignore-type */ $cellText, "\n") !== false) { $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 0fe10e4d..16026c3c 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -337,9 +337,19 @@ class StringHelper mb_substitute_character(65533); // Unicode substitution character // Phpstan does not think this can return false. $returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); - mb_substitute_character($subst); + mb_substitute_character(/** @scrutinizer ignore-type */ $subst); - return $returnValue; + return self::returnString($returnValue); + } + + /** + * Strictly to satisfy Scrutinizer. + * + * @param mixed $value + */ + private static function returnString($value): string + { + return is_string($value) ? $value : ''; } /** @@ -433,7 +443,7 @@ class StringHelper } } - return mb_convert_encoding($textValue, $to, $from); + return self::returnString(mb_convert_encoding($textValue, $to, $from)); } /** diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 19d67563..3d7bc1bc 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -743,14 +743,14 @@ class Font extends Supervisor private function hashChartColor(?ChartColor $underlineColor): string { - if ($this->underlineColor === null) { + if ($underlineColor === null) { return ''; } return - $this->underlineColor->getValue() - . $this->underlineColor->getType() - . (string) $this->underlineColor->getAlpha(); + $underlineColor->getValue() + . $underlineColor->getType() + . (string) $underlineColor->getAlpha(); } /** diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index cd17cccf..690b0c54 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -33,9 +33,6 @@ class Dompdf extends Pdf { $fileHandle = parent::prepareForSave($filename); - // Default PDF paper size - $paperSize = 'LETTER'; // Letter (8.5 in. by 11 in.) - // Check for paper size and page orientation $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup(); $orientation = $this->getOrientation() ?? $setup->getOrientation(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 8dab9529..3b64bd8e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -233,8 +233,6 @@ class Chart extends WriterPart if ($plotArea === null) { return; } - $majorGridlines = ($yAxis === null) ? null : $yAxis->getMajorGridlines(); - $minorGridlines = ($yAxis === null) ? null : $yAxis->getMinorGridlines(); $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; @@ -1163,7 +1161,6 @@ class Chart extends WriterPart $intercept = $trendLine->getIntercept(); $name = $trendLine->getName(); $trendLineColor = $trendLine->getLineColor(); // ChartColor - $trendLineWidth = $trendLine->getLineStyleProperty('width'); $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' if ($name !== '') { diff --git a/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php index 4682c6b2..d401f9c9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php @@ -12,7 +12,7 @@ class FormulaParserTest extends TestCase { $this->expectException(CalcException::class); $this->expectExceptionMessage('Invalid parameter passed: formula'); - $result = new FormulaParser(null); + new FormulaParser(null); } public function testInvalidTokenId(): void diff --git a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php index 0ed4a1b4..b14d63fb 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -107,7 +107,7 @@ class AxisGlowTest extends AbstractFunctional $yAxis = $chart->getChartAxisY(); $xAxis = $chart->getChartAxisX(); $yGlowSize = 10.0; - $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, ChartColor::EXCEL_COLOR_TYPE_RGB); $expectedGlowColor = [ 'type' => 'srgbClr', 'value' => 'FFFF00', @@ -231,7 +231,7 @@ class AxisGlowTest extends AbstractFunctional ); $yAxis = $chart->getChartAxisX(); // deliberate $yGlowSize = 20.0; - $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, Properties::EXCEL_COLOR_TYPE_SCHEME); + $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $expectedGlowColor = [ 'type' => 'schemeClr', 'value' => 'accent1', diff --git a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php index d6f122ef..78deece4 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -113,7 +113,7 @@ class AxisShadowTest extends AbstractFunctional 'distance' => 3, 'rotWithShape' => 0, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -139,7 +139,7 @@ class AxisShadowTest extends AbstractFunctional 'ky' => null, ], 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'type' => ChartColor::EXCEL_COLOR_TYPE_RGB, 'value' => 'FF0000', 'alpha' => 20, ], diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php index e2c91eba..e1215441 100644 --- a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php @@ -4,12 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -98,7 +98,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional $majorGridlines = new GridLines(); $yAxis->setMajorGridlines($majorGridlines); $majorGlowSize = 10.0; - $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, ChartColor::EXCEL_COLOR_TYPE_RGB); $softEdgeSize = 2.5; $majorGridlines->setSoftEdges($softEdgeSize); $expectedGlowColor = [ @@ -122,7 +122,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional 'distance' => 3, 'rotWithShape' => 0, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php index a2a14c9d..e1c30770 100644 --- a/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php +++ b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php @@ -23,7 +23,7 @@ class Issue2506Test extends AbstractFunctional public function testDataSeriesValues(): void { $reader = new XlsxReader(); - self::readCharts($reader); + $this->readCharts($reader); $spreadsheet = $reader->load(self::DIRECTORY . 'issue.2506.xlsx'); $worksheet = $spreadsheet->getActiveSheet(); $charts = $worksheet->getChartCollection(); diff --git a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php index 35161ff7..db602e8a 100644 --- a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php +++ b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -109,7 +109,7 @@ class MultiplierTest extends TestCase 'ky' => null, ], 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'type' => ChartColor::EXCEL_COLOR_TYPE_RGB, 'value' => 'FF0000', 'alpha' => 20, ], diff --git a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php index e96d6c14..20cacbc1 100644 --- a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PHPUnit\Framework\TestCase; @@ -131,7 +132,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -160,7 +161,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -189,7 +190,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 6d6949d8..0bb9f130 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -11,12 +11,14 @@ class AutoFilter2Test extends TestCase { private const TESTBOOK = 'tests/data/Reader/XLSX/autofilter2.xlsx'; - public function getVisibleSheet(Worksheet $sheet, int $maxRow): array + public function getVisibleSheet(?Worksheet $sheet, int $maxRow): array { $actualVisible = []; - for ($row = 2; $row <= $maxRow; ++$row) { - if ($sheet->getRowDimension($row)->getVisible()) { - $actualVisible[] = $row; + if ($sheet !== null) { + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } } } @@ -35,13 +37,14 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; self::assertSame(Rule::AUTOFILTER_RULETYPE_DATEGROUP, $rule->getRuleType()); $value = $rule->getValue(); self::assertIsArray($value); - self::assertCount(6, $value); + self::assertCount(6, /** @scrutinizer ignore-type */ $value); self::assertSame('2002', $value['year']); self::assertSame('', $value['month']); self::assertSame('', $value['day']); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php index ab304e7b..68b3bb01 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -24,14 +24,14 @@ class RibbonTest extends AbstractFunctional self::assertSame('customUI/customUI.xml', $target); $data = $spreadsheet->getRibbonXMLData('data'); self::assertIsString($data); - self::assertSame(1522, strlen($data)); + self::assertSame(1522, strlen(/** @scrutinizer ignore-type */ $data)); $vbaCode = (string) $spreadsheet->getMacrosCode(); self::assertSame(13312, strlen($vbaCode)); self::assertNull($spreadsheet->getRibbonBinObjects()); - self::assertNull($spreadsheet->getRibbonBinObjects('names')); - self::assertNull($spreadsheet->getRibbonBinObjects('data')); + foreach (['names', 'data', 'xxxxx'] as $type) { + self::assertNull($spreadsheet->getRibbonBinObjects($type), "Expecting null when type is $type"); + } self::assertEmpty($spreadsheet->getRibbonBinObjects('types')); - self::assertNull($spreadsheet->getRibbonBinObjects('xxxxx')); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); $spreadsheet->disconnectWorksheets(); @@ -58,7 +58,7 @@ class RibbonTest extends AbstractFunctional self::assertSame('customUI/customUI.xml', $target); $data = $spreadsheet->getRibbonXMLData('data'); self::assertIsString($data); - self::assertSame(1522, strlen($data)); + self::assertSame(1522, strlen(/** @scrutinizer ignore-type */ $data)); $vbaCode = (string) $spreadsheet->getMacrosCode(); self::assertSame(13312, strlen($vbaCode)); $spreadsheet->discardMacros(); diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index e2dfcce7..49878529 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -28,7 +28,9 @@ class RichTextTest extends TestCase public function testTextElements(): void { $element1 = new TextElement('A'); - self::assertNull($element1->getFont()); + if ($element1->getFont() !== null) { + self::fail('Expected font to be null'); + } $element2 = new TextElement('B'); $element3 = new TextElement('C'); $richText = new RichText(); diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php index 584c53fe..a455a7d2 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -9,81 +9,93 @@ use PHPUnit\Framework\TestCase; class SpreadsheetCoverageTest extends TestCase { + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var ?Spreadsheet */ + private $spreadsheet2; + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->spreadsheet2 !== null) { + $this->spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2 = null; + } + } + public function testDocumentProperties(): void { - $spreadsheet = new Spreadsheet(); - $properties = $spreadsheet->getProperties(); + $this->spreadsheet = new Spreadsheet(); + $properties = $this->spreadsheet->getProperties(); $properties->setCreator('Anyone'); $properties->setTitle('Description'); - $spreadsheet2 = new Spreadsheet(); - self::assertNotEquals($properties, $spreadsheet2->getProperties()); + $this->spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $this->spreadsheet2->getProperties()); $properties2 = clone $properties; - $spreadsheet2->setProperties($properties2); - self::assertEquals($properties, $spreadsheet2->getProperties()); - $spreadsheet->disconnectWorksheets(); - $spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $this->spreadsheet2->getProperties()); } public function testDocumentSecurity(): void { - $spreadsheet = new Spreadsheet(); - $security = $spreadsheet->getSecurity(); + $this->spreadsheet = new Spreadsheet(); + $security = $this->spreadsheet->getSecurity(); $security->setLockRevision(true); $revisionsPassword = 'revpasswd'; $security->setRevisionsPassword($revisionsPassword); - $spreadsheet2 = new Spreadsheet(); - self::assertNotEquals($security, $spreadsheet2->getSecurity()); + $this->spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $this->spreadsheet2->getSecurity()); $security2 = clone $security; - $spreadsheet2->setSecurity($security2); - self::assertEquals($security, $spreadsheet2->getSecurity()); - $spreadsheet->disconnectWorksheets(); - $spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2->setSecurity($security2); + self::assertEquals($security, $this->spreadsheet2->getSecurity()); } public function testCellXfCollection(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $sheet->getStyle('A1')->getFont()->setName('font1'); $sheet->getStyle('A2')->getFont()->setName('font2'); $sheet->getStyle('A3')->getFont()->setName('font3'); $sheet->getStyle('B1')->getFont()->setName('font1'); $sheet->getStyle('B2')->getFont()->setName('font2'); - $collection = $spreadsheet->getCellXfCollection(); + $collection = $this->spreadsheet->getCellXfCollection(); self::assertCount(4, $collection); $font1Style = $collection[1]; - self::assertTrue($spreadsheet->cellXfExists($font1Style)); - self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertTrue($this->spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $this->spreadsheet->getCellXfCollection()[1]->getFont()->getName()); self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); - $spreadsheet->removeCellXfByIndex(1); - self::assertFalse($spreadsheet->cellXfExists($font1Style)); - self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + $this->spreadsheet->removeCellXfByIndex(1); + self::assertFalse($this->spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $this->spreadsheet->getCellXfCollection()[1]->getFont()->getName()); self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); - $spreadsheet->disconnectWorksheets(); } public function testInvalidRemoveCellXfByIndex(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('CellXf index is out of bounds.'); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $sheet->getStyle('A1')->getFont()->setName('font1'); $sheet->getStyle('A2')->getFont()->setName('font2'); $sheet->getStyle('A3')->getFont()->setName('font3'); $sheet->getStyle('B1')->getFont()->setName('font1'); $sheet->getStyle('B2')->getFont()->setName('font2'); - $spreadsheet->removeCellXfByIndex(5); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet->removeCellXfByIndex(5); } public function testInvalidRemoveDefaultStyle(): void @@ -91,71 +103,63 @@ class SpreadsheetCoverageTest extends TestCase $this->expectException(SSException::class); $this->expectExceptionMessage('No default style found for this workbook'); // Removing default style probably should be disallowed. - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $spreadsheet->removeCellXfByIndex(0); - $style = $spreadsheet->getDefaultStyle(); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->removeCellXfByIndex(0); + $this->spreadsheet->getDefaultStyle(); } public function testCellStyleXF(): void { - $spreadsheet = new Spreadsheet(); - $collection = $spreadsheet->getCellStyleXfCollection(); + $this->spreadsheet = new Spreadsheet(); + $collection = $this->spreadsheet->getCellStyleXfCollection(); self::assertCount(1, $collection); $styleXf = $collection[0]; - self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0)); + self::assertSame($styleXf, $this->spreadsheet->getCellStyleXfByIndex(0)); $hash = $styleXf->getHashCode(); - self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash)); - self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x')); - $spreadsheet->disconnectWorksheets(); + self::assertSame($styleXf, $this->spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($this->spreadsheet->getCellStyleXfByHashCode($hash . 'x')); } public function testInvalidRemoveCellStyleXfByIndex(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $spreadsheet->removeCellStyleXfByIndex(5); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->removeCellStyleXfByIndex(5); } public function testInvalidFirstSheetIndex(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('First sheet index must be a positive integer.'); - $spreadsheet = new Spreadsheet(); - $spreadsheet->setFirstSheetIndex(-1); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setFirstSheetIndex(-1); } public function testInvalidVisibility(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('Invalid visibility value.'); - $spreadsheet = new Spreadsheet(); - $spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); - self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility()); - $spreadsheet->setVisibility(null); - self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility()); - $spreadsheet->setVisibility('badvalue'); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $this->spreadsheet->getVisibility()); + $this->spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $this->spreadsheet->getVisibility()); + $this->spreadsheet->setVisibility('badvalue'); } public function testInvalidTabRatio(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); - $spreadsheet = new Spreadsheet(); - $spreadsheet->setTabRatio(2000); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setTabRatio(2000); } public function testCopy(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $sheet->getStyle('A1')->getFont()->setName('font1'); $sheet->getStyle('A2')->getFont()->setName('font2'); $sheet->getStyle('A3')->getFont()->setName('font3'); @@ -166,8 +170,8 @@ class SpreadsheetCoverageTest extends TestCase $sheet->getCell('A3')->setValue('this is a3'); $sheet->getCell('B1')->setValue('this is b1'); $sheet->getCell('B2')->setValue('this is b2'); - $copied = $spreadsheet->copy(); - $copysheet = $copied->getActiveSheet(); + $this->spreadsheet2 = $this->spreadsheet->copy(); + $copysheet = $this->spreadsheet2->getActiveSheet(); $copysheet->getStyle('A2')->getFont()->setName('font12'); $copysheet->getCell('A2')->setValue('this was a2'); @@ -192,18 +196,13 @@ class SpreadsheetCoverageTest extends TestCase self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); - - $spreadsheet->disconnectWorksheets(); - $copied->disconnectWorksheets(); } public function testClone(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); - $spreadsheet = new Spreadsheet(); - $clone = clone $spreadsheet; - $spreadsheet->disconnectWorksheets(); - $clone->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet2 = clone $this->spreadsheet; } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php index 1a7ff675..02b96426 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php @@ -40,6 +40,7 @@ class ColumnCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $ColumnCell) { self::assertNotNull($ColumnCell); + /** @scrutinizer ignore-call */ $values[] = $ColumnCell->getValue(); self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); @@ -60,6 +61,7 @@ class ColumnCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $ColumnCell) { self::assertNotNull($ColumnCell); + /** @scrutinizer ignore-call */ $values[] = $ColumnCell->getValue(); self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); @@ -81,6 +83,7 @@ class ColumnCellIteratorTest extends TestCase while ($iterator->valid()) { $current = $iterator->current(); self::assertNotNull($current); + /** @scrutinizer ignore-call */ $cell = $current->getCoordinate(); $values[] = $sheet->getCell($cell)->getValue(); $iterator->prev(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php index b0606804..9afda3cb 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php @@ -40,6 +40,7 @@ class RowCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $RowCell) { self::assertNotNull($RowCell); + /** @scrutinizer ignore-call */ $values[] = $RowCell->getValue(); self::assertEquals($RowCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $RowCell); @@ -59,6 +60,7 @@ class RowCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $RowCell) { self::assertNotNull($RowCell); + /** @scrutinizer ignore-call */ $values[] = $RowCell->getValue(); self::assertEquals($RowCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $RowCell); @@ -80,6 +82,7 @@ class RowCellIteratorTest extends TestCase while ($iterator->valid()) { $current = $iterator->current(); self::assertNotNull($current); + /** @scrutinizer ignore-call */ $cell = $current->getCoordinate(); $values[] = $sheet->getCell($cell)->getValue(); $iterator->prev(); From 3884aa74773d4b9b3275176c633cf9f8f3610245 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 3 Sep 2022 22:03:16 +0200 Subject: [PATCH 107/156] Update Excel function samples for Date/Time functions --- samples/Calculations/DateTime/DATE.php | 16 ++++-- samples/Calculations/DateTime/DATEDIF.php | 60 ++++++++++++++++++++ samples/Calculations/DateTime/DATEVALUE.php | 18 +++--- samples/Calculations/DateTime/DAY.php | 50 ++++++++++++++++ samples/Calculations/DateTime/DAYS.php | 52 +++++++++++++++++ samples/Calculations/DateTime/DAYS360.php | 57 +++++++++++++++++++ samples/Calculations/DateTime/EDATE.php | 43 ++++++++++++++ samples/Calculations/DateTime/EOMONTH.php | 43 ++++++++++++++ samples/Calculations/DateTime/HOUR.php | 48 ++++++++++++++++ samples/Calculations/DateTime/ISOWEEKNUM.php | 50 ++++++++++++++++ samples/Calculations/DateTime/MINUTE.php | 48 ++++++++++++++++ samples/Calculations/DateTime/MONTH.php | 50 ++++++++++++++++ samples/Calculations/DateTime/NOW.php | 27 +++++++++ samples/Calculations/DateTime/SECOND.php | 48 ++++++++++++++++ samples/Calculations/DateTime/TIME.php | 14 +++-- samples/Calculations/DateTime/TIMEVALUE.php | 8 ++- samples/Calculations/DateTime/TODAY.php | 27 +++++++++ samples/Calculations/DateTime/WEEKDAY.php | 58 +++++++++++++++++++ samples/Calculations/DateTime/WEEKNUM.php | 52 +++++++++++++++++ samples/Calculations/DateTime/YEAR.php | 50 ++++++++++++++++ src/PhpSpreadsheet/Helper/Sample.php | 4 +- 21 files changed, 801 insertions(+), 22 deletions(-) create mode 100644 samples/Calculations/DateTime/DATEDIF.php create mode 100644 samples/Calculations/DateTime/DAY.php create mode 100644 samples/Calculations/DateTime/DAYS.php create mode 100644 samples/Calculations/DateTime/DAYS360.php create mode 100644 samples/Calculations/DateTime/EDATE.php create mode 100644 samples/Calculations/DateTime/EOMONTH.php create mode 100644 samples/Calculations/DateTime/HOUR.php create mode 100644 samples/Calculations/DateTime/ISOWEEKNUM.php create mode 100644 samples/Calculations/DateTime/MINUTE.php create mode 100644 samples/Calculations/DateTime/MONTH.php create mode 100644 samples/Calculations/DateTime/NOW.php create mode 100644 samples/Calculations/DateTime/SECOND.php create mode 100644 samples/Calculations/DateTime/TODAY.php create mode 100644 samples/Calculations/DateTime/WEEKDAY.php create mode 100644 samples/Calculations/DateTime/WEEKNUM.php create mode 100644 samples/Calculations/DateTime/YEAR.php diff --git a/samples/Calculations/DateTime/DATE.php b/samples/Calculations/DateTime/DATE.php index 5d758f76..d526cbdb 100644 --- a/samples/Calculations/DateTime/DATE.php +++ b/samples/Calculations/DateTime/DATE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the serial number of a particular date.'); +$category = 'Date/Time'; +$functionName = 'DATE'; +$description = 'Returns the Excel serial number of a particular date'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -27,15 +31,15 @@ for ($row = 1; $row <= $testDateCount; ++$row) { } $worksheet->getStyle('E1:E' . $testDateCount) ->getNumberFormat() - ->setFormatCode('yyyy-mmm-dd'); + ->setFormatCode('yyyy-mm-dd'); // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Year: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); - $helper->log('Month: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Day: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Year: " . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(B{$row}) Month: " . $worksheet->getCell('B' . $row)->getFormattedValue()); + $helper->log("(C{$row}) Day: " . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('D' . $row)->getValue()); - $helper->log('Excel DateStamp: ' . $worksheet->getCell('D' . $row)->getFormattedValue()); + $helper->log('Excel DateStamp: ' . $worksheet->getCell('D' . $row)->getCalculatedValue()); $helper->log('Formatted DateStamp: ' . $worksheet->getCell('E' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/DATEDIF.php b/samples/Calculations/DateTime/DATEDIF.php new file mode 100644 index 00000000..7bc077c9 --- /dev/null +++ b/samples/Calculations/DateTime/DATEDIF.php @@ -0,0 +1,60 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=TODAY()'); + $worksheet->setCellValue('G' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "Y")'); + $worksheet->setCellValue('H' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "M")'); + $worksheet->setCellValue('I' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "D")'); + $worksheet->setCellValue('J' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "MD")'); + $worksheet->setCellValue('K' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "YM")'); + $worksheet->setCellValue('L' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "YD")'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log('In years ("Y"): ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); + $helper->log('In months ("M"): ' . $worksheet->getCell('H' . $row)->getCalculatedValue()); + $helper->log('In days ("D"): ' . $worksheet->getCell('I' . $row)->getCalculatedValue()); + $helper->log('In days ignoring months and years ("MD"): ' . $worksheet->getCell('J' . $row)->getCalculatedValue()); + $helper->log('In months ignoring days and years ("YM"): ' . $worksheet->getCell('K' . $row)->getCalculatedValue()); + $helper->log('In days ignoring years ("YD"): ' . $worksheet->getCell('L' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DATEVALUE.php b/samples/Calculations/DateTime/DATEVALUE.php index 5cdb936d..c506c6f2 100644 --- a/samples/Calculations/DateTime/DATEVALUE.php +++ b/samples/Calculations/DateTime/DATEVALUE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Converts a date in the form of text to a serial number.'); +$category = 'Date/Time'; +$functionName = 'DATEVALUE'; +$description = 'Converts a date in the form of text to an Excel serial number'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -13,8 +17,8 @@ $worksheet = $spreadsheet->getActiveSheet(); // Add some data $testDates = ['26 March 2012', '29 Feb 2012', 'April 1, 2012', '25/12/2012', '2012-Oct-31', '5th November', 'January 1st', 'April 2012', - '17-03', '03-2012', '29 Feb 2011', '03-05-07', - '03-MAY-07', '03-13-07', + '17-03', '03-17', '03-2012', '29 Feb 2011', '03-05-07', + '03-MAY-07', '03-13-07', '13-03-07', '03/13/07', '13/03/07', ]; $testDateCount = count($testDates); @@ -26,14 +30,14 @@ for ($row = 1; $row <= $testDateCount; ++$row) { $worksheet->getStyle('C1:C' . $testDateCount) ->getNumberFormat() - ->setFormatCode('yyyy-mmm-dd'); + ->setFormatCode('yyyy-mm-dd'); // Test the formulae $helper->log('Warning: The PhpSpreadsheet DATEVALUE() function accepts a wider range of date formats than MS Excel DATEFORMAT() function.'); for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Date String: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Date String: " . $worksheet->getCell('A' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('B' . $row)->getValue()); - $helper->log('Excel DateStamp: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Formatted DateStamp' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log('Excel DateStamp: ' . $worksheet->getCell('B' . $row)->getCalculatedValue()); + $helper->log('Formatted DateStamp: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/DAY.php b/samples/Calculations/DateTime/DAY.php new file mode 100644 index 00000000..3a4da7e6 --- /dev/null +++ b/samples/Calculations/DateTime/DAY.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DAY(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Day is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DAYS.php b/samples/Calculations/DateTime/DAYS.php new file mode 100644 index 00000000..ddd47f37 --- /dev/null +++ b/samples/Calculations/DateTime/DAYS.php @@ -0,0 +1,52 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=TODAY()'); + $worksheet->setCellValue('G' . $row, '=DAYS(D' . $row . ', F' . $row . ')'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log('Days: ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DAYS360.php b/samples/Calculations/DateTime/DAYS360.php new file mode 100644 index 00000000..b0e2fdbb --- /dev/null +++ b/samples/Calculations/DateTime/DAYS360.php @@ -0,0 +1,57 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DATE(2022,12,31)'); + $worksheet->setCellValue('G' . $row, '=DAYS360(D' . $row . ', F' . $row . ', FALSE)'); + $worksheet->setCellValue('H' . $row, '=DAYS360(D' . $row . ', F' . $row . ', TRUE)'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log(sprintf( + 'Days: %d (US) %d (European)', + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/EDATE.php b/samples/Calculations/DateTime/EDATE.php new file mode 100644 index 00000000..be6e4d19 --- /dev/null +++ b/samples/Calculations/DateTime/EDATE.php @@ -0,0 +1,43 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$months = range(-12, 12); +$testDateCount = count($months); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=DATE(2020,12,31)'); + $worksheet->setCellValue('B' . $row, '=A' . $row); + $worksheet->setCellValue('C' . $row, $months[$row - 1]); + $worksheet->setCellValue('D' . $row, '=EDATE(B' . $row . ', C' . $row . ')'); +} +$worksheet->getStyle('B1:B' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('D1:D' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + '%s and %d months is %d (%s)', + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getFormattedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/EOMONTH.php b/samples/Calculations/DateTime/EOMONTH.php new file mode 100644 index 00000000..e0b7568a --- /dev/null +++ b/samples/Calculations/DateTime/EOMONTH.php @@ -0,0 +1,43 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$months = range(-12, 12); +$testDateCount = count($months); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=DATE(2020,1,1)'); + $worksheet->setCellValue('B' . $row, '=A' . $row); + $worksheet->setCellValue('C' . $row, $months[$row - 1]); + $worksheet->setCellValue('D' . $row, '=EOMONTH(B' . $row . ', C' . $row . ')'); +} +$worksheet->getStyle('B1:B' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('D1:D' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + '%s and %d months is %d (%s)', + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getFormattedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/HOUR.php b/samples/Calculations/DateTime/HOUR.php new file mode 100644 index 00000000..53c21644 --- /dev/null +++ b/samples/Calculations/DateTime/HOUR.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=HOUR(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Hour is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/ISOWEEKNUM.php b/samples/Calculations/DateTime/ISOWEEKNUM.php new file mode 100644 index 00000000..4a989fbd --- /dev/null +++ b/samples/Calculations/DateTime/ISOWEEKNUM.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=ISOWEEKNUM(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('ISO Week number is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/MINUTE.php b/samples/Calculations/DateTime/MINUTE.php new file mode 100644 index 00000000..11e90e13 --- /dev/null +++ b/samples/Calculations/DateTime/MINUTE.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=MINUTE(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Minute is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/MONTH.php b/samples/Calculations/DateTime/MONTH.php new file mode 100644 index 00000000..8cceaf4c --- /dev/null +++ b/samples/Calculations/DateTime/MONTH.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=MONTH(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Month is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/NOW.php b/samples/Calculations/DateTime/NOW.php new file mode 100644 index 00000000..858a3162 --- /dev/null +++ b/samples/Calculations/DateTime/NOW.php @@ -0,0 +1,27 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->setCellValue('A1', '=NOW()'); +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd hh:mm:ss'); + +// Test the formulae +$helper->log(sprintf( + 'Today is %f (%s)', + $worksheet->getCell('A1')->getCalculatedValue(), + $worksheet->getCell('A1')->getFormattedValue() +)); diff --git a/samples/Calculations/DateTime/SECOND.php b/samples/Calculations/DateTime/SECOND.php new file mode 100644 index 00000000..33806fd5 --- /dev/null +++ b/samples/Calculations/DateTime/SECOND.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=SECOND(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Second is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/TIME.php b/samples/Calculations/DateTime/TIME.php index 3d4208ad..42c45488 100644 --- a/samples/Calculations/DateTime/TIME.php +++ b/samples/Calculations/DateTime/TIME.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the serial number of a particular time.'); +$category = 'Date/Time'; +$functionName = 'TIME'; +$description = 'Returns the Excel serial number of a particular time'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -29,11 +33,11 @@ $worksheet->getStyle('E1:E' . $testDateCount) // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Hour: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); - $helper->log('Minute: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Second: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Hour: " . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(B{$row}) Minute: " . $worksheet->getCell('B' . $row)->getFormattedValue()); + $helper->log("(C{$row}) Second: " . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('D' . $row)->getValue()); - $helper->log('Excel TimeStamp: ' . $worksheet->getCell('D' . $row)->getFormattedValue()); + $helper->log('Excel TimeStamp: ' . $worksheet->getCell('D' . $row)->getCalculatedValue()); $helper->log('Formatted TimeStamp: ' . $worksheet->getCell('E' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/TIMEVALUE.php b/samples/Calculations/DateTime/TIMEVALUE.php index f75393cd..15ea8cd9 100644 --- a/samples/Calculations/DateTime/TIMEVALUE.php +++ b/samples/Calculations/DateTime/TIMEVALUE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Converts a time in the form of text to a serial number.'); +$category = 'Date/Time'; +$functionName = 'DATEVALUE'; +$description = 'Converts a time in the form of text to an Excel serial number'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -27,7 +31,7 @@ $worksheet->getStyle('C1:C' . $testDateCount) // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Time String: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Time String: " . $worksheet->getCell('A' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('B' . $row)->getValue()); $helper->log('Excel TimeStamp: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); $helper->log('Formatted TimeStamp: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); diff --git a/samples/Calculations/DateTime/TODAY.php b/samples/Calculations/DateTime/TODAY.php new file mode 100644 index 00000000..031149d5 --- /dev/null +++ b/samples/Calculations/DateTime/TODAY.php @@ -0,0 +1,27 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->setCellValue('A1', '=TODAY()'); +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log(sprintf( + 'Today is %d (%s)', + $worksheet->getCell('A1')->getCalculatedValue(), + $worksheet->getCell('A1')->getFormattedValue() +)); diff --git a/samples/Calculations/DateTime/WEEKDAY.php b/samples/Calculations/DateTime/WEEKDAY.php new file mode 100644 index 00000000..7d4b4288 --- /dev/null +++ b/samples/Calculations/DateTime/WEEKDAY.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=WEEKDAY(D' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=WEEKDAY(D' . $row . ', 2)'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log(sprintf( + 'Weekday is: %d (1-7 = Sun-Sat)', + $worksheet->getCell('F' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Weekday is: %d (1-7 = Mon-Sun)', + $worksheet->getCell('G' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/WEEKNUM.php b/samples/Calculations/DateTime/WEEKNUM.php new file mode 100644 index 00000000..1d4c4a12 --- /dev/null +++ b/samples/Calculations/DateTime/WEEKNUM.php @@ -0,0 +1,52 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=WEEKNUM(D' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=WEEKNUM(D' . $row . ', 21)'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('System 1 Week number is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); + $helper->log('System 2 (ISO-8601) Week number is: ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/YEAR.php b/samples/Calculations/DateTime/YEAR.php new file mode 100644 index 00000000..f7bfa6ea --- /dev/null +++ b/samples/Calculations/DateTime/YEAR.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=YEAR(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Year is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index aeb2bea6..9f7563d6 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -187,8 +187,8 @@ class Sample { $this->log(sprintf('%s Functions:', $category)); $description === null - ? $this->log(sprintf('%s()', rtrim($functionName, '()'))) - : $this->log(sprintf('%s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + ? $this->log(sprintf('Function: %s()', rtrim($functionName, '()'))) + : $this->log(sprintf('Function: %s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); } public function displayGrid(array $matrix): void From 7e3807309de6794fb277ceb8ef9af8bb6969e775 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 9 Sep 2022 07:34:36 -0700 Subject: [PATCH 108/156] Reconcile Differences between Css and Excel For Cell Alignment (#3048) This PR expands on PR #2195 from @nkjackzhang. That PR has been stalled for some time awaiting requested fixes. Those fixes are part of this PR, and additional tests and samples are added. The original request was to handle `vertical-align:middle` in Css (Excel uses `center`). This PR does its best to also handle vertical alignment Excel values not found in Css - `justify` (as `middle`) and `distributed` (as `middle`). It likewises handles valid Css values not found in Excel (`baseline`, `sub`, and `text-bottom` as `bottom`; `super` and `text-top` as `top`; `middle` as `center`). It also handles horizontal alignment Excel values not found in Css - `center-continuous` as `center` and `distributed` as `justify`; I couldn't think of a reasonable equivalent for `fill`, so it is ignored. The values assigned for vertical and horizontal alignment are now lower-cased (special handling required for `centerContinuous`). --- samples/Basic/49_alignment.php | 80 +++++++++++++++++ src/PhpSpreadsheet/Style/Alignment.php | 69 +++++++++++++-- src/PhpSpreadsheet/Writer/Html.php | 24 ++--- src/PhpSpreadsheet/Writer/Xlsx/Style.php | 21 +++-- .../Style/AlignmentMiddleTest.php | 87 +++++++++++++++++++ .../Style/AlignmentTest.php | 31 ++++--- 6 files changed, 273 insertions(+), 39 deletions(-) create mode 100644 samples/Basic/49_alignment.php create mode 100644 tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php diff --git a/samples/Basic/49_alignment.php b/samples/Basic/49_alignment.php new file mode 100644 index 00000000..83fdb3d4 --- /dev/null +++ b/samples/Basic/49_alignment.php @@ -0,0 +1,80 @@ +log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); +$spreadsheet->getProperties()->setTitle('Alignment'); +$sheet = $spreadsheet->getActiveSheet(); +$hi = 'Hi There'; +$ju = 'This is a longer than normal sentence'; +$sheet->fromArray([ + ['', 'default', 'bottom', 'top', 'center', 'justify', 'distributed'], + ['default', $hi, $hi, $hi, $hi, $hi, $hi], + ['left', $hi, $hi, $hi, $hi, $hi, $hi], + ['right', $hi, $hi, $hi, $hi, $hi, $hi], + ['center', $hi, $hi, $hi, $hi, $hi, $hi], + ['justify', $ju, $ju, $ju, $ju, $ju, $ju], + ['distributed', $ju, $ju, $ju, $ju, $ju, $ju], +]); +$sheet->getColumnDimension('B')->setWidth(20); +$sheet->getColumnDimension('C')->setWidth(20); +$sheet->getColumnDimension('D')->setWidth(20); +$sheet->getColumnDimension('E')->setWidth(20); +$sheet->getColumnDimension('F')->setWidth(20); +$sheet->getColumnDimension('G')->setWidth(20); +$sheet->getRowDimension(2)->setRowHeight(30); +$sheet->getRowDimension(3)->setRowHeight(30); +$sheet->getRowDimension(4)->setRowHeight(30); +$sheet->getRowDimension(5)->setRowHeight(30); +$sheet->getRowDimension(6)->setRowHeight(40); +$sheet->getRowDimension(7)->setRowHeight(40); +$minRow = 2; +$maxRow = 7; +$minCol = 'B'; +$maxCol = 'g'; +$sheet->getStyle("C$minRow:C$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_BOTTOM); +$sheet->getStyle("D$minRow:D$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_TOP); +$sheet->getStyle("E$minRow:E$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); +$sheet->getStyle("F$minRow:F$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_JUSTIFY); +$sheet->getStyle("G$minRow:G$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_DISTRIBUTED); +$sheet->getStyle("{$minCol}3:{$maxCol}3") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_LEFT); +$sheet->getStyle("{$minCol}4:{$maxCol}4") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_RIGHT); +$sheet->getStyle("{$minCol}5:{$maxCol}5") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER); +$sheet->getStyle("{$minCol}6:{$maxCol}6") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_JUSTIFY); +$sheet->getStyle("{$minCol}7:{$maxCol}7") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_DISTRIBUTED); + +$sheet->getCell('A9')->setValue('Center Continuous A9-C9'); +$sheet->getStyle('A9:C9') + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER_CONTINUOUS); +$sheet->getCell('A10')->setValue('Fill'); +$sheet->getStyle('A10') + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_FILL); +$sheet->setSelectedCells('A1'); + +$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Html', 'Xls']); diff --git a/src/PhpSpreadsheet/Style/Alignment.php b/src/PhpSpreadsheet/Style/Alignment.php index 83ac5b0d..68edfaca 100644 --- a/src/PhpSpreadsheet/Style/Alignment.php +++ b/src/PhpSpreadsheet/Style/Alignment.php @@ -15,6 +15,27 @@ class Alignment extends Supervisor const HORIZONTAL_JUSTIFY = 'justify'; const HORIZONTAL_FILL = 'fill'; const HORIZONTAL_DISTRIBUTED = 'distributed'; // Excel2007 only + private const HORIZONTAL_CENTER_CONTINUOUS_LC = 'centercontinuous'; + // Mapping for horizontal alignment + const HORIZONTAL_ALIGNMENT_FOR_XLSX = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER_CONTINUOUS, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_DISTRIBUTED, + ]; + // Mapping for horizontal alignment CSS + const HORIZONTAL_ALIGNMENT_FOR_HTML = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + //self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, // no reasonable equivalent for fill + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_JUSTIFY, + ]; // Vertical alignment styles const VERTICAL_BOTTOM = 'bottom'; @@ -22,6 +43,45 @@ class Alignment extends Supervisor const VERTICAL_CENTER = 'center'; const VERTICAL_JUSTIFY = 'justify'; const VERTICAL_DISTRIBUTED = 'distributed'; // Excel2007 only + // Vertical alignment CSS + private const VERTICAL_BASELINE = 'baseline'; + private const VERTICAL_MIDDLE = 'middle'; + private const VERTICAL_SUB = 'sub'; + private const VERTICAL_SUPER = 'super'; + private const VERTICAL_TEXT_BOTTOM = 'text-bottom'; + private const VERTICAL_TEXT_TOP = 'text-top'; + + // Mapping for vertical alignment + const VERTICAL_ALIGNMENT_FOR_XLSX = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_CENTER, + self::VERTICAL_JUSTIFY => self::VERTICAL_JUSTIFY, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_DISTRIBUTED, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BOTTOM, + self::VERTICAL_MIDDLE => self::VERTICAL_CENTER, + self::VERTICAL_SUB => self::VERTICAL_BOTTOM, + self::VERTICAL_SUPER => self::VERTICAL_TOP, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TOP, + ]; + + // Mapping for vertical alignment for Html + const VERTICAL_ALIGNMENT_FOR_HTML = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_MIDDLE, + self::VERTICAL_JUSTIFY => self::VERTICAL_MIDDLE, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_MIDDLE, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BASELINE, + self::VERTICAL_MIDDLE => self::VERTICAL_MIDDLE, + self::VERTICAL_SUB => self::VERTICAL_SUB, + self::VERTICAL_SUPER => self::VERTICAL_SUPER, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_TEXT_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TEXT_TOP, + ]; // Read order const READORDER_CONTEXT = 0; @@ -202,8 +262,9 @@ class Alignment extends Supervisor */ public function setHorizontal(string $horizontalAlignment) { - if ($horizontalAlignment == '') { - $horizontalAlignment = self::HORIZONTAL_GENERAL; + $horizontalAlignment = strtolower($horizontalAlignment); + if ($horizontalAlignment === self::HORIZONTAL_CENTER_CONTINUOUS_LC) { + $horizontalAlignment = self::HORIZONTAL_CENTER_CONTINUOUS; } if ($this->isSupervisor) { @@ -239,9 +300,7 @@ class Alignment extends Supervisor */ public function setVertical($verticalAlignment) { - if ($verticalAlignment == '') { - $verticalAlignment = self::VERTICAL_BOTTOM; - } + $verticalAlignment = strtolower($verticalAlignment); if ($this->isSupervisor) { $styleArray = $this->getStyleArray(['vertical' => $verticalAlignment]); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 6a400673..c115cabb 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -230,13 +230,6 @@ class Html extends BaseWriter $this->editHtmlCallback = $callback; } - const VALIGN_ARR = [ - Alignment::VERTICAL_BOTTOM => 'bottom', - Alignment::VERTICAL_TOP => 'top', - Alignment::VERTICAL_CENTER => 'middle', - Alignment::VERTICAL_JUSTIFY => 'middle', - ]; - /** * Map VAlign. * @@ -246,17 +239,9 @@ class Html extends BaseWriter */ private function mapVAlign($vAlign) { - return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline'; + return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? ''; } - const HALIGN_ARR = [ - Alignment::HORIZONTAL_LEFT => 'left', - Alignment::HORIZONTAL_RIGHT => 'right', - Alignment::HORIZONTAL_CENTER => 'center', - Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center', - Alignment::HORIZONTAL_JUSTIFY => 'justify', - ]; - /** * Map HAlign. * @@ -266,7 +251,7 @@ class Html extends BaseWriter */ private function mapHAlign($hAlign) { - return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : ''; + return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? ''; } const BORDER_ARR = [ @@ -988,7 +973,10 @@ class Html extends BaseWriter $css = []; // Create CSS - $css['vertical-align'] = $this->mapVAlign($alignment->getVertical() ?? ''); + $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? ''); + if ($verticalAlign) { + $css['vertical-align'] = $verticalAlign; + } $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? ''); if ($textAlign) { $css['text-align'] = $textAlign; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php index b24b7533..0442c25d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Conditional; @@ -403,8 +404,14 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - $objWriter->writeAttribute('horizontal', (string) $style->getAlignment()->getHorizontal()); - $objWriter->writeAttribute('vertical', (string) $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal !== '') { + $objWriter->writeAttribute('horizontal', $horizontal); + } + if ($vertical !== '') { + $objWriter->writeAttribute('vertical', $vertical); + } $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { @@ -459,11 +466,13 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - if ($style->getAlignment()->getHorizontal() !== null) { - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal) { + $objWriter->writeAttribute('horizontal', $horizontal); } - if ($style->getAlignment()->getVertical() !== null) { - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + if ($vertical) { + $objWriter->writeAttribute('vertical', $vertical); } if ($style->getAlignment()->getTextRotation() !== null) { diff --git a/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php b/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php new file mode 100644 index 00000000..1d260a6e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php @@ -0,0 +1,87 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + public function testCenterWriteHtml(): void + { + // Html Writer changes vertical align center to middle + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + $writer = new HTML($this->spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('vertical-align:middle', $html); + self::assertStringNotContainsString('vertical-align:center', $html); + } + + public function testCenterWriteXlsx(): void + { + // Xlsx Writer uses vertical align center unchanged + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + $this->outputFileName = File::temporaryFilename(); + $writer = new Xlsx($this->spreadsheet); + $writer->save($this->outputFileName); + $zip = new ZipArchive(); + $zip->open($this->outputFileName); + $html = $zip->getFromName('xl/styles.xml'); + $zip->close(); + self::assertStringContainsString('vertical="center"', $html); + self::assertStringNotContainsString('vertical="middle"', $html); + } + + public function testCenterWriteXlsx2(): void + { + // Xlsx Writer changes vertical align middle to center + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical('middle'); + $this->outputFileName = File::temporaryFilename(); + $writer = new Xlsx($this->spreadsheet); + $writer->save($this->outputFileName); + $zip = new ZipArchive(); + $zip->open($this->outputFileName); + $html = $zip->getFromName('xl/styles.xml'); + $zip->close(); + self::assertStringContainsString('vertical="center"', $html); + self::assertStringNotContainsString('vertical="middle"', $html); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/AlignmentTest.php b/tests/PhpSpreadsheetTests/Style/AlignmentTest.php index d10e3211..6f50ce22 100644 --- a/tests/PhpSpreadsheetTests/Style/AlignmentTest.php +++ b/tests/PhpSpreadsheetTests/Style/AlignmentTest.php @@ -9,10 +9,21 @@ use PHPUnit\Framework\TestCase; class AlignmentTest extends TestCase { + /** @var ?Spreadsheet */ + private $spreadsheet; + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + public function testAlignment(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(45); @@ -31,8 +42,8 @@ class AlignmentTest extends TestCase public function testRotationTooHigh(): void { $this->expectException(PhpSpreadsheetException::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(91); @@ -42,8 +53,8 @@ class AlignmentTest extends TestCase public function testRotationTooLow(): void { $this->expectException(PhpSpreadsheetException::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(-91); @@ -52,8 +63,8 @@ class AlignmentTest extends TestCase public function testHorizontal(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('X'); $cell1->getStyle()->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT)->setIndent(1); @@ -74,8 +85,8 @@ class AlignmentTest extends TestCase public function testReadOrder(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('ABC'); $cell1->getStyle()->getAlignment()->setReadOrder(0); From b5f70de61d3ebe5ee09220e9c7f47d8a60f976a4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 9 Sep 2022 07:56:11 -0700 Subject: [PATCH 109/156] More Scrutinizer Catch Up (#3050) * More Scrutinizer Catch Up Continue the work of PR #3043 by attending to the 12 remaining 'new' issues. * Php 8.1 Problem One new null-instead-of-string problem. --- phpstan-baseline.neon | 15 ------- .../Chart/33_Chart_create_line_dateaxis.php | 22 +++++----- src/PhpSpreadsheet/Chart/Axis.php | 2 +- src/PhpSpreadsheet/Chart/Chart.php | 6 ++- src/PhpSpreadsheet/Reader/Xlsx.php | 13 +++--- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 12 +++--- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 40 +++++++++---------- src/PhpSpreadsheet/Worksheet/PageSetup.php | 9 +++-- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- src/PhpSpreadsheet/Writer/Html.php | 29 +++++++------- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 4 +- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 8 ++-- .../Reader/Xlsx/AutoFilter2Test.php | 1 + tests/PhpSpreadsheetTests/RichTextTest.php | 3 -- 14 files changed, 78 insertions(+), 88 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cc2eaa3d..3e2ccfc1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2590,21 +2590,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - message: "#^Parameter \\#1 \\$value of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\PageSetup\\:\\:setFirstPageNumber\\(\\) expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\PageSetup\\:\\:\\$pageOrder has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Strict comparison using \\=\\=\\= between int\\ and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\SheetView\\:\\:\\$sheetViewTypes has no type specified\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php index 1a47e5aa..413866e2 100644 --- a/samples/Chart/33_Chart_create_line_dateaxis.php +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -101,10 +101,10 @@ $dataSeriesValues = [ // marker details $dataSeriesValues[0] ->getMarkerFillColor() - ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0] ->getMarkerBorderColor() - ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); // line details - dashed, smooth line (Bezier) with arrows, 40% transparent $dataSeriesValues[0] @@ -129,18 +129,18 @@ $dataSeriesValues[1] // square marker border color ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[1] // square marker fill color ->getMarkerFillColor() - ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1] ->setScatterLines(true) ->setSmoothLine(false) - ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1]->setLineWidth(2.0); // series 3 - metric3, markers, no line $dataSeriesValues[2] // triangle? fill //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle ->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[2] // triangle border ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); @@ -239,7 +239,7 @@ $dataSeriesValues[0] ->setScatterlines(false); // disable connecting lines $dataSeriesValues[0] ->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0] ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); @@ -326,7 +326,7 @@ $chart = new Chart( // Set the position of the chart in the chart sheet below the first chart $chart->setTopLeftPosition('A13'); $chart->setBottomRightPosition('P25'); -$chart->setRoundedCorners('true'); // Rounded corners in Chart Outline +$chart->setRoundedCorners(true); // Rounded corners in Chart Outline // Add the chart to the worksheet $chartSheet $chartSheet->addChart($chart); @@ -350,8 +350,8 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj // get date of first day of the quarter of the start date - $startMonth = $startDate->format('n'); // suppress leading zero - $startYr = $startDate->format('Y'); + $startMonth = (int) $startDate->format('n'); // suppress leading zero + $startYr = (int) $startDate->format('Y'); $qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0); $qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3)); $qtrStartStr = "$startYr-$qtrStartMonth-01"; @@ -360,8 +360,8 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array // end the xaxis at the end of the quarter of the last date $lastDateStr = $dataSheet->getCellByColumnAndRow(2, $nrows + 1)->getValue(); $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); - $lastMonth = $lastDate->format('n'); - $lastYr = $lastDate->format('Y'); + $lastMonth = (int) $lastDate->format('n'); + $lastYr = (int) $lastDate->format('Y'); $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); $qtrEndMonth = 3 + (($qtr - 1) * 3); $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 3ac5c3cd..9ebb081f 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -219,7 +219,7 @@ class Axis extends Properties * @param ?int $alpha * @param ?string $AlphaType */ - public function setFillParameters($color, $alpha = null, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function setFillParameters($color, $alpha = null, $AlphaType = ChartColor::EXCEL_COLOR_TYPE_RGB): void { $this->fillColor->setColorProperties($color, $alpha, $AlphaType); } diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 036338c6..f41d7883 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -774,9 +774,11 @@ class Chart return $this->roundedCorners; } - public function setRoundedCorners(bool $roundedCorners): self + public function setRoundedCorners(?bool $roundedCorners): self { - $this->roundedCorners = $roundedCorners; + if ($roundedCorners !== null) { + $this->roundedCorners = $roundedCorners; + } return $this; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 244baddd..fc38375a 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -306,22 +306,25 @@ class Xlsx extends BaseReader return (bool) $c->v; } - private static function castToError(SimpleXMLElement $c): ?string + private static function castToError(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } - private static function castToString(SimpleXMLElement $c): ?string + private static function castToString(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } /** * @param mixed $value * @param mixed $calculatedValue */ - private function castToFormula(SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void + private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void { + if ($c === null) { + return; + } $attr = $c->f->attributes(); $cellDataType = 'f'; $value = "={$c->f}"; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 623c6691..39328adb 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -85,9 +85,9 @@ class AutoFilter } } - private function readCustomAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->customFilters) { + if (isset($filterColumn, $filterColumn->customFilters)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); $customFilters = $filterColumn->customFilters; // Custom filters can an AND or an OR join; @@ -104,9 +104,9 @@ class AutoFilter } } - private function readDynamicAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->dynamicFilter) { + if (isset($filterColumn, $filterColumn->dynamicFilter)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); // We should only ever have one dynamic filter foreach ($filterColumn->dynamicFilter as $filterRule) { @@ -126,9 +126,9 @@ class AutoFilter } } - private function readTopTenAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->top10) { + if (isset($filterColumn, $filterColumn->top10)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); // We should only ever have one top10 filter foreach ($filterColumn->top10 as $filterRule) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 507c8f5b..6bc6d7c5 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -11,7 +11,7 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; +use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -113,6 +113,7 @@ class Chart $plotSeries = $plotAttributes = []; $catAxRead = false; $plotNoFill = false; + /** @var SimpleXMLElement $chartDetail */ foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { case 'spPr': @@ -121,17 +122,18 @@ class Chart $plotNoFill = true; } if (isset($possibleNoFill->gradFill->gsLst)) { + /** @var SimpleXMLElement $gradient */ foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { /** @var float */ $pos = self::getAttribute($gradient, 'pos', 'float'); $gradientArray[] = [ - $pos / Properties::PERCENTAGE_MULTIPLIER, + $pos / ChartProperties::PERCENTAGE_MULTIPLIER, new ChartColor($this->readColor($gradient)), ]; } } if (isset($possibleNoFill->gradFill->lin)) { - $gradientLin = Properties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + $gradientLin = ChartProperties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); } break; @@ -464,12 +466,13 @@ class Chart $pointSize = null; $noFill = false; $bubble3D = false; - $dPtColors = []; + $dptColors = []; $markerFillColor = null; $markerBorderColor = null; $lineStyle = null; $labelLayout = null; $trendLines = []; + /** @var SimpleXMLElement $seriesDetail */ foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -487,7 +490,6 @@ class Chart break; case 'spPr': $children = $seriesDetail->children($this->aNamespace); - $ln = $children->ln; if (isset($children->ln)) { $ln = $children->ln; if (is_countable($ln->noFill) && count($ln->noFill) === 1) { @@ -1161,7 +1163,7 @@ class Chart } } - private function readEffects(SimpleXMLElement $chartDetail, ?Properties $chartObject): void + private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; @@ -1169,7 +1171,7 @@ class Chart $sppr = $chartDetail->spPr->children($this->aNamespace); if (isset($sppr->effectLst->glow)) { - $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / Properties::POINTS_WIDTH_MULTIPLIER; + $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / ChartProperties::POINTS_WIDTH_MULTIPLIER; if ($axisGlowSize != 0.0) { $colorArray = $this->readColor($sppr->effectLst->glow); $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']); @@ -1180,7 +1182,7 @@ class Chart /** @var string */ $softEdgeSize = self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string'); if (is_numeric($softEdgeSize)) { - $chartObject->setSoftEdges((float) Properties::xmlToPoints($softEdgeSize)); + $chartObject->setSoftEdges((float) ChartProperties::xmlToPoints($softEdgeSize)); } } @@ -1195,20 +1197,20 @@ class Chart if ($type !== '') { /** @var string */ $blur = self::getAttribute($sppr->effectLst->$type, 'blurRad', 'string'); - $blur = is_numeric($blur) ? Properties::xmlToPoints($blur) : null; + $blur = is_numeric($blur) ? ChartProperties::xmlToPoints($blur) : null; /** @var string */ $dist = self::getAttribute($sppr->effectLst->$type, 'dist', 'string'); - $dist = is_numeric($dist) ? Properties::xmlToPoints($dist) : null; + $dist = is_numeric($dist) ? ChartProperties::xmlToPoints($dist) : null; /** @var string */ $direction = self::getAttribute($sppr->effectLst->$type, 'dir', 'string'); - $direction = is_numeric($direction) ? Properties::xmlToAngle($direction) : null; + $direction = is_numeric($direction) ? ChartProperties::xmlToAngle($direction) : null; $algn = self::getAttribute($sppr->effectLst->$type, 'algn', 'string'); $rot = self::getAttribute($sppr->effectLst->$type, 'rotWithShape', 'string'); $size = []; foreach (['sx', 'sy'] as $sizeType) { $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); if (is_numeric($sizeValue)) { - $size[$sizeType] = Properties::xmlToTenthOfPercent((string) $sizeValue); + $size[$sizeType] = ChartProperties::xmlToTenthOfPercent((string) $sizeValue); } else { $size[$sizeType] = null; } @@ -1216,7 +1218,7 @@ class Chart foreach (['kx', 'ky'] as $sizeType) { $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); if (is_numeric($sizeValue)) { - $size[$sizeType] = Properties::xmlToAngle((string) $sizeValue); + $size[$sizeType] = ChartProperties::xmlToAngle((string) $sizeValue); } else { $size[$sizeType] = null; } @@ -1273,7 +1275,7 @@ class Chart return $result; } - private function readLineStyle(SimpleXMLElement $chartDetail, ?Properties $chartObject): void + private function readLineStyle(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; @@ -1287,7 +1289,7 @@ class Chart /** @var string */ $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string'); if (is_numeric($lineWidthTemp)) { - $lineWidth = Properties::xmlToPoints($lineWidthTemp); + $lineWidth = ChartProperties::xmlToPoints($lineWidthTemp); } /** @var string */ $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string'); @@ -1296,15 +1298,13 @@ class Chart /** @var string */ $capType = self::getAttribute($sppr->ln, 'cap', 'string'); if (isset($sppr->ln->miter)) { - $joinType = Properties::LINE_STYLE_JOIN_MITER; + $joinType = ChartProperties::LINE_STYLE_JOIN_MITER; } elseif (isset($sppr->ln->bevel)) { - $joinType = Properties::LINE_STYLE_JOIN_BEVEL; + $joinType = ChartProperties::LINE_STYLE_JOIN_BEVEL; } else { $joinType = ''; } - $headArrowType = ''; $headArrowSize = ''; - $endArrowType = ''; $endArrowSize = ''; /** @var string */ $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string'); @@ -1403,7 +1403,7 @@ class Chart /** @var string */ $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); if (is_numeric($textRotation)) { - $whichAxis->setAxisOption('textRotation', (string) Properties::xmlToAngle($textRotation)); + $whichAxis->setAxisOption('textRotation', (string) ChartProperties::xmlToAngle($textRotation)); } } } diff --git a/src/PhpSpreadsheet/Worksheet/PageSetup.php b/src/PhpSpreadsheet/Worksheet/PageSetup.php index c23bfc59..4bdc2d4c 100644 --- a/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -259,10 +259,11 @@ class PageSetup /** * First page number. * - * @var int + * @var ?int */ private $firstPageNumber; + /** @var string */ private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER; /** @@ -375,7 +376,7 @@ class PageSetup { // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, // but it is apparently still able to handle any scale >= 0, where 0 results in 100 - if (($scale >= 0) || $scale === null) { + if ($scale === null || $scale >= 0) { $this->scale = $scale; if ($update) { $this->fitToPage = false; @@ -845,7 +846,7 @@ class PageSetup /** * Get first page number. * - * @return int + * @return ?int */ public function getFirstPageNumber() { @@ -855,7 +856,7 @@ class PageSetup /** * Set first page number. * - * @param int $value + * @param ?int $value * * @return $this */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 413ec9ef..d13d4141 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1359,7 +1359,7 @@ class Worksheet implements IComparable if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) { // then there is a row dimension with explicit style, assign it to the cell - $cell->setXfIndex($rowDimension->getXfIndex()); + $cell->setXfIndex(/** @scrutinizer ignore-type */ $rowDimension->getXfIndex()); } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) { // then there is a column dimension, assign it to the cell $cell->setXfIndex($columnDimension->getXfIndex()); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index c115cabb..fca5ee89 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -24,6 +24,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Html extends BaseWriter @@ -1277,23 +1278,22 @@ class Html extends BaseWriter } } - private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, ?string &$cellData): void + private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void { if ($cell->getValue() instanceof RichText) { $this->generateRowCellDataValueRich($cell, $cellData); } else { $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); - if ($formatCode !== null) { - $cellData = NumberFormat::toFormattedString( - $origData, - $formatCode, - [$this, 'formatColor'] - ); - } + + $cellData = NumberFormat::toFormattedString( + $origData ?? '', + $formatCode ?? NumberFormat::FORMAT_GENERAL, + [$this, 'formatColor'] + ); if ($cellData === $origData) { - $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags()); + $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags()); } if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { $cellData = '' . $cellData . ''; @@ -1477,8 +1477,8 @@ class Html extends BaseWriter && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]); // Colspan and Rowspan - $colspan = 1; - $rowspan = 1; + $colSpan = 1; + $rowSpan = 1; if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum])) { $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]; $rowSpan = $spans['rowspan']; @@ -1791,7 +1791,8 @@ class Html extends BaseWriter public function getOrientation(): ?string { - return null; + // Expect Pdf classes to override this method. + return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null; } /** @@ -1830,9 +1831,9 @@ class Html extends BaseWriter $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; '; $htmlPage .= 'margin-bottom: ' . $bottom; $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation(); - if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) { + if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) { $htmlPage .= 'size: landscape; '; - } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { + } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) { $htmlPage .= 'size: portrait; '; } $htmlPage .= '}' . PHP_EOL; diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 8f09af69..847d772a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -465,7 +465,7 @@ class Worksheet extends BIFFwriter switch ($calctype) { case 'integer': case 'double': - $this->writeNumber($row, $column, $calculatedValue, $xfIndex); + $this->writeNumber($row, $column, (float) $calculatedValue, $xfIndex); break; case 'string': @@ -473,7 +473,7 @@ class Worksheet extends BIFFwriter break; case 'boolean': - $this->writeBoolErr($row, $column, $calculatedValue, 0, $xfIndex); + $this->writeBoolErr($row, $column, (int) $calculatedValue, 0, $xfIndex); break; default: diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 5680281f..1aa4f1ca 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -503,7 +503,7 @@ class Worksheet extends WriterPart private static function writeTimePeriodCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('timePeriod', $txt); if (empty($conditional->getConditions())) { if ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TODAY) { @@ -536,7 +536,7 @@ class Worksheet extends WriterPart private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('text', $txt); if (empty($conditional->getConditions())) { if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) { @@ -1034,7 +1034,7 @@ class Worksheet extends WriterPart } else { $objWriter->writeAttribute('fitToWidth', '0'); } - if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) { + if (!empty($worksheet->getPageSetup()->getFirstPageNumber())) { $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } @@ -1228,7 +1228,7 @@ class Worksheet extends WriterPart StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags())) ); $objWriter->endElement(); - } elseif ($cellValue instanceof RichText) { + } else { $objWriter->startElement('is'); $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 0bb9f130..57c09de5 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -70,6 +70,7 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index 49878529..e6f55514 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -28,9 +28,6 @@ class RichTextTest extends TestCase public function testTextElements(): void { $element1 = new TextElement('A'); - if ($element1->getFont() !== null) { - self::fail('Expected font to be null'); - } $element2 = new TextElement('B'); $element3 = new TextElement('C'); $richText = new RichText(); From 2fe66d097fc4f5a310d5e6e387f37011793007bb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 12 Sep 2022 08:23:43 -0700 Subject: [PATCH 110/156] Upgrade mitoteam/jpgraph for Php8.2 Usage (#3058) They just released a Php8.2-compatible version. We should use that version going forward. Some tests had been disabled in 8.2 due to the problems which the new release fixes; these are now restored. --- composer.json | 2 +- composer.lock | 23 +++++++++++-------- .../PhpSpreadsheetTests/Helper/SampleTest.php | 6 ----- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 46ee56b9..6835b05b 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.1", + "mitoteam/jpgraph": "10.2.2", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 37e2dfa8..4097420d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dd19bb54ddc39f5b24f564818cb46c7e", + "content-hash": "8512207f173cb137bc2085b7fdf698bc", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1342,20 +1342,23 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.1.3", + "version": "10.2.2", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "425a2a0f0c97a28fe0aca60a4384ce85880e438a" + "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/425a2a0f0c97a28fe0aca60a4384ce85880e438a", - "reference": "425a2a0f0c97a28fe0aca60a4384ce85880e438a", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", + "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=5.5 <=8.2" + }, + "replace": { + "jpgraph/jpgraph": "4.0.2" }, "type": "library", "autoload": { @@ -1372,16 +1375,16 @@ "name": "JpGraph team" } ], - "description": "Composer compatible version of JpGraph library with PHP 8.1 support", + "description": "Composer compatible version of JpGraph library with PHP 8.2 support", "homepage": "https://github.com/mitoteam/jpgraph", "keywords": [ "jpgraph" ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.1.3" + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.2" }, - "time": "2022-07-05T16:46:34+00:00" + "time": "2022-09-09T08:16:10+00:00" }, { "name": "mpdf/mpdf", @@ -5265,5 +5268,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index a104e8ff..2195155f 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -27,12 +27,6 @@ class SampleTest extends TestCase { $skipped = [ ]; - if (PHP_VERSION_ID >= 80200) { - // Hopefully temporary. Continue to try - // 32_chart_read_write_PDF/HTML - // so as not to lose track of the problem. - $skipped[] = 'Chart/35_Chart_render.php'; - } // Unfortunately some tests are too long to run with code-coverage // analysis on GitHub Actions, so we need to exclude them From 3e8d50547c0b7a8acf9352834b3e9155a86210b3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 12 Sep 2022 08:45:13 -0700 Subject: [PATCH 111/156] Minor Fix for Percentage Formatting (#3053) Fix #1929. This was already substantially fixed, but there was a lingering problem with an unexpected leading space. It turns out there was also a problem with leading zeros, also fixed. There are also problems involving commas; fixing those seems too complicated to delay these changes, but I will add it to my to-do list. --- .../Style/NumberFormat/PercentageFormatter.php | 12 +++++++----- tests/PhpSpreadsheetTests/Style/NumberFormatTest.php | 2 +- tests/data/Style/NumberFormat.php | 9 +++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index f4d3412b..07aaff1f 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -20,7 +20,8 @@ class PercentageFormatter extends BaseFormatter $format = str_replace('%', '%%', $format); $wholePartSize = strlen((string) floor($value)); - $decimalPartSize = $placeHolders = 0; + $decimalPartSize = 0; + $placeHolders = ''; // Number of decimals if (preg_match('/\.([?0]+)/u', $format, $matches)) { $decimalPartSize = strlen($matches[1]); @@ -29,12 +30,13 @@ class PercentageFormatter extends BaseFormatter $placeHolders = str_repeat(' ', strlen($matches[1]) - $decimalPartSize); } // Number of digits to display before the decimal - if (preg_match('/([#0,]+)\./u', $format, $matches)) { - $wholePartSize = max($wholePartSize, strlen($matches[1])); + if (preg_match('/([#0,]+)\.?/u', $format, $matches)) { + $firstZero = preg_replace('/^[#,]*/', '', $matches[1]); + $wholePartSize = max($wholePartSize, strlen($firstZero)); } - $wholePartSize += $decimalPartSize; - $replacement = "{$wholePartSize}.{$decimalPartSize}"; + $wholePartSize += $decimalPartSize + (int) ($decimalPartSize > 0); + $replacement = "0{$wholePartSize}.{$decimalPartSize}"; $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); /** @var float */ diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php index e386b292..f09c34d7 100644 --- a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php +++ b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php @@ -47,7 +47,7 @@ class NumberFormatTest extends TestCase public function testFormatValueWithMask($expectedResult, ...$args): void { $result = NumberFormat::toFormattedString(...$args); - self::assertEquals($expectedResult, $result); + self::assertSame($expectedResult, $result); } public function providerNumberFormat(): array diff --git a/tests/data/Style/NumberFormat.php b/tests/data/Style/NumberFormat.php index 80f7080b..b307e23d 100644 --- a/tests/data/Style/NumberFormat.php +++ b/tests/data/Style/NumberFormat.php @@ -146,13 +146,13 @@ return [ '#,###', ], [ - 12, + '12', 12000, '#,', ], // Scaling test [ - 12.199999999999999, + '12.2', 12200000, '0.0,,', ], @@ -1486,4 +1486,9 @@ return [ '-1111.119', NumberFormat::FORMAT_ACCOUNTING_EUR, ], + 'issue 1929' => ['(79.3%)', -0.793, '#,##0.0%;(#,##0.0%)'], + 'percent without leading 0' => ['6.2%', 0.062, '##.0%'], + 'percent with leading 0' => ['06.2%', 0.062, '00.0%'], + 'percent lead0 no decimal' => ['06%', 0.062, '00%'], + 'percent nolead0 no decimal' => ['6%', 0.062, '##%'], ]; From 441ae741d7eaca24e677f553456f9b0c6a9efcda Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 5 Sep 2022 06:36:42 +0200 Subject: [PATCH 112/156] Update Excel function samples for Date/Time and Engineering functions --- samples/Calculations/DateTime/DATEDIF.php | 1 + samples/Calculations/DateTime/DAYS.php | 1 + samples/Calculations/DateTime/NETWORKDAYS.php | 66 ++++++++++++++++ samples/Calculations/DateTime/WORKDAY.php | 67 ++++++++++++++++ samples/Calculations/DateTime/YEARFRAC.php | 76 +++++++++++++++++++ samples/Calculations/Engineering/BESSELI.php | 29 +++++++ samples/Calculations/Engineering/BESSELJ.php | 29 +++++++ samples/Calculations/Engineering/BESSELK.php | 29 +++++++ samples/Calculations/Engineering/BESSELY.php | 29 +++++++ samples/Calculations/Engineering/BIN2DEC.php | 46 +++++++++++ samples/Calculations/Engineering/BIN2HEX.php | 46 +++++++++++ samples/Calculations/Engineering/BIN2OCT.php | 46 +++++++++++ samples/Calculations/Engineering/BITAND.php | 49 ++++++++++++ .../Calculations/Engineering/BITLSHIFT.php | 65 ++++++++++++++++ samples/Calculations/Engineering/BITOR.php | 49 ++++++++++++ .../Calculations/Engineering/BITRSHIFT.php | 63 +++++++++++++++ samples/Calculations/Engineering/BITXOR.php | 49 ++++++++++++ samples/Calculations/Engineering/COMPLEX.php | 41 ++++++++++ samples/Calculations/Engineering/CONVERT.php | 58 ++++++++++++++ samples/Calculations/Engineering/DEC2BIN.php | 47 ++++++++++++ samples/Calculations/Engineering/DEC2HEX.php | 48 ++++++++++++ samples/Calculations/Engineering/DEC2OCT.php | 48 ++++++++++++ samples/Calculations/Engineering/DELTA.php | 46 +++++++++++ samples/Calculations/Engineering/ERF.php | 67 ++++++++++++++++ samples/Calculations/Engineering/ERFC.php | 41 ++++++++++ samples/Calculations/Engineering/GESTEP.php | 53 +++++++++++++ samples/Calculations/Engineering/HEX2BIN.php | 46 +++++++++++ samples/Calculations/Engineering/HEX2DEC.php | 48 ++++++++++++ samples/Calculations/Engineering/HEX2OCT.php | 46 +++++++++++ samples/Calculations/Engineering/IMABS.php | 48 ++++++++++++ .../Calculations/Engineering/IMAGINARY.php | 48 ++++++++++++ .../Calculations/Engineering/IMARGUMENT.php | 48 ++++++++++++ .../Calculations/Engineering/IMCONJUGATE.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCOS.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCOSH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCOT.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCSC.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCSCH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMDIV.php | 42 ++++++++++ samples/Calculations/Engineering/IMEXP.php | 48 ++++++++++++ samples/Calculations/Engineering/IMLN.php | 48 ++++++++++++ samples/Calculations/Engineering/IMLOG10.php | 48 ++++++++++++ samples/Calculations/Engineering/IMLOG2.php | 48 ++++++++++++ samples/Calculations/Engineering/IMPOWER.php | 49 ++++++++++++ .../Calculations/Engineering/IMPRODUCT.php | 42 ++++++++++ samples/Calculations/Engineering/IMREAL.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSEC.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSECH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSIN.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSINH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSQRT.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSUB.php | 42 ++++++++++ samples/Calculations/Engineering/IMSUM.php | 42 ++++++++++ samples/Calculations/Engineering/IMTAN.php | 48 ++++++++++++ samples/Calculations/Engineering/OCT2BIN.php | 47 ++++++++++++ samples/Calculations/Engineering/OCT2DEC.php | 49 ++++++++++++ samples/Calculations/Engineering/OCT2HEX.php | 49 ++++++++++++ .../Calculation/Engineering/Compare.php | 4 +- tests/data/Calculation/DateTime/DATE.php | 5 +- .../data/Calculation/Engineering/BIN2DEC.php | 2 + 60 files changed, 2659 insertions(+), 3 deletions(-) create mode 100644 samples/Calculations/DateTime/NETWORKDAYS.php create mode 100644 samples/Calculations/DateTime/WORKDAY.php create mode 100644 samples/Calculations/DateTime/YEARFRAC.php create mode 100644 samples/Calculations/Engineering/BESSELI.php create mode 100644 samples/Calculations/Engineering/BESSELJ.php create mode 100644 samples/Calculations/Engineering/BESSELK.php create mode 100644 samples/Calculations/Engineering/BESSELY.php create mode 100644 samples/Calculations/Engineering/BIN2DEC.php create mode 100644 samples/Calculations/Engineering/BIN2HEX.php create mode 100644 samples/Calculations/Engineering/BIN2OCT.php create mode 100644 samples/Calculations/Engineering/BITAND.php create mode 100644 samples/Calculations/Engineering/BITLSHIFT.php create mode 100644 samples/Calculations/Engineering/BITOR.php create mode 100644 samples/Calculations/Engineering/BITRSHIFT.php create mode 100644 samples/Calculations/Engineering/BITXOR.php create mode 100644 samples/Calculations/Engineering/COMPLEX.php create mode 100644 samples/Calculations/Engineering/CONVERT.php create mode 100644 samples/Calculations/Engineering/DEC2BIN.php create mode 100644 samples/Calculations/Engineering/DEC2HEX.php create mode 100644 samples/Calculations/Engineering/DEC2OCT.php create mode 100644 samples/Calculations/Engineering/DELTA.php create mode 100644 samples/Calculations/Engineering/ERF.php create mode 100644 samples/Calculations/Engineering/ERFC.php create mode 100644 samples/Calculations/Engineering/GESTEP.php create mode 100644 samples/Calculations/Engineering/HEX2BIN.php create mode 100644 samples/Calculations/Engineering/HEX2DEC.php create mode 100644 samples/Calculations/Engineering/HEX2OCT.php create mode 100644 samples/Calculations/Engineering/IMABS.php create mode 100644 samples/Calculations/Engineering/IMAGINARY.php create mode 100644 samples/Calculations/Engineering/IMARGUMENT.php create mode 100644 samples/Calculations/Engineering/IMCONJUGATE.php create mode 100644 samples/Calculations/Engineering/IMCOS.php create mode 100644 samples/Calculations/Engineering/IMCOSH.php create mode 100644 samples/Calculations/Engineering/IMCOT.php create mode 100644 samples/Calculations/Engineering/IMCSC.php create mode 100644 samples/Calculations/Engineering/IMCSCH.php create mode 100644 samples/Calculations/Engineering/IMDIV.php create mode 100644 samples/Calculations/Engineering/IMEXP.php create mode 100644 samples/Calculations/Engineering/IMLN.php create mode 100644 samples/Calculations/Engineering/IMLOG10.php create mode 100644 samples/Calculations/Engineering/IMLOG2.php create mode 100644 samples/Calculations/Engineering/IMPOWER.php create mode 100644 samples/Calculations/Engineering/IMPRODUCT.php create mode 100644 samples/Calculations/Engineering/IMREAL.php create mode 100644 samples/Calculations/Engineering/IMSEC.php create mode 100644 samples/Calculations/Engineering/IMSECH.php create mode 100644 samples/Calculations/Engineering/IMSIN.php create mode 100644 samples/Calculations/Engineering/IMSINH.php create mode 100644 samples/Calculations/Engineering/IMSQRT.php create mode 100644 samples/Calculations/Engineering/IMSUB.php create mode 100644 samples/Calculations/Engineering/IMSUM.php create mode 100644 samples/Calculations/Engineering/IMTAN.php create mode 100644 samples/Calculations/Engineering/OCT2BIN.php create mode 100644 samples/Calculations/Engineering/OCT2DEC.php create mode 100644 samples/Calculations/Engineering/OCT2HEX.php diff --git a/samples/Calculations/DateTime/DATEDIF.php b/samples/Calculations/DateTime/DATEDIF.php index 7bc077c9..3e035f5a 100644 --- a/samples/Calculations/DateTime/DATEDIF.php +++ b/samples/Calculations/DateTime/DATEDIF.php @@ -24,6 +24,7 @@ $testDates = [ [2000, 1, 1], [2019, 2, 14], [2020, 7, 4], + [2020, 2, 29], ]; $testDateCount = count($testDates); diff --git a/samples/Calculations/DateTime/DAYS.php b/samples/Calculations/DateTime/DAYS.php index ddd47f37..15e9a58f 100644 --- a/samples/Calculations/DateTime/DAYS.php +++ b/samples/Calculations/DateTime/DAYS.php @@ -24,6 +24,7 @@ $testDates = [ [2000, 1, 1], [2019, 2, 14], [2020, 7, 4], + [2020, 2, 29], [2029, 12, 31], [2525, 1, 1], ]; diff --git a/samples/Calculations/DateTime/NETWORKDAYS.php b/samples/Calculations/DateTime/NETWORKDAYS.php new file mode 100644 index 00000000..585c0438 --- /dev/null +++ b/samples/Calculations/DateTime/NETWORKDAYS.php @@ -0,0 +1,66 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$publicHolidays = [ + [2022, 1, 3, '=DATE(G1, H1, I1)', 'New Year'], + [2022, 4, 15, '=DATE(G2, H2, I2)', 'Good Friday'], + [2022, 4, 18, '=DATE(G3, H3, I3)', 'Easter Monday'], + [2022, 5, 2, '=DATE(G4, H4, I4)', 'Early May Bank Holiday'], + [2022, 6, 2, '=DATE(G5, H5, I5)', 'Spring Bank Holiday'], + [2022, 6, 3, '=DATE(G6, H6, I6)', 'Platinum Jubilee Bank Holiday'], + [2022, 8, 29, '=DATE(G7, H7, I7)', 'Summer Bank Holiday'], + [2022, 12, 26, '=DATE(G8, H8, I8)', 'Boxing Day'], + [2022, 12, 27, '=DATE(G9, H9, I9)', 'Christmas Day'], +]; + +$holidayCount = count($publicHolidays); +$worksheet->fromArray($publicHolidays, null, 'G1', true); + +$worksheet->getStyle('J1:J' . $holidayCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->setCellValue('A1', '=DATE(2022,1,1)'); + +for ($numberOfMonths = 0; $numberOfMonths < 12; ++$numberOfMonths) { + $worksheet->setCellValue('B' . ($numberOfMonths + 1), '=EOMONTH(A1, ' . $numberOfMonths . ')'); + $worksheet->setCellValue('C' . ($numberOfMonths + 1), '=NETWORKDAYS(A1, B' . ($numberOfMonths + 1) . ')'); + $worksheet->setCellValue('D' . ($numberOfMonths + 1), '=NETWORKDAYS(A1, B' . ($numberOfMonths + 1) . ', J1:J' . $holidayCount . ')'); +} + +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('B1:B12') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log('UK Public Holidays'); +$holidayData = $worksheet->rangeToArray('J1:K' . $holidayCount, null, true, true, true); +$helper->displayGrid($holidayData); + +for ($row = 1; $row <= 12; ++$row) { + $helper->log(sprintf( + 'Between %s and %s is %d working days; %d with public holidays', + $worksheet->getCell('A1')->getFormattedValue(), + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/WORKDAY.php b/samples/Calculations/DateTime/WORKDAY.php new file mode 100644 index 00000000..d18e3463 --- /dev/null +++ b/samples/Calculations/DateTime/WORKDAY.php @@ -0,0 +1,67 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$publicHolidays = [ + [2022, 1, 3, '=DATE(G1, H1, I1)', 'New Year'], + [2022, 4, 15, '=DATE(G2, H2, I2)', 'Good Friday'], + [2022, 4, 18, '=DATE(G3, H3, I3)', 'Easter Monday'], + [2022, 5, 2, '=DATE(G4, H4, I4)', 'Early May Bank Holiday'], + [2022, 6, 2, '=DATE(G5, H5, I5)', 'Spring Bank Holiday'], + [2022, 6, 3, '=DATE(G6, H6, I6)', 'Platinum Jubilee Bank Holiday'], + [2022, 8, 29, '=DATE(G7, H7, I7)', 'Summer Bank Holiday'], + [2022, 12, 26, '=DATE(G8, H8, I8)', 'Boxing Day'], + [2022, 12, 27, '=DATE(G9, H9, I9)', 'Christmas Day'], +]; + +$holidayCount = count($publicHolidays); +$worksheet->fromArray($publicHolidays, null, 'G1', true); + +$worksheet->getStyle('J1:J' . $holidayCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->setCellValue('A1', '=DATE(2022,1,1)'); + +$workdayStep = 10; +for ($days = $workdayStep; $days <= 366; $days += $workdayStep) { + $worksheet->setCellValue('B' . ((int) $days / $workdayStep + 1), $days); + $worksheet->setCellValue('C' . ((int) $days / $workdayStep + 1), '=WORKDAY(A1, B' . ((int) $days / $workdayStep + 1) . ')'); + $worksheet->setCellValue('D' . ((int) $days / $workdayStep + 1), '=WORKDAY(A1, B' . ((int) $days / $workdayStep + 1) . ', J1:J' . $holidayCount . ')'); +} + +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('C1:D50') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log('UK Public Holidays'); +$holidayData = $worksheet->rangeToArray('J1:K' . $holidayCount, null, true, true, true); +$helper->displayGrid($holidayData); + +for ($days = $workdayStep; $days <= 366; $days += $workdayStep) { + $helper->log(sprintf( + '%d workdays from %s is %s; %s with public holidays', + $worksheet->getCell('B' . ((int) $days / $workdayStep + 1))->getFormattedValue(), + $worksheet->getCell('A1')->getFormattedValue(), + $worksheet->getCell('C' . ((int) $days / $workdayStep + 1))->getFormattedValue(), + $worksheet->getCell('D' . ((int) $days / $workdayStep + 1))->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/YEARFRAC.php b/samples/Calculations/DateTime/YEARFRAC.php new file mode 100644 index 00000000..81b36435 --- /dev/null +++ b/samples/Calculations/DateTime/YEARFRAC.php @@ -0,0 +1,76 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2020, 2, 29], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DATE(2022,12,31)'); + $worksheet->setCellValue('G' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ')'); + $worksheet->setCellValue('H' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 1)'); + $worksheet->setCellValue('I' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 2)'); + $worksheet->setCellValue('J' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 3)'); + $worksheet->setCellValue('K' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 4)'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log(sprintf( + 'Days: %f - US (NASD) 30/360', + $worksheet->getCell('G' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual', + $worksheet->getCell('H' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual/360', + $worksheet->getCell('I' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual/365', + $worksheet->getCell('J' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - European 30/360', + $worksheet->getCell('K' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/Engineering/BESSELI.php b/samples/Calculations/Engineering/BESSELI.php new file mode 100644 index 00000000..bd5d9b71 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELI.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELI({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELJ.php b/samples/Calculations/Engineering/BESSELJ.php new file mode 100644 index 00000000..3aa47886 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELJ.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELJ({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELK.php b/samples/Calculations/Engineering/BESSELK.php new file mode 100644 index 00000000..ee8698e9 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELK.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELK({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELY.php b/samples/Calculations/Engineering/BESSELY.php new file mode 100644 index 00000000..750b7204 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELY.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELY({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BIN2DEC.php b/samples/Calculations/Engineering/BIN2DEC.php new file mode 100644 index 00000000..0c4c4532 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2DEC.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BIN2HEX.php b/samples/Calculations/Engineering/BIN2HEX.php new file mode 100644 index 00000000..51a11199 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2HEX.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BIN2OCT.php b/samples/Calculations/Engineering/BIN2OCT.php new file mode 100644 index 00000000..c320d360 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2OCT.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITAND.php b/samples/Calculations/Engineering/BITAND.php new file mode 100644 index 00000000..2a8f7a3c --- /dev/null +++ b/samples/Calculations/Engineering/BITAND.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITAND(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise AND of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITLSHIFT.php b/samples/Calculations/Engineering/BITLSHIFT.php new file mode 100644 index 00000000..872c8098 --- /dev/null +++ b/samples/Calculations/Engineering/BITLSHIFT.php @@ -0,0 +1,65 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1], + [3], + [9], + [15], + [26], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); + $worksheet->setCellValue('C' . $row, '=BITLSHIFT(A' . $row . ',1)'); + $worksheet->setCellValue('D' . $row, '=DEC2BIN(C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=BITLSHIFT(A' . $row . ',2)'); + $worksheet->setCellValue('F' . $row, '=DEC2BIN(E' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=BITLSHIFT(A' . $row . ',3)'); + $worksheet->setCellValue('H' . $row, '=DEC2BIN(G' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 1 bit is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 2 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 3 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITOR.php b/samples/Calculations/Engineering/BITOR.php new file mode 100644 index 00000000..1bf7f71d --- /dev/null +++ b/samples/Calculations/Engineering/BITOR.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITOR(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise OR of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITRSHIFT.php b/samples/Calculations/Engineering/BITRSHIFT.php new file mode 100644 index 00000000..3e7f3a88 --- /dev/null +++ b/samples/Calculations/Engineering/BITRSHIFT.php @@ -0,0 +1,63 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [9], + [15], + [26], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); + $worksheet->setCellValue('C' . $row, '=BITRSHIFT(A' . $row . ',1)'); + $worksheet->setCellValue('D' . $row, '=DEC2BIN(C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=BITRSHIFT(A' . $row . ',2)'); + $worksheet->setCellValue('F' . $row, '=DEC2BIN(E' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=BITRSHIFT(A' . $row . ',3)'); + $worksheet->setCellValue('H' . $row, '=DEC2BIN(G' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 1 bit is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 2 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 3 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITXOR.php b/samples/Calculations/Engineering/BITXOR.php new file mode 100644 index 00000000..482662cd --- /dev/null +++ b/samples/Calculations/Engineering/BITXOR.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITXOR(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise XOR of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/COMPLEX.php b/samples/Calculations/Engineering/COMPLEX.php new file mode 100644 index 00000000..c58a1797 --- /dev/null +++ b/samples/Calculations/Engineering/COMPLEX.php @@ -0,0 +1,41 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3, 4], + [3, 4, '"j"'], + [3.5, 4.75], + [0, 1], + [1, 0], + [0, -1], + [0, 2], + [2, 0], +]; +$testDataCount = count($testData); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=COMPLEX(' . implode(',', $testData[$row - 1]) . ')'); +} + +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(A%d): Formula %s result is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/Engineering/CONVERT.php b/samples/Calculations/Engineering/CONVERT.php new file mode 100644 index 00000000..bce56ba5 --- /dev/null +++ b/samples/Calculations/Engineering/CONVERT.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$conversions = [ + [1, '"lbm"', '"kg"'], + [1, '"gal"', '"l"'], + [24, '"in"', '"ft"'], + [100, '"yd"', '"m"'], + [500, '"mi"', '"km"'], + [7.5, '"min"', '"sec"'], + [5, '"F"', '"C"'], + [32, '"C"', '"K"'], + [100, '"m2"', '"ft2"'], +]; +$testDataCount = count($conversions); + +$worksheet->fromArray($conversions, null, 'A1'); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=CONVERT(' . implode(',', $conversions[$row - 1]) . ')'); +} + +$worksheet->setCellValue('H1', '=CONVERT(CONVERT(100,"m","ft"),"m","ft")'); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(A%d): Unit of Measure Conversion Formula %s - %d %s is %f %s', + $row, + $worksheet->getCell('D' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + trim($worksheet->getCell('B' . $row)->getValue(), '"'), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + trim($worksheet->getCell('C' . $row)->getValue(), '"') + )); +} + +$helper->log('Old method for area conversions, before MS Excel introduced area Units of Measure'); + +$helper->log(sprintf( + '(A%d): Unit of Measure Conversion Formula %s result is %s', + $row, + $worksheet->getCell('H1')->getValue(), + $worksheet->getCell('H1')->getCalculatedValue() +)); diff --git a/samples/Calculations/Engineering/DEC2BIN.php b/samples/Calculations/Engineering/DEC2BIN.php new file mode 100644 index 00000000..2a064c06 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2BIN.php @@ -0,0 +1,47 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DEC2HEX.php b/samples/Calculations/Engineering/DEC2HEX.php new file mode 100644 index 00000000..0a19ae54 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2HEX.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], + [12345678], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DEC2OCT.php b/samples/Calculations/Engineering/DEC2OCT.php new file mode 100644 index 00000000..fc11a832 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2OCT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], + [12345678], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DELTA.php b/samples/Calculations/Engineering/DELTA.php new file mode 100644 index 00000000..cd51b161 --- /dev/null +++ b/samples/Calculations/Engineering/DELTA.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [4, 5], + [3, 3], + [0.5, 0], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=DELTA(A' . $row . ',B' . $row . ')'); +} + +$comparison = [ + 0 => 'The values are not equal', + 1 => 'The values are equal', +]; + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Compare values %d and %d - Result is %d - %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $comparison[$worksheet->getCell('C' . $row)->getCalculatedValue()] + )); +} diff --git a/samples/Calculations/Engineering/ERF.php b/samples/Calculations/Engineering/ERF.php new file mode 100644 index 00000000..e6505888 --- /dev/null +++ b/samples/Calculations/Engineering/ERF.php @@ -0,0 +1,67 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData1 = [ + [0.745], + [1], + [1.5], + [-2], +]; + +$testData2 = [ + [0, 1.5], + [1, 2], + [-2, 1], +]; +$testDataCount1 = count($testData1); +$testDataCount2 = count($testData2); +$testData2StartRow = $testDataCount1 + 1; + +$worksheet->fromArray($testData1, null, 'A1', true); +$worksheet->fromArray($testData2, null, "A{$testData2StartRow}", true); + +for ($row = 1; $row <= $testDataCount1; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERF(A' . $row . ')'); +} + +for ($row = $testDataCount1 + 1; $row <= $testDataCount2 + $testDataCount1; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERF(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +$helper->log('ERF() With a single argument'); +for ($row = 1; $row <= $testDataCount1; ++$row) { + $helper->log(sprintf( + '(C%d): %s The error function integrated between 0 and %f is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} + +$helper->log('ERF() With two arguments'); +for ($row = $testDataCount1 + 1; $row <= $testDataCount2 + $testDataCount1; ++$row) { + $helper->log(sprintf( + '(C%d): %s The error function integrated between %f and %f is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/ERFC.php b/samples/Calculations/Engineering/ERFC.php new file mode 100644 index 00000000..5e7bcc6d --- /dev/null +++ b/samples/Calculations/Engineering/ERFC.php @@ -0,0 +1,41 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [0], + [0.5], + [1], + [-1], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERFC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): %s The complementary error function integrated by %f and infinity is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/GESTEP.php b/samples/Calculations/Engineering/GESTEP.php new file mode 100644 index 00000000..73f0a31c --- /dev/null +++ b/samples/Calculations/Engineering/GESTEP.php @@ -0,0 +1,53 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [5, 4], + [5, 5], + [4, 5], + [-4, -5], + [-5, -4], + [1], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=GESTEP(A' . $row . ',B' . $row . ')'); +} + +$comparison = [ + 0 => 'Value %d is less than step %d', + 1 => 'Value %d is greater than or equal to step %d', +]; + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Compare value %d and step %d - Result is %d - %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + sprintf( + $comparison[$worksheet->getCell('C' . $row)->getCalculatedValue()], + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + ) + )); +} diff --git a/samples/Calculations/Engineering/HEX2BIN.php b/samples/Calculations/Engineering/HEX2BIN.php new file mode 100644 index 00000000..2ad08925 --- /dev/null +++ b/samples/Calculations/Engineering/HEX2BIN.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [8], + [42], + [99], + ['A2'], + ['F0'], + ['100'], + ['128'], + ['1AB'], + ['1FF'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/HEX2DEC.php b/samples/Calculations/Engineering/HEX2DEC.php new file mode 100644 index 00000000..745d4110 --- /dev/null +++ b/samples/Calculations/Engineering/HEX2DEC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['08'], + ['42'], + ['A2'], + ['400'], + ['1000'], + ['1234'], + ['ABCD'], + ['C3B0'], + ['FFFFFFFFF'], + ['FFFFFFFFFF'], + ['FFFFFFF800'], + ['FEDCBA9876'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/HEX2OCT.php b/samples/Calculations/Engineering/HEX2OCT.php new file mode 100644 index 00000000..3608c1bb --- /dev/null +++ b/samples/Calculations/Engineering/HEX2OCT.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['08'], + ['42'], + ['A2'], + ['400'], + ['100'], + ['1234'], + ['ABCD'], + ['C3B0'], + ['FFFFFFFFFF'], + ['FFFFFFF800'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMABS.php b/samples/Calculations/Engineering/IMABS.php new file mode 100644 index 00000000..9c6b843c --- /dev/null +++ b/samples/Calculations/Engineering/IMABS.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMABS(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The absolute value of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMAGINARY.php b/samples/Calculations/Engineering/IMAGINARY.php new file mode 100644 index 00000000..99138938 --- /dev/null +++ b/samples/Calculations/Engineering/IMAGINARY.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMAGINARY(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The imaginary component of %s is %f', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMARGUMENT.php b/samples/Calculations/Engineering/IMARGUMENT.php new file mode 100644 index 00000000..e559e951 --- /dev/null +++ b/samples/Calculations/Engineering/IMARGUMENT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMARGUMENT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Theta Argument of %s is %f radians', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCONJUGATE.php b/samples/Calculations/Engineering/IMCONJUGATE.php new file mode 100644 index 00000000..3b4429ed --- /dev/null +++ b/samples/Calculations/Engineering/IMCONJUGATE.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCONJUGATE(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Conjugate of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOS.php b/samples/Calculations/Engineering/IMCOS.php new file mode 100644 index 00000000..5b8f81ea --- /dev/null +++ b/samples/Calculations/Engineering/IMCOS.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOS(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cosine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOSH.php b/samples/Calculations/Engineering/IMCOSH.php new file mode 100644 index 00000000..9a393766 --- /dev/null +++ b/samples/Calculations/Engineering/IMCOSH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOSH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Cosine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOT.php b/samples/Calculations/Engineering/IMCOT.php new file mode 100644 index 00000000..e3d980cd --- /dev/null +++ b/samples/Calculations/Engineering/IMCOT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cotangent of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCSC.php b/samples/Calculations/Engineering/IMCSC.php new file mode 100644 index 00000000..ab6695d0 --- /dev/null +++ b/samples/Calculations/Engineering/IMCSC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCSC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cosecant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCSCH.php b/samples/Calculations/Engineering/IMCSCH.php new file mode 100644 index 00000000..4513d9e9 --- /dev/null +++ b/samples/Calculations/Engineering/IMCSCH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCSCH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Cosecant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMDIV.php b/samples/Calculations/Engineering/IMDIV.php new file mode 100644 index 00000000..9512be57 --- /dev/null +++ b/samples/Calculations/Engineering/IMDIV.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMDIV(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Quotient of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMEXP.php b/samples/Calculations/Engineering/IMEXP.php new file mode 100644 index 00000000..7f5837b2 --- /dev/null +++ b/samples/Calculations/Engineering/IMEXP.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMEXP(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Exponential of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLN.php b/samples/Calculations/Engineering/IMLN.php new file mode 100644 index 00000000..95618257 --- /dev/null +++ b/samples/Calculations/Engineering/IMLN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Natural Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLOG10.php b/samples/Calculations/Engineering/IMLOG10.php new file mode 100644 index 00000000..d501c3de --- /dev/null +++ b/samples/Calculations/Engineering/IMLOG10.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLOG10(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Base-10 Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLOG2.php b/samples/Calculations/Engineering/IMLOG2.php new file mode 100644 index 00000000..25986b39 --- /dev/null +++ b/samples/Calculations/Engineering/IMLOG2.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLOG2(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Base-2 Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMPOWER.php b/samples/Calculations/Engineering/IMPOWER.php new file mode 100644 index 00000000..c6674fbe --- /dev/null +++ b/samples/Calculations/Engineering/IMPOWER.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', 2], + ['5-12i', 2], + ['3.25+7.5i', 3], + ['3.25-12.5i', 2], + ['-3.25+7.5i', 3], + ['-3.25-7.5i', 4], + ['0-j', 5], + ['0-2.5j', 3], + ['0+j', 2.5], + ['0+1.25j', 2], + [4, 3], + [-2.5, 2], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMPOWER(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): %s raised to the power of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMPRODUCT.php b/samples/Calculations/Engineering/IMPRODUCT.php new file mode 100644 index 00000000..f81bc666 --- /dev/null +++ b/samples/Calculations/Engineering/IMPRODUCT.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMPRODUCT(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Product of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMREAL.php b/samples/Calculations/Engineering/IMREAL.php new file mode 100644 index 00000000..4e537c0f --- /dev/null +++ b/samples/Calculations/Engineering/IMREAL.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMREAL(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The real component of %s is %f radians', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSEC.php b/samples/Calculations/Engineering/IMSEC.php new file mode 100644 index 00000000..e6c524b3 --- /dev/null +++ b/samples/Calculations/Engineering/IMSEC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Secant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSECH.php b/samples/Calculations/Engineering/IMSECH.php new file mode 100644 index 00000000..e07b6e08 --- /dev/null +++ b/samples/Calculations/Engineering/IMSECH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSECH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Secant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSIN.php b/samples/Calculations/Engineering/IMSIN.php new file mode 100644 index 00000000..d3b8c281 --- /dev/null +++ b/samples/Calculations/Engineering/IMSIN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Sine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSINH.php b/samples/Calculations/Engineering/IMSINH.php new file mode 100644 index 00000000..ac0a9039 --- /dev/null +++ b/samples/Calculations/Engineering/IMSINH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSINH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Sine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSQRT.php b/samples/Calculations/Engineering/IMSQRT.php new file mode 100644 index 00000000..c2573c91 --- /dev/null +++ b/samples/Calculations/Engineering/IMSQRT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSQRT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Square Root of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSUB.php b/samples/Calculations/Engineering/IMSUB.php new file mode 100644 index 00000000..90bd27a4 --- /dev/null +++ b/samples/Calculations/Engineering/IMSUB.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMSUB(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Difference between %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSUM.php b/samples/Calculations/Engineering/IMSUM.php new file mode 100644 index 00000000..2a8be320 --- /dev/null +++ b/samples/Calculations/Engineering/IMSUM.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMSUM(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Sum of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMTAN.php b/samples/Calculations/Engineering/IMTAN.php new file mode 100644 index 00000000..ffaa53b2 --- /dev/null +++ b/samples/Calculations/Engineering/IMTAN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMTAN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Tangent of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2BIN.php b/samples/Calculations/Engineering/OCT2BIN.php new file mode 100644 index 00000000..9c4bbf86 --- /dev/null +++ b/samples/Calculations/Engineering/OCT2BIN.php @@ -0,0 +1,47 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [7], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [567], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2DEC.php b/samples/Calculations/Engineering/OCT2DEC.php new file mode 100644 index 00000000..ea6afb2f --- /dev/null +++ b/samples/Calculations/Engineering/OCT2DEC.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [7], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [4567], + [7777700001], + [7777776543], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2HEX.php b/samples/Calculations/Engineering/OCT2HEX.php new file mode 100644 index 00000000..47e9b6e1 --- /dev/null +++ b/samples/Calculations/Engineering/OCT2HEX.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [12], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [4567], + [7777700001], + [7777776543], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php index 4d4bc07e..6aaf1faa 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php @@ -57,7 +57,7 @@ class Compare * * @param array|float $number the value to test against step * Or can be an array of values - * @param array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. + * @param null|array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. * Or can be an array of values * * @return array|int|string (string in the event of an error) @@ -72,7 +72,7 @@ class Compare try { $number = EngineeringValidations::validateFloat($number); - $step = EngineeringValidations::validateFloat($step); + $step = EngineeringValidations::validateFloat($step ?? 0.0); } catch (Exception $e) { return $e->getMessage(); } diff --git a/tests/data/Calculation/DateTime/DATE.php b/tests/data/Calculation/DateTime/DATE.php index 72816b76..d8b4303a 100644 --- a/tests/data/Calculation/DateTime/DATE.php +++ b/tests/data/Calculation/DateTime/DATE.php @@ -1,7 +1,9 @@ Date: Wed, 14 Sep 2022 07:11:20 -0700 Subject: [PATCH 113/156] Scrutinizer Clean Up Tests (#3061) * Scrutinizer Clean Up Tests No source code involved. * Scrutinizer Whack-a-mole Fixed 17, added 10. Trying again. * Simplify Some Tests Eliminate some null assertions. * Dead Code Remove 2 statements. --- infra/DocumentGenerator.php | 2 +- ...dling_loader_exceptions_using_TryCatch.php | 2 +- src/PhpSpreadsheet/Spreadsheet.php | 13 ++ .../Engineering/ParseComplexTest.php | 8 +- .../Functions/Financial/IrrTest.php | 45 +++-- .../Functions/LookupRef/ColumnsTest.php | 8 +- .../Functions/LookupRef/IndexTest.php | 8 +- .../Functions/LookupRef/RowsTest.php | 8 +- .../Functions/MathTrig/RandBetweenTest.php | 2 +- tests/PhpSpreadsheetTests/DefinedNameTest.php | 36 ++-- .../Functional/PrintAreaTest.php | 3 +- .../PhpSpreadsheetTests/NamedFormulaTest.php | 12 +- tests/PhpSpreadsheetTests/NamedRangeTest.php | 12 +- .../Reader/Csv/CsvContiguousTest.php | 10 +- .../Reader/Xls/NumberFormatGeneralTest.php | 24 +-- .../Reader/Xls/RichTextSizeTest.php | 3 +- .../Reader/Xlsx/AutoFilter2Test.php | 9 +- .../Reader/Xlsx/ChartSheetTest.php | 7 +- .../Reader/Xlsx/Issue2501Test.php | 16 +- .../Reader/Xlsx/NamespaceNonStdTest.php | 10 +- .../Reader/Xlsx/NamespaceOpenpyxl35Test.php | 10 +- .../Reader/Xlsx/NamespaceStdTest.php | 10 +- .../Reader/Xlsx/PageSetup2Test.php | 3 +- .../Shared/PasswordHasherTest.php | 23 ++- tests/PhpSpreadsheetTests/SpreadsheetTest.php | 171 ++++++++++-------- .../ConditionalFormatting/CellMatcherTest.php | 92 ++++++---- .../Wizard/WizardFactoryTest.php | 9 +- .../Writer/Xls/VisibilityTest.php | 2 +- .../Writer/Xlsx/UnparsedDataCloneTest.php | 12 +- .../Writer/Xlsx/VisibilityTest.php | 2 +- tests/data/Calculation/Financial/IRR.php | 1 + 31 files changed, 292 insertions(+), 281 deletions(-) diff --git a/infra/DocumentGenerator.php b/infra/DocumentGenerator.php index e2c3c86c..8a6be076 100644 --- a/infra/DocumentGenerator.php +++ b/infra/DocumentGenerator.php @@ -41,7 +41,7 @@ class DocumentGenerator private static function tableRow(array $lengths, ?array $values = null): string { $result = ''; - foreach (array_map(null, $lengths, $values ?? []) as $i => [$length, $value]) { + foreach (array_map(/** @scrutinizer ignore-type */ null, $lengths, $values ?? []) as $i => [$length, $value]) { $pad = $value === null ? '-' : ' '; if ($i > 0) { $result .= '|' . $pad; diff --git a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php index 603f6cb8..d984b7b6 100644 --- a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php +++ b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php @@ -11,5 +11,5 @@ $helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFi try { $spreadsheet = IOFactory::load($inputFileName); } catch (ReaderException $e) { - $helper->log('Error loading file "' . pathinfo($inputFileName, PATHINFO_BASENAME) . '": ' . $e->getMessage()); + $helper->log('Error loading file "' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . '": ' . $e->getMessage()); } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 4bb93987..364700e2 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -724,6 +724,19 @@ class Spreadsheet return null; } + /** + * Get sheet by name, throwing exception if not found. + */ + public function getSheetByNameOrThrow(string $worksheetName): Worksheet + { + $worksheet = $this->getSheetByName($worksheetName); + if ($worksheet === null) { + throw new Exception("Sheet $worksheetName does not exist."); + } + + return $worksheet; + } + /** * Get index for sheet. * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php index 1022052e..a32d946f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php @@ -3,21 +3,15 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ParseComplexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - public function testParseComplex(): void { [$real, $imaginary, $suffix] = [1.23e-4, 5.67e+8, 'j']; - $result = Engineering::parseComplex('1.23e-4+5.67e+8j'); + $result = /** @scrutinizer ignore-deprecated */ Engineering::parseComplex('1.23e-4+5.67e+8j'); self::assertArrayHasKey('real', $result); self::assertEquals($real, $result['real']); self::assertArrayHasKey('imaginary', $result); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php index 3c4bdb5a..2565f6c2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php @@ -2,26 +2,45 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PHPUnit\Framework\TestCase; - -class IrrTest extends TestCase +class IrrTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerIRR * * @param mixed $expectedResult + * @param mixed $values */ - public function testIRR($expectedResult, ...$args): void + public function testIRR($expectedResult, $values = null): void { - $result = Financial::IRR(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $formula = '=IRR('; + if ($values !== null) { + if (is_array($values)) { + $row = 0; + foreach ($values as $value) { + if (is_array($value)) { + foreach ($value as $arrayValue) { + ++$row; + $sheet->getCell("A$row")->setValue($arrayValue); + } + } else { + ++$row; + $sheet->getCell("A$row")->setValue($value); + } + } + $formula .= "A1:A$row"; + } else { + $sheet->getCell('A1')->setValue($values); + $formula .= 'A1'; + } + } + $formula .= ')'; + $sheet->getCell('D1')->setValue($formula); + $result = $sheet->getCell('D1')->getCalculatedValue(); + $this->adjustResult($result, $expectedResult); + + self::assertEqualsWithDelta($expectedResult, $result, 0.1E-8); } public function providerIRR(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php index f4f9bb9e..e8790cbf 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class ColumnsTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOLUMNS * @@ -21,7 +15,7 @@ class ColumnsTest extends TestCase */ public function testCOLUMNS($expectedResult, ...$args): void { - $result = LookupRef::COLUMNS(...$args); + $result = LookupRef::COLUMNS(/** @scrutinizer ignore-type */ ...$args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php index 03c87b50..fa3f4848 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class IndexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerINDEX * @@ -21,7 +15,7 @@ class IndexTest extends TestCase */ public function testINDEX($expectedResult, ...$args): void { - $result = LookupRef::INDEX(...$args); + $result = LookupRef::INDEX(/** @scrutinizer ignore-type */ ...$args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php index 2155bdf1..fd9902f4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class RowsTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerROWS * @@ -21,7 +15,7 @@ class RowsTest extends TestCase */ public function testROWS($expectedResult, ...$args): void { - $result = LookupRef::ROWS(...$args); + $result = LookupRef::ROWS(/** @scrutinizer ignore-type */ ...$args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php index 99cc1fa8..50b7117c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php @@ -60,7 +60,7 @@ class RandBetweenTest extends AllSetupTeardown $formula = "=RandBetween({$argument1}, {$argument2})"; $result = $calculation->_calculateFormulaValue($formula); self::assertIsArray($result); - self::assertCount($expectedRows, $result); + self::assertCount($expectedRows, /** @scrutinizer ignore-type */ $result); self::assertIsArray($result[0]); self::assertCount($expectedColumns, /** @scrutinizer ignore-type */ $result[0]); } diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 82950880..c001f204 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -58,7 +58,7 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); self::assertCount(2, $this->spreadsheet->getDefinedNames()); @@ -66,7 +66,7 @@ class DefinedNameTest extends TestCase self::assertNotNull($definedName1); self::assertSame('=A1', $definedName1->getValue()); - $definedName2 = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $definedName2 = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($definedName2); self::assertSame('=B1', $definedName2->getValue()); } @@ -103,13 +103,13 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getDefinedNames()); - $definedName = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $definedName = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($definedName); self::assertSame('=B1', $definedName->getValue()); } @@ -120,10 +120,10 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); - $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getDefinedNames()); $definedName = $this->spreadsheet->getDefinedName('foo'); @@ -154,10 +154,8 @@ class DefinedNameTest extends TestCase public function testChangeWorksheet(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -172,10 +170,8 @@ class DefinedNameTest extends TestCase public function testLocalOnly(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -190,10 +186,8 @@ class DefinedNameTest extends TestCase public function testScope(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -208,10 +202,8 @@ class DefinedNameTest extends TestCase public function testClone(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); diff --git a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php index 700c7c7f..921c59f5 100644 --- a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php +++ b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php @@ -58,8 +58,7 @@ class PrintAreaTest extends AbstractFunctional private static function getPrintArea(Spreadsheet $spreadsheet, string $name): string { - $sheet = $spreadsheet->getSheetByName($name); - self::assertNotNull($sheet, "Unable to get sheet $name"); + $sheet = $spreadsheet->getSheetByNameOrThrow($name); return $sheet->getPageSetup()->getPrintArea(); } diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 02e9d818..28857abe 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -62,7 +62,7 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); self::assertCount(2, $this->spreadsheet->getNamedFormulae()); @@ -72,7 +72,7 @@ class NamedFormulaTest extends TestCase '=19%', $formula->getValue() ); - $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($formula); self::assertSame( '=16%', @@ -100,13 +100,13 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getNamedFormulae()); - $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($formula); self::assertSame( '=16%', @@ -120,10 +120,10 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); - $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getNamedFormulae()); $formula = $this->spreadsheet->getNamedFormula('foo'); diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index 402e7eba..9440ef21 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -62,7 +62,7 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); self::assertCount(2, $this->spreadsheet->getNamedRanges()); @@ -72,7 +72,7 @@ class NamedRangeTest extends TestCase '=A1', $range->getValue() ); - $range = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $range = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($range); self::assertSame( '=B1', @@ -100,13 +100,13 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getNamedRanges()); - $sheet = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $sheet = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($sheet); self::assertSame( '=B1', @@ -120,10 +120,10 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); - $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getNamedRanges()); $range = $this->spreadsheet->getNamedRange('foo'); diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php index 291a29d0..8c28c1e7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php @@ -56,13 +56,11 @@ class CsvContiguousTest extends TestCase private static function getCellValue(Spreadsheet $spreadsheet, string $sheetName, string $cellAddress): string { - $sheet = $spreadsheet->getSheetByName($sheetName); + $sheet = $spreadsheet->getSheetByNameOrThrow($sheetName); $result = ''; - if ($sheet !== null) { - $value = $sheet->getCell($cellAddress)->getValue(); - if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { - $result = (string) $value; - } + $value = $sheet->getCell($cellAddress)->getValue(); + if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { + $result = (string) $value; } return $result; diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php index a67fbb34..80867892 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php @@ -15,20 +15,16 @@ class NumberFormatGeneralTest extends AbstractFunctional $reader = new Xls(); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('Blad1'); - if ($sheet === null) { - self::fail('Expected to find sheet Blad1'); - } else { - $array = $sheet->toArray(); - self::assertSame('€ 2.95', $array[1][3]); - self::assertSame(2.95, $sheet->getCell('D2')->getValue()); - self::assertSame(2.95, $sheet->getCell('D2')->getCalculatedValue()); - self::assertSame('€ 2.95', $sheet->getCell('D2')->getFormattedValue()); - self::assertSame(21, $array[1][4]); - self::assertSame(21, $sheet->getCell('E2')->getValue()); - self::assertSame(21, $sheet->getCell('E2')->getCalculatedValue()); - self::assertSame('21', $sheet->getCell('E2')->getFormattedValue()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('Blad1'); + $array = $sheet->toArray(); + self::assertSame('€ 2.95', $array[1][3]); + self::assertSame(2.95, $sheet->getCell('D2')->getValue()); + self::assertSame(2.95, $sheet->getCell('D2')->getCalculatedValue()); + self::assertSame('€ 2.95', $sheet->getCell('D2')->getFormattedValue()); + self::assertSame(21, $array[1][4]); + self::assertSame(21, $sheet->getCell('E2')->getValue()); + self::assertSame(21, $sheet->getCell('E2')->getCalculatedValue()); + self::assertSame('21', $sheet->getCell('E2')->getFormattedValue()); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php index 54274e8c..cf45dec1 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php @@ -12,8 +12,7 @@ class RichTextSizeTest extends AbstractFunctional $filename = 'tests/data/Reader/XLS/RichTextFontSize.xls'; $reader = new Xls(); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('橱柜门板'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('橱柜门板'); $text = $sheet->getCell('L15')->getValue(); $elements = $text->getRichTextElements(); self::assertEquals(10, $elements[2]->getFont()->getSize()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 57c09de5..06d6b562 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -28,8 +28,7 @@ class AutoFilter2Test extends TestCase public function testReadDateRange(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('daterange'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('daterange'); $filter = $sheet->getAutoFilter(); $maxRow = 30; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -61,8 +60,7 @@ class AutoFilter2Test extends TestCase public function testReadTopTen(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('top10'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('top10'); $filter = $sheet->getAutoFilter(); $maxRow = 65; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -87,8 +85,7 @@ class AutoFilter2Test extends TestCase public function testReadDynamic(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('dynamic'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('dynamic'); $filter = $sheet->getAutoFilter(); $maxRow = 30; self::assertSame("A1:A$maxRow", $filter->getRange()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php index 0f1605ff..e7862697 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class ChartSheetTest extends TestCase @@ -16,8 +15,7 @@ class ChartSheetTest extends TestCase $spreadsheet = $reader->load($filename); self::assertCount(2, $spreadsheet->getAllSheets()); - $chartSheet = $spreadsheet->getSheetByName('Chart1'); - self::assertInstanceOf(Worksheet::class, $chartSheet); + $chartSheet = $spreadsheet->getSheetByNameOrThrow('Chart1'); self::assertSame(1, $chartSheet->getChartCount()); } @@ -29,7 +27,6 @@ class ChartSheetTest extends TestCase $spreadsheet = $reader->load($filename); self::assertCount(1, $spreadsheet->getAllSheets()); - $chartSheet = $spreadsheet->getSheetByName('Chart1'); - self::assertNull($chartSheet); + self::assertNull($spreadsheet->getSheetByName('Chart1')); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php index 6b090fe8..57958245 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php @@ -42,28 +42,20 @@ class Issue2501Test extends TestCase $filename = self::$testbook; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('Columns'); + $sheet = $spreadsheet->getSheetByNameOrThrow('Columns'); $expected = [ 'A1:A1048576', 'B1:D1048576', 'E2:E4', ]; - if ($sheet === null) { - self::fail('Unable to find sheet Columns'); - } else { - self::assertSame($expected, array_values($sheet->getMergeCells())); - } - $sheet = $spreadsheet->getSheetByName('Rows'); + self::assertSame($expected, array_values($sheet->getMergeCells())); + $sheet = $spreadsheet->getSheetByNameOrThrow('Rows'); $expected = [ 'A1:XFD1', 'A2:XFD4', 'B5:D5', ]; - if ($sheet === null) { - self::fail('Unable to find sheet Rows'); - } else { - self::assertSame($expected, array_values($sheet->getMergeCells())); - } + self::assertSame($expected, array_values($sheet->getMergeCells())); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php index 1849c5bd..cc2c03dc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php @@ -62,13 +62,9 @@ class NamespaceNonStdTest extends \PHPUnit\Framework\TestCase self::assertSame('A2', $sheet->getFreezePane()); self::assertSame('A2', $sheet->getTopLeftCell()); self::assertSame('B3', $sheet->getSelectedCells()); - $sheet = $spreadsheet->getSheetByName('SylkTest'); - if ($sheet === null) { - self::fail('Unable to load expected sheet'); - } else { - self::assertNull($sheet->getFreezePane()); - self::assertNull($sheet->getTopLeftCell()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('SylkTest'); + self::assertNull($sheet->getFreezePane()); + self::assertNull($sheet->getTopLeftCell()); } public function testLoadXlsx(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php index 1b1743c9..afb36862 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php @@ -100,13 +100,9 @@ class NamespaceOpenpyxl35Test extends \PHPUnit\Framework\TestCase ], ]; foreach ($expectedArray as $sheetName => $array1) { - $sheet = $spreadsheet->getSheetByName($sheetName); - if ($sheet === null) { - self::fail("Unable to find sheet $sheetName"); - } else { - foreach ($array1 as $key => $value) { - self::assertSame($value, self::getCellValue($sheet, $key), "error in sheet $sheetName cell $key"); - } + $sheet = $spreadsheet->getSheetByNameOrThrow($sheetName); + foreach ($array1 as $key => $value) { + self::assertSame($value, self::getCellValue($sheet, $key), "error in sheet $sheetName cell $key"); } } $spreadsheet->disconnectWorksheets(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php index 8b47d141..5f8c8bc3 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php @@ -62,13 +62,9 @@ class NamespaceStdTest extends \PHPUnit\Framework\TestCase self::assertSame('A2', $sheet->getFreezePane()); self::assertSame('A2', $sheet->getTopLeftCell()); self::assertSame('B3', $sheet->getSelectedCells()); - $sheet = $spreadsheet->getSheetByName('SylkTest'); - if ($sheet === null) { - self::fail('Unable to load expected sheet'); - } else { - self::assertNull($sheet->getFreezePane()); - self::assertNull($sheet->getTopLeftCell()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('SylkTest'); + self::assertNull($sheet->getFreezePane()); + self::assertNull($sheet->getTopLeftCell()); } public function testLoadXlsx(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php index 1bbc88ac..12675b41 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php @@ -28,8 +28,7 @@ class PageSetup2Test extends TestCase public function testColumnBreak(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('colbreak'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('colbreak'); $breaks = $sheet->getBreaks(); self::assertCount(1, $breaks); $break = $breaks['D1'] ?? null; diff --git a/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php b/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php index 4b7923d8..c9912b8c 100644 --- a/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php +++ b/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php @@ -10,16 +10,27 @@ class PasswordHasherTest extends TestCase { /** * @dataProvider providerHashPassword - * - * @param mixed $expectedResult */ - public function testHashPassword($expectedResult, ...$args): void - { + public function testHashPassword( + string $expectedResult, + string $password, + ?string $algorithm = null, + ?string $salt = null, + ?int $spinCount = null + ): void { if ($expectedResult === 'exception') { $this->expectException(SpException::class); } - $result = PasswordHasher::hashPassword(...$args); - self::assertEquals($expectedResult, $result); + if ($algorithm === null) { + $result = PasswordHasher::hashPassword($password); + } elseif ($salt === null) { + $result = PasswordHasher::hashPassword($password, $algorithm); + } elseif ($spinCount === null) { + $result = PasswordHasher::hashPassword($password, $algorithm, $salt); + } else { + $result = PasswordHasher::hashPassword($password, $algorithm, $salt, $spinCount); + } + self::assertSame($expectedResult, $result); } public function providerHashPassword(): array diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index 11fb56e4..76e28b3d 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -2,28 +2,38 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Exception as ssException; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class SpreadsheetTest extends TestCase { - /** @var Spreadsheet */ - private $object; + /** @var ?Spreadsheet */ + private $spreadsheet; - protected function setUp(): void + protected function tearDown(): void { - parent::setUp(); - $this->object = new Spreadsheet(); - $sheet = $this->object->getActiveSheet(); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + private function getSpreadsheet(): Spreadsheet + { + $this->spreadsheet = $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('someSheet1'); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); $sheet = new Worksheet(); $sheet->setTitle('someSheet 3'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); + + return $spreadsheet; } public function dataProviderForSheetNames(): array @@ -35,6 +45,7 @@ class SpreadsheetTest extends TestCase [1, "'someSheet2'"], [2, 'someSheet 3'], [2, "'someSheet 3'"], + [null, 'someSheet 33'], ]; return $array; @@ -43,135 +54,153 @@ class SpreadsheetTest extends TestCase /** * @dataProvider dataProviderForSheetNames */ - public function testGetSheetByName(int $index, string $sheetName): void + public function testGetSheetByName(?int $index, string $sheetName): void { - self::assertSame($this->object->getSheet($index), $this->object->getSheetByName($sheetName)); + $spreadsheet = $this->getSpreadsheet(); + if ($index === null) { + self::assertNull($spreadsheet->getSheetByName($sheetName)); + } else { + self::assertSame($spreadsheet->getSheet($index), $spreadsheet->getSheetByName($sheetName)); + } } public function testAddSheetDuplicateTitle(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); } public function testAddSheetNoAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->addSheet($sheet); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); } public function testAddSheetAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet0'); - $this->object->addSheet($sheet, 0); - self::assertEquals(3, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet, 0); + self::assertEquals(3, $spreadsheet->getActiveSheetIndex()); } public function testRemoveSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->removeSheetByIndex(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->removeSheetByIndex(4); } public function testRemoveSheetNoAdjustActive(): void { - $this->object->setActiveSheetIndex(1); - self::assertEquals(1, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(2); - self::assertEquals(1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(1); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(2); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); } public function testRemoveSheetAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(1); - self::assertEquals(1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(1); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); } public function testGetSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->getSheet(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->getSheet(4); } public function testGetIndexNonExistent(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->getIndex($sheet); + $spreadsheet->getIndex($sheet); } public function testSetIndexByName(): void { - $this->object->setIndexByName('someSheet1', 1); - self::assertEquals('someSheet2', $this->object->getSheet(0)->getTitle()); - self::assertEquals('someSheet1', $this->object->getSheet(1)->getTitle()); - self::assertEquals('someSheet 3', $this->object->getSheet(2)->getTitle()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setIndexByName('someSheet1', 1); + self::assertEquals('someSheet2', $spreadsheet->getSheet(0)->getTitle()); + self::assertEquals('someSheet1', $spreadsheet->getSheet(1)->getTitle()); + self::assertEquals('someSheet 3', $spreadsheet->getSheet(2)->getTitle()); } public function testRemoveAllSheets(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(1, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(0, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(-1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(-1, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->addSheet($sheet); - self::assertEquals(0, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); } public function testBug1735(): void { - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $spreadsheet->createSheet()->setTitle('addedsheet'); - $spreadsheet->setActiveSheetIndex(1); - $spreadsheet->removeSheetByIndex(0); - $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet1 = new Spreadsheet(); + $spreadsheet1->createSheet()->setTitle('addedsheet'); + $spreadsheet1->setActiveSheetIndex(1); + $spreadsheet1->removeSheetByIndex(0); + $sheet = $spreadsheet1->getActiveSheet(); self::assertEquals('addedsheet', $sheet->getTitle()); } public function testSetActiveSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->setActiveSheetIndex(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->setActiveSheetIndex(4); } public function testSetActiveSheetNoSuchName(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->setActiveSheetIndexByName('unknown'); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->setActiveSheetIndexByName('unknown'); } public function testAddExternal(): void { - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $sheet = $spreadsheet->createSheet()->setTitle('someSheet19'); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet1 = new Spreadsheet(); + $sheet = $spreadsheet1->createSheet()->setTitle('someSheet19'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); $sheet->getCell('B1')->getStyle()->getFont()->setSuperscript(true); $sheet->getCell('C1')->getStyle()->getFont()->setSubscript(true); - self::assertCount(4, $spreadsheet->getCellXfCollection()); + self::assertCount(4, $spreadsheet1->getCellXfCollection()); self::assertEquals(1, $sheet->getCell('A1')->getXfIndex()); - $this->object->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); - self::assertCount(2, $this->object->getCellXfCollection()); - $sheet3 = $this->object->addExternalSheet($sheet); - self::assertCount(6, $this->object->getCellXfCollection()); + $spreadsheet->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); + self::assertCount(2, $spreadsheet->getCellXfCollection()); + $sheet3 = $spreadsheet->addExternalSheet($sheet); + self::assertCount(6, $spreadsheet->getCellXfCollection()); self::assertEquals('someSheet19', $sheet3->getTitle()); self::assertEquals(1, $sheet3->getCell('A1')->getValue()); self::assertTrue($sheet3->getCell('A1')->getStyle()->getFont()->getBold()); @@ -181,17 +210,17 @@ class SpreadsheetTest extends TestCase public function testAddExternalDuplicateName(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $this->expectException(ssException::class); + $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->createSheet()->setTitle('someSheet1'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); - $this->object->addExternalSheet($sheet); + $spreadsheet->addExternalSheet($sheet); } public function testAddExternalColumnDimensionStyles(): void { - $spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet1 = new Spreadsheet(); $sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension'); $sheet1->getCell('A1')->setValue(1); $sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true); @@ -200,7 +229,7 @@ class SpreadsheetTest extends TestCase self::assertEquals(1, $index); self::assertCount(2, $spreadsheet1->getCellXfCollection()); - $spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet2 = new Spreadsheet(); $sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles'); $sheet2->getCell('A1')->setValue(1); $sheet2->getCell('A1')->getStyle()->getFont()->setBold(true); @@ -220,7 +249,7 @@ class SpreadsheetTest extends TestCase public function testAddExternalRowDimensionStyles(): void { - $spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet1 = new Spreadsheet(); $sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension'); $sheet1->getCell('A1')->setValue(1); $sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true); @@ -229,7 +258,7 @@ class SpreadsheetTest extends TestCase self::assertEquals(1, $index); self::assertCount(2, $spreadsheet1->getCellXfCollection()); - $spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet2 = new Spreadsheet(); $sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles'); $sheet2->getCell('A1')->setValue(1); $sheet2->getCell('A1')->getStyle()->getFont()->setBold(true); diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 2c6d0da8..decbad5b 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -2,9 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Style\ConditionalFormatting; +use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Exception as ssException; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellMatcher; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase @@ -30,18 +33,26 @@ class CellMatcherTest extends TestCase } } + private function confirmString(Worksheet $worksheet, Cell $cell, string $cellAddress): string + { + $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()) ?? ''; + if ($cfRange === '') { + self::fail("{$cellAddress} is not in a Conditional Format range"); + } + + return $cfRange; + } + /** * @dataProvider basicCellIsComparisonDataProvider */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "{$cellAddress} is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -83,18 +94,35 @@ class CellMatcherTest extends TestCase ]; } + public function testNotInRange(): void + { + $this->spreadsheet = $this->loadSpreadsheet(); + $sheetname = 'cellIs Comparison'; + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); + $cell = $worksheet->getCell('J20'); + + $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); + self::assertNull($cfRange); + } + + public function testUnknownSheet(): void + { + $this->expectException(ssException::class); + $this->spreadsheet = $this->loadSpreadsheet(); + $sheetname = 'cellIs Comparisonxxx'; + $this->spreadsheet->getSheetByNameOrThrow($sheetname); + } + /** * @dataProvider rangeCellIsComparisonDataProvider */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -132,12 +160,10 @@ class CellMatcherTest extends TestCase public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -168,12 +194,10 @@ class CellMatcherTest extends TestCase public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -214,12 +238,10 @@ class CellMatcherTest extends TestCase public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -324,12 +346,10 @@ class CellMatcherTest extends TestCase public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -357,12 +377,10 @@ class CellMatcherTest extends TestCase public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -389,12 +407,10 @@ class CellMatcherTest extends TestCase public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -433,12 +449,10 @@ class CellMatcherTest extends TestCase public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::AssertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -469,12 +483,10 @@ class CellMatcherTest extends TestCase public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php index c35c626e..2d4991a4 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php @@ -24,9 +24,9 @@ class WizardFactoryTest extends TestCase /** * @dataProvider basicWizardFactoryProvider * - * @param class-string $expectedWizard + * @psalm-param class-string $expectedWizard */ - public function testBasicWizardFactory(string $ruleType, $expectedWizard): void + public function testBasicWizardFactory(string $ruleType, string $expectedWizard): void { $wizard = $this->wizardFactory->newRule($ruleType); self::assertInstanceOf($expectedWizard, $wizard); @@ -54,10 +54,7 @@ class WizardFactoryTest extends TestCase $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); - $worksheet = $spreadsheet->getSheetByName($sheetName); - if ($worksheet === null) { - self::markTestSkipped("{$sheetName} not found in test workbook"); - } + $worksheet = $spreadsheet->getSheetByNameOrThrow($sheetName); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php index 7de39328..8c4aa1b9 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php @@ -79,7 +79,7 @@ class VisibilityTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); foreach ($visibleSheets as $sheetName => $visibility) { - $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByNameOrThrow($sheetName); self::assertSame($visibility, $reloadedWorksheet->getSheetState()); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php index f9535714..eba7288f 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php @@ -77,19 +77,15 @@ class UnparsedDataCloneTest extends TestCase $reader1 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet1 = $reader1->load($resultFilename1); unlink($resultFilename1); - $sheet1c = $spreadsheet1->getSheetByName('Clone'); - self::assertNotNull($sheet1c); - $sheet1o = $spreadsheet1->getSheetByName('Original'); - self::assertNotNull($sheet1o); + $sheet1c = $spreadsheet1->getSheetByNameOrThrow('Clone'); + $sheet1o = $spreadsheet1->getSheetByNameOrThrow('Original'); $writer->save($resultFilename2); $reader2 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet2 = $reader2->load($resultFilename2); unlink($resultFilename2); - $sheet2c = $spreadsheet2->getSheetByName('Clone'); - self::assertNotNull($sheet2c); - $sheet2o = $spreadsheet2->getSheetByName('Original'); - self::assertNotNull($sheet2o); + $sheet2c = $spreadsheet2->getSheetByNameOrThrow('Clone'); + $sheet2o = $spreadsheet2->getSheetByNameOrThrow('Original'); self::assertEquals($spreadsheet1->getSheetCount(), $spreadsheet2->getSheetCount()); self::assertCount(1, $sheet1c->getDrawingCollection()); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php index 7e1ca967..ec2534fd 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php @@ -79,7 +79,7 @@ class VisibilityTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); foreach ($visibleSheets as $sheetName => $visibility) { - $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByNameOrThrow($sheetName); self::assertSame($visibility, $reloadedWorksheet->getSheetState()); } } diff --git a/tests/data/Calculation/Financial/IRR.php b/tests/data/Calculation/Financial/IRR.php index f6c24c13..5051182a 100644 --- a/tests/data/Calculation/Financial/IRR.php +++ b/tests/data/Calculation/Financial/IRR.php @@ -83,4 +83,5 @@ return [ -21000, ], ], + 'no arguments' => ['exception'], ]; From 6c1651e9955982186be45f578ce9c94cb61bfce4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:05:01 -0700 Subject: [PATCH 114/156] Floating-point Equality in Two Tests (#3064) * Floating-point Equality in Two Tests Merging a change today, Git reported failures that did not occur during "normal" unit testing. The merge still succeeded, but ... The problem was an error comparing float values for equal, and the inequality occurred beyond the 14th decimal digit. Change the tests in question, which incidentally were not part of the merged changed, to use assertEqualsWithDelta. * Egad - 112 More Precision-related Problems Spread across 9 test members. --- .../Functions/Engineering/ConvertUoMTest.php | 10 +++------- .../Calculation/Functions/Engineering/ErfCTest.php | 9 +-------- .../Functions/Engineering/ErfPreciseTest.php | 9 +-------- .../Calculation/Functions/Engineering/ErfTest.php | 9 +-------- .../Calculation/Functions/MathTrig/FactTest.php | 6 ++++-- .../Calculation/Functions/MathTrig/MUnitTest.php | 4 +++- .../Calculation/Functions/TextData/NumberValueTest.php | 6 ++++-- .../Cell/AdvancedValueBinderTest.php | 4 +++- tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php | 10 ++++++---- tests/PhpSpreadsheetTests/Shared/FontTest.php | 6 ++++-- .../Shared/Trend/LinearBestFitTest.php | 10 ++++++---- 11 files changed, 36 insertions(+), 47 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php index 9a448824..f15725cb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php @@ -4,15 +4,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ConvertUoMTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } + const UOM_PRECISION = 1E-12; public function testGetConversionGroups(): void { @@ -52,7 +48,7 @@ class ConvertUoMTest extends TestCase public function testCONVERTUOM($expectedResult, ...$args): void { $result = Engineering::CONVERTUOM(...$args); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::UOM_PRECISION); } public function providerCONVERTUOM(): array @@ -69,7 +65,7 @@ class ConvertUoMTest extends TestCase $formula = "=CONVERT({$value}, {$fromUoM}, {$toUoM})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::UOM_PRECISION); } public function providerConvertUoMArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php index f0a721c7..88e808ab 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfCTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERFC * @@ -25,7 +19,6 @@ class ErfCTest extends TestCase public function testERFC($expectedResult, $lower): void { $result = Engineering::ERFC($lower); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -43,7 +36,7 @@ class ErfCTest extends TestCase $formula = "=ERFC({$lower})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfCArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php index dc3ee84c..8a069ff1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfPreciseTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERFPRECISE * @@ -25,7 +19,6 @@ class ErfPreciseTest extends TestCase public function testERFPRECISE($expectedResult, $limit): void { $result = Engineering::ERFPRECISE($limit); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -43,7 +36,7 @@ class ErfPreciseTest extends TestCase $formula = "=ERF.PRECISE({$limit})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfPreciseArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php index 4d13d47d..26e0381c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERF * @@ -26,7 +20,6 @@ class ErfTest extends TestCase public function testERF($expectedResult, $lower, $upper = null): void { $result = Engineering::ERF($lower, $upper); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -44,7 +37,7 @@ class ErfTest extends TestCase $formula = "=ERF({$lower}, {$upper})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php index 328773e0..6bdfa570 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class FactTest extends AllSetupTeardown { + const FACT_PRECISION = 1E-12; + /** * @dataProvider providerFACT * @@ -53,7 +55,7 @@ class FactTest extends AllSetupTeardown $sheet->getCell('B1')->setValue('=FACT(A1)'); } $result = $sheet->getCell('B1')->getCalculatedValue(); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FACT_PRECISION); } public function providerFACTGnumeric(): array @@ -70,7 +72,7 @@ class FactTest extends AllSetupTeardown $formula = "=FACT({$array})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::FACT_PRECISION); } public function providerFactArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php index 96ebc45b..9ac68ee5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\MatrixFunctions; class MUnitTest extends AllSetupTeardown { + const MU_PRECISION = 1.0E-12; + public function testMUNIT(): void { $identity = MatrixFunctions::identity(3); @@ -15,7 +17,7 @@ class MUnitTest extends AllSetupTeardown self::assertEquals($startArray, $resultArray); $inverseArray = MatrixFunctions::inverse($startArray); $resultArray = MatrixFunctions::multiply($startArray, $inverseArray); - self::assertEquals($identity, $resultArray); + self::assertEqualsWithDelta($identity, $resultArray, self::MU_PRECISION); self::assertEquals('#VALUE!', MatrixFunctions::identity(0)); self::assertEquals('#VALUE!', MatrixFunctions::identity(-1)); self::assertEquals('#VALUE!', MatrixFunctions::identity('X')); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php index a100695f..1a909932 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class NumberValueTest extends AllSetupTeardown { + const NV_PRECISION = 1.0E-8; + /** * @dataProvider providerNUMBERVALUE * @@ -34,7 +36,7 @@ class NumberValueTest extends AllSetupTeardown $sheet->getCell('B1')->setValue('=NUMBERVALUE(A1, A2, A3)'); } $result = $sheet->getCell('B1')->getCalculatedValue(); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::NV_PRECISION); } public function providerNUMBERVALUE(): array @@ -51,7 +53,7 @@ class NumberValueTest extends AllSetupTeardown $formula = "=NumberValue({$argument1}, {$argument2}, {$argument3})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::NV_PRECISION); } public function providerNumberValueArray(): array diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index 34ff2121..54f3e0cd 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -11,6 +11,8 @@ use PHPUnit\Framework\TestCase; class AdvancedValueBinderTest extends TestCase { + const AVB_PRECISION = 1.0E-8; + /** * @var string */ @@ -161,7 +163,7 @@ class AdvancedValueBinderTest extends TestCase $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->getCell('A1')->setValue($value); - self::assertEquals($valueBinded, $sheet->getCell('A1')->getValue()); + self::assertEqualsWithDelta($valueBinded, $sheet->getCell('A1')->getValue(), self::AVB_PRECISION); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php index e1271b9a..6b0ebf67 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase; class XlsxTest extends TestCase { + const XLSX_PRECISION = 1.0E-8; + public function testLoadXlsxRowColumnAttributes(): void { $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; @@ -133,10 +135,10 @@ class XlsxTest extends TestCase $pageMargins = $worksheet->getPageMargins(); // Convert from inches to cm for testing - self::assertEquals(2.5, $pageMargins->getTop() * 2.54); - self::assertEquals(3.3, $pageMargins->getLeft() * 2.54); - self::assertEquals(3.3, $pageMargins->getRight() * 2.54); - self::assertEquals(1.3, $pageMargins->getHeader() * 2.54); + self::assertEqualsWithDelta(2.5, $pageMargins->getTop() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(3.3, $pageMargins->getLeft() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(3.3, $pageMargins->getRight() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(1.3, $pageMargins->getHeader() * 2.54, self::XLSX_PRECISION); self::assertEquals(PageSetup::PAPERSIZE_A4, $worksheet->getPageSetup()->getPaperSize()); self::assertEquals(['A10', 'A20', 'A30', 'A40', 'A50'], array_keys($worksheet->getBreaks())); diff --git a/tests/PhpSpreadsheetTests/Shared/FontTest.php b/tests/PhpSpreadsheetTests/Shared/FontTest.php index c733f8ee..2d5feb6e 100644 --- a/tests/PhpSpreadsheetTests/Shared/FontTest.php +++ b/tests/PhpSpreadsheetTests/Shared/FontTest.php @@ -8,6 +8,8 @@ use PHPUnit\Framework\TestCase; class FontTest extends TestCase { + const FONT_PRECISION = 1.0E-12; + public function testGetAutoSizeMethod(): void { $expectedResult = Font::AUTOSIZE_METHOD_APPROX; @@ -63,7 +65,7 @@ class FontTest extends TestCase public function testInchSizeToPixels($expectedResult, $size): void { $result = Font::inchSizeToPixels($size); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FONT_PRECISION); } public function providerInchSizeToPixels(): array @@ -80,7 +82,7 @@ class FontTest extends TestCase public function testCentimeterSizeToPixels($expectedResult, $size): void { $result = Font::centimeterSizeToPixels($size); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FONT_PRECISION); } public function providerCentimeterSizeToPixels(): array diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php index 9ada87a5..34321227 100644 --- a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php +++ b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php @@ -7,6 +7,8 @@ use PHPUnit\Framework\TestCase; class LinearBestFitTest extends TestCase { + const LBF_PRECISION = 1.0E-8; + /** * @dataProvider providerLinearBestFit * @@ -27,13 +29,13 @@ class LinearBestFitTest extends TestCase ): void { $bestFit = new LinearBestFit($yValues, $xValues); $slope = $bestFit->getSlope(1); - self::assertEquals($expectedSlope[0], $slope); + self::assertEqualsWithDelta($expectedSlope[0], $slope, self::LBF_PRECISION); $slope = $bestFit->getSlope(); - self::assertEquals($expectedSlope[1], $slope); + self::assertEqualsWithDelta($expectedSlope[1], $slope, self::LBF_PRECISION); $intersect = $bestFit->getIntersect(1); - self::assertEquals($expectedIntersect[0], $intersect); + self::assertEqualsWithDelta($expectedIntersect[0], $intersect, self::LBF_PRECISION); $intersect = $bestFit->getIntersect(); - self::assertEquals($expectedIntersect[1], $intersect); + self::assertEqualsWithDelta($expectedIntersect[1], $intersect, self::LBF_PRECISION); $equation = $bestFit->getEquation(2); self::assertEquals($expectedEquation, $equation); From 8513c6418c61b41eebda73c4b9faaad85dd273bf Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 14 Sep 2022 10:12:53 -0700 Subject: [PATCH 115/156] Memory Leak in Sample35 (#3062) * Memory Leak in Sample35 All but 6 chart samples can be rendered by Sample35. Of those 6, 3 of the problems are because the script runs out of memory processing them. Adopting a suggestion from @MAKS-dev in issue #2092, adding a call to gc_collect_cycles after the charts from each spreadsheet is rendered appears to make it possible to include those 3 spreadsheets in Sample35 after all. Also take advantage of this opportunity to correct a number (hopefully all) of Scrutinizer problems with JpgraphRendererBases. * Minor Fix Problem running 8.1 unit tests. * Resolve Problems with Pie 3D Charts Minor fix, leaving only one spreadsheet unusable in Sample35. The reasons for its unusability are now documented in the code. * Mitoteam Made Changes Discussing this problem with them, they decided they should make a change for Pie3D rather than forcing us to use the workaround pushed earlier. Change to require mitoteam 10.2.3, revert workaround. --- composer.json | 2 +- composer.lock | 16 ++--- samples/Chart/35_Chart_render.php | 13 ++-- .../Chart/Renderer/JpGraphRendererBase.php | 61 +++++++------------ 4 files changed, 38 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 6835b05b..a4b033cd 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "10.2.2", + "mitoteam/jpgraph": "10.2.3", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 4097420d..508733ee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8512207f173cb137bc2085b7fdf698bc", + "content-hash": "b5bdb9f96d18ce59557436521053fdd9", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1342,16 +1342,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.2.2", + "version": "10.2.3", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf" + "reference": "21121535537e05c32e7964327b80746462a6057d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", - "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/21121535537e05c32e7964327b80746462a6057d", + "reference": "21121535537e05c32e7964327b80746462a6057d", "shasum": "" }, "require": { @@ -1375,16 +1375,16 @@ "name": "JpGraph team" } ], - "description": "Composer compatible version of JpGraph library with PHP 8.2 support", + "description": "JpGraph library composer package with PHP 8.2 support", "homepage": "https://github.com/mitoteam/jpgraph", "keywords": [ "jpgraph" ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.2.2" + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.3" }, - "time": "2022-09-09T08:16:10+00:00" + "time": "2022-09-14T04:02:09+00:00" }, { "name": "mpdf/mpdf", diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 891cd27c..f6dfeb46 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -25,12 +25,10 @@ if (count($inputFileNames) === 1) { $unresolvedErrors = []; } else { $unresolvedErrors = [ + // The following spreadsheet was created by 3rd party software, + // and doesn't include the data that usually accompanies a chart. + // That is good enough for Excel, but not for JpGraph. '32readwriteBubbleChart2.xlsx', - '32readwritePieChart3.xlsx', - '32readwritePieChart4.xlsx', - '32readwritePieChart3D1.xlsx', - '32readwritePieChartExploded1.xlsx', - '32readwritePieChartExploded3D1.xlsx', ]; } foreach ($inputFileNames as $inputFileName) { @@ -42,7 +40,9 @@ foreach ($inputFileNames as $inputFileName) { continue; } if (in_array($inputFileNameShort, $unresolvedErrors, true)) { - $helper->log('File ' . $inputFileNameShort . ' does not yet work with this script'); + $helper->log('*****'); + $helper->log('***** File ' . $inputFileNameShort . ' does not yet work with this script'); + $helper->log('*****'); continue; } @@ -92,6 +92,7 @@ foreach ($inputFileNames as $inputFileName) { $spreadsheet->disconnectWorksheets(); unset($spreadsheet); + gc_collect_cycles(); } $helper->log('Done rendering charts as images'); diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index d31a55b3..cb9b544b 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -102,13 +102,11 @@ abstract class JpGraphRendererBase implements IRenderer return $seriesPlot; } - private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') + private function formatDataSetLabels($groupID, $datasetLabels, $rotation = '') { - $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); - if ($datasetLabelFormatCode !== null) { - // Retrieve any label formatting code - $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); - } + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? ''; + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); $testCurrentIndex = 0; foreach ($datasetLabels as $i => $datasetLabel) { @@ -273,7 +271,7 @@ abstract class JpGraphRendererBase implements IRenderer $this->renderTitle(); } - private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void + private function renderPlotLine($groupID, $filled = false, $combination = false): void { $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); @@ -281,7 +279,7 @@ abstract class JpGraphRendererBase implements IRenderer $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); $this->graph->xaxis->SetTickLabels($datasetLabels); } @@ -353,7 +351,7 @@ abstract class JpGraphRendererBase implements IRenderer $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation); // Rotate for bar rather than column chart if ($rotation == 'bar') { $datasetLabels = array_reverse($datasetLabels); @@ -430,11 +428,9 @@ abstract class JpGraphRendererBase implements IRenderer private function renderPlotScatter($groupID, $bubble): void { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { @@ -478,7 +474,6 @@ abstract class JpGraphRendererBase implements IRenderer $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { @@ -513,15 +508,11 @@ abstract class JpGraphRendererBase implements IRenderer private function renderPlotContour($groupID): void { - $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; $dataValues = []; // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); $dataValues[$i] = $dataValuesX; @@ -565,7 +556,7 @@ abstract class JpGraphRendererBase implements IRenderer $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); $this->graph->xaxis->SetTickLabels($datasetLabels); } @@ -575,21 +566,21 @@ abstract class JpGraphRendererBase implements IRenderer $this->graph->Add($seriesPlot); } - private function renderAreaChart($groupCount, $dimensions = '2d'): void + private function renderAreaChart($groupCount): void { $this->renderCartesianPlotArea(); for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, true, false, $dimensions); + $this->renderPlotLine($i, true, false); } } - private function renderLineChart($groupCount, $dimensions = '2d'): void + private function renderLineChart($groupCount): void { $this->renderCartesianPlotArea(); for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, false, false, $dimensions); + $this->renderPlotLine($i, false, false); } } @@ -626,19 +617,17 @@ abstract class JpGraphRendererBase implements IRenderer $iLimit = ($multiplePlots) ? $groupCount : 1; for ($groupID = 0; $groupID < $iLimit; ++$groupID) { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); $datasetLabels = []; if ($groupID == 0) { $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); } } $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; // For pie charts, we only display the first series: doughnut charts generally display all series $jLimit = ($multiplePlots) ? $seriesCount : 1; // Loop through each data series in turn @@ -669,7 +658,7 @@ abstract class JpGraphRendererBase implements IRenderer $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); } - if ($doughnut) { + if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) { $seriesPlot->SetMidColor('white'); } @@ -710,7 +699,7 @@ abstract class JpGraphRendererBase implements IRenderer } } - private function renderContourChart($groupCount, $dimensions): void + private function renderContourChart($groupCount): void { $this->renderCartesianPlotArea('intint'); @@ -719,7 +708,7 @@ abstract class JpGraphRendererBase implements IRenderer } } - private function renderCombinationChart($groupCount, $dimensions, $outputDestination) + private function renderCombinationChart($groupCount, $outputDestination) { $this->renderCartesianPlotArea(); @@ -728,10 +717,8 @@ abstract class JpGraphRendererBase implements IRenderer $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); switch ($chartType) { case 'area3DChart': - $dimensions = '3d'; - // no break case 'areaChart': - $this->renderPlotLine($i, true, true, $dimensions); + $this->renderPlotLine($i, true, true); break; case 'bar3DChart': @@ -742,10 +729,8 @@ abstract class JpGraphRendererBase implements IRenderer break; case 'line3DChart': - $dimensions = '3d'; - // no break case 'lineChart': - $this->renderPlotLine($i, false, true, $dimensions); + $this->renderPlotLine($i, false, true); break; case 'scatterChart': @@ -792,7 +777,7 @@ abstract class JpGraphRendererBase implements IRenderer return false; } else { - return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); + return $this->renderCombinationChart($groupCount, $outputDestination); } } @@ -801,7 +786,7 @@ abstract class JpGraphRendererBase implements IRenderer $dimensions = '3d'; // no break case 'areaChart': - $this->renderAreaChart($groupCount, $dimensions); + $this->renderAreaChart($groupCount); break; case 'bar3DChart': @@ -815,7 +800,7 @@ abstract class JpGraphRendererBase implements IRenderer $dimensions = '3d'; // no break case 'lineChart': - $this->renderLineChart($groupCount, $dimensions); + $this->renderLineChart($groupCount); break; case 'pie3DChart': @@ -845,10 +830,8 @@ abstract class JpGraphRendererBase implements IRenderer break; case 'surface3DChart': - $dimensions = '3d'; - // no break case 'surfaceChart': - $this->renderContourChart($groupCount, $dimensions); + $this->renderContourChart($groupCount); break; case 'stockChart': From 8ecf69a5c4469111ed5adb58421e442f5eadfdbe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 15 Sep 2022 21:07:31 +0200 Subject: [PATCH 116/156] Handle additional merge options like those provide in OpenOffice or LibreOffice to hide cell values in a merge range rather than empty them, or to merge the values as well as the cells This includes reading hidden values in merge ranges, so that Unmerging can restore their visibility --- CHANGELOG.md | 6 +- .../images/12-01-MergeCells-Options-2.png | Bin 0 -> 3313 bytes .../images/12-01-MergeCells-Options-3.png | Bin 0 -> 5029 bytes .../images/12-01-MergeCells-Options.png | Bin 0 -> 22477 bytes docs/topics/recipes.md | 60 ++++++- src/PhpSpreadsheet/Reader/Gnumeric.php | 2 +- src/PhpSpreadsheet/Reader/Ods.php | 3 +- src/PhpSpreadsheet/Reader/Xls.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 3 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 76 ++++++-- .../Worksheet/MergeBehaviourTest.php | 166 ++++++++++++++++++ 12 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 docs/topics/images/12-01-MergeCells-Options-2.png create mode 100644 docs/topics/images/12-01-MergeCells-Options-3.png create mode 100644 docs/topics/images/12-01-MergeCells-Options.png create mode 100644 tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc17d852..f15e9d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed -- Nothing +- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) + - Merge methods now allow an additional `$behaviour` argument. Permitted values are: + - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) + - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell ### Deprecated diff --git a/docs/topics/images/12-01-MergeCells-Options-2.png b/docs/topics/images/12-01-MergeCells-Options-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5e745fc934dc88c114b39dc1a45a48ddd88ad0aa GIT binary patch literal 3313 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 literal 0 HcmV?d00001 diff --git a/docs/topics/images/12-01-MergeCells-Options-3.png b/docs/topics/images/12-01-MergeCells-Options-3.png new file mode 100644 index 0000000000000000000000000000000000000000..30ad346eac43344b9200a9c49928209a71663e35 GIT binary patch literal 5029 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 Qf|tw0@Tft#zFXY?0K=D0BLDyZ literal 0 HcmV?d00001 diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index afa3dcc8..04a4e6aa 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1332,22 +1332,72 @@ rows (default), or above. The following code adds the summary above: $spreadsheet->getActiveSheet()->setShowSummaryBelow(false); ``` -## Merge/unmerge cells +## Merge/Unmerge cells -If you have a big piece of data you want to display in a worksheet, you -can merge two or more cells together, to become one cell. This can be -done using the following code: +If you have a big piece of data you want to display in a worksheet, or a +heading that needs to span multiple sub-heading columns, you can merge +two or more cells together, to become one cell. This can be done using +the following code: ```php $spreadsheet->getActiveSheet()->mergeCells('A18:E22'); ``` -Removing a merge can be done using the unmergeCells method: +Removing a merge can be done using the `unmergeCells()` method: ```php $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` +MS Excel itself doesn't yet offer the functionality to simply hide the merged cells, or to merge the content of cells into a single cell, but it is available in Open/Libre Office. + +### Merge with MERGE_CELL_CONTENT_EMPTY + +The default behaviour is to empty all cells except for the top-left corner cell in the merge range; and this is also the default behaviour for the `mergeCells()` method in PhpSpreadsheet. +When this behaviour is applied, those cell values will be set to null; and if they are subsequently Unmerged, they will be empty cells. + +Passing an extra flag value to the `mergeCells()` method in PhpSpreadsheet can change this behaviour. + +![12-01-MergeCells-Options.png](./images/12-01-MergeCells-Options.png) + +Possible flag values are: +- Worksheet::MERGE_CELL_CONTENT_EMPTY (the default) +- Worksheet::MERGE_CELL_CONTENT_HIDE +- Worksheet::MERGE_CELL_CONTENT_MERGE + +### Merge with MERGE_CELL_CONTENT_HIDE + +The first alternative, available only in OpenOffice, is to hide those cells, but to leave their content intact. +When a file saved as `Xlsx` in those applications is opened in MS Excel, and those cells are unmerged, the original content will still be present. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_HIDE); +``` + +Will replicate that behaviour. + +### Merge with MERGE_CELL_CONTENT_MERGE + +The second alternative, available in both OpenOffice and LibreOffice is to merge the content of every cell in the merge range into the top-left cell, while setting those hidden cells to empty. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_MERGE); +``` + +Particularly when the merged cells contain formulae, the logic for this merge seems strange: +walking through the merge range, each cell is calculated in turn, and appended to the "master" cell, then it is emptied, so any subsequent calculations that reference the cell see an empty cell, not the pre-merge value. +For example, suppose our spreadsheet contains + +![12-01-MergeCells-Options-2.png](./images/12-01-MergeCells-Options-2.png) + +where `B2` is the formula `=5-B1` and `C2` is the formula `=A2/B2`, +and we want to merge cells `A2` to `C2` with all the cell values merged. +The result is: + +![12-01-MergeCells-Options-3.png](./images/12-01-MergeCells-Options-3.png) + +The cell value `12` from cell `A2` is fixed; the value from `B2` is the result of the formula `=5-B1` (`4`, which is appended to our merged value), and cell `B2` is then emptied, so when we evaluate cell `C2` with the formula `=A2/B2` it gives us `12 / 0` which results in a `#DIV/0!` error (so the error `#DIV/0!` is appended to our merged value rather than the original calculation result of `3`). + ## Inserting or Removing rows/columns You can insert/remove rows/columns at a specific position. The following diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ca087e61..1dcb0a12 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -363,7 +363,7 @@ class Gnumeric extends BaseReader if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos((string) $mergeCells, ':') !== false) { - $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 7e776ab7..e3de4731 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -20,6 +20,7 @@ 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; @@ -759,7 +760,7 @@ class Ods extends BaseReader } $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 71496ece..a8de5228 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4585,7 +4585,7 @@ class Xls extends BaseReader (strpos($cellRangeAddress, ':') !== false) && ($this->includeCellRangeFiltered($cellRangeAddress)) ) { - $this->phpSheet->mergeCells($cellRangeAddress); + $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index fc38375a..26cd1af3 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -914,7 +914,7 @@ class Xlsx extends BaseReader foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { $mergeRef = (string) $mergeCell['ref']; if (strpos($mergeRef, ':') !== false) { - $docSheet->mergeCells((string) $mergeCell['ref']); + $docSheet->mergeCells((string) $mergeCell['ref'], Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 0b5e0966..d8f0d9dc 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; /** @@ -364,7 +365,7 @@ class Xml extends BaseReader $rowTo = $rowTo + $cell_ss['MergeDown']; } $cellRange .= ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } $hasCalculatedValue = false; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d13d4141..55327932 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -41,6 +41,10 @@ class Worksheet implements IComparable public const SHEETSTATE_HIDDEN = 'hidden'; public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const MERGE_CELL_CONTENT_EMPTY = 'empty'; + public const MERGE_CELL_CONTENT_HIDE = 'hide'; + public const MERGE_CELL_CONTENT_MERGE = 'merge'; + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** @@ -1758,10 +1762,15 @@ class Worksheet implements IComparable * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCells($range) + public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); @@ -1793,18 +1802,22 @@ class Worksheet implements IComparable $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); } - // Blank out the rest of the cells in the range (if they exist) - if ($numberRows > $numberColumns) { - $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); - } else { - $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); + if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) { + // Blank out the rest of the cells in the range (if they exist) + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour); + } else { + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour); + } } return $this; } - private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) { $iterator = $column->getCellIterator($firstRow); $iterator->setIterateOnlyExistingCells(true); @@ -1814,17 +1827,21 @@ class Worksheet implements IComparable if ($row > $lastRow) { break; } - $thisCell = $cell->getColumn() . $row; - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } } - private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getRowIterator($firstRow, $lastRow) as $row) { $iterator = $row->getCellIterator($firstColumn); $iterator->setIterateOnlyExistingCells(true); @@ -1835,13 +1852,31 @@ class Worksheet implements IComparable if ($columnIndex > $lastColumnIndex) { break; } - $thisCell = $column . $cell->getRow(); - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } + } + + public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array + { + if ($cell->getCoordinate() !== $upperLeft) { + Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance(); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $cellValue = $cell->getFormattedValue(); + if ($cellValue !== '') { + $leftCellValue[] = $cellValue; + } + } + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + + return $leftCellValue; } /** @@ -1856,17 +1891,22 @@ class Worksheet implements IComparable * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $row2 Numeric row coordinate of the last cell + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $cellRange = new CellRange( CellAddress::fromColumnAndRow($columnIndex1, $row1), CellAddress::fromColumnAndRow($columnIndex2, $row2) ); - return $this->mergeCells($cellRange); + return $this->mergeCells($cellRange, $behaviour); } /** diff --git a/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php new file mode 100644 index 00000000..68c9d87a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php @@ -0,0 +1,166 @@ +getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsDefaultBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviour(): void + { + $expectedResult = [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 9.9], + [5.5, 7.7, 13.2], + ]; + + $mergeRange = 'A1:C3'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', '2022-09-15'], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + /** + * @dataProvider mergeCellsMergeBehaviourProvider + */ + public function testMergeCellsMergeBehaviour(array $testData, string $mergeRange, array $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($testData, null, 'A1', true); + // Force a precalculation to populate the calculation cache, so that we can verify that it is being cleared + $worksheet->toArray(); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function mergeCellsMergeBehaviourProvider(): array + { + return [ + 'With Calculated Values' => [ + $this->testDataRaw, + 'A1:C3', + [ + ['1.1 2.2 1.1 4.4 5.5 0 1.1 0 0', null, null], + [null, null, null], + [null, null, null], + ], + ], + 'With Empty Cells' => [ + [ + [1, '', 2], + [null, 3, null], + [4, null, 5], + ], + 'A1:C3', + [ + ['1 2 3 4 5', null, null], + [null, null, null], + [null, null, null], + ], + ], + [ + [ + [12, '=5+1', '=A1/A2'], + ], + 'A1:C1', + [ + ['12 6 #DIV/0!', null, null], + ], + ], + ]; + } + + public function testMergeCellsMergeBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19 2022-09-15', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } +} From 84d6d983482adfc094cca24087cac454244c2d11 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 16 Sep 2022 01:55:36 +0200 Subject: [PATCH 117/156] Unit tests for correctly handling hidden merged cells in Readers --- .../Reader/Ods/HiddenMergeCellsTest.php | 48 ++++++++++++++++++ .../Reader/Xls/HiddenMergeCellsTest.php | 48 ++++++++++++++++++ .../Reader/Xlsx/HiddenMergeCellsTest.php | 48 ++++++++++++++++++ .../data/Reader/Ods/HiddenMergeCellsTest.ods | Bin 0 -> 8045 bytes .../data/Reader/XLS/HiddenMergeCellsTest.xls | Bin 0 -> 5632 bytes .../Reader/XLSX/HiddenMergeCellsTest.xlsx | Bin 0 -> 4576 bytes 6 files changed, 144 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php create mode 100644 tests/data/Reader/Ods/HiddenMergeCellsTest.ods create mode 100644 tests/data/Reader/XLS/HiddenMergeCellsTest.xls create mode 100644 tests/data/Reader/XLSX/HiddenMergeCellsTest.xlsx diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php new file mode 100644 index 00000000..710b292a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php new file mode 100644 index 00000000..c7085281 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php new file mode 100644 index 00000000..4919cd27 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/data/Reader/Ods/HiddenMergeCellsTest.ods b/tests/data/Reader/Ods/HiddenMergeCellsTest.ods new file mode 100644 index 0000000000000000000000000000000000000000..b8beb972c568a6702fa7e36c61d245776fa8c798 GIT binary patch literal 8045 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLS/HiddenMergeCellsTest.xls b/tests/data/Reader/XLS/HiddenMergeCellsTest.xls new file mode 100644 index 0000000000000000000000000000000000000000..fefbcecc802d7aafa01a4321bd6667e1a9adcfc5 GIT binary patch literal 5632 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 Date: Fri, 16 Sep 2022 08:25:26 -0700 Subject: [PATCH 118/156] R1C1 Format and Internationalization, plus Relative Offsets (#3052) * R1C1 Format and Internationalization, plus Relative Offsets Fix #1704, albeit imperfectly. Excel's implementation of this feature makes it impossible to fix perfectly. I don't know why it was necessary to internationalize R1C1 in the first place - the benefits are so minimal,and the result is worksheets that break when opened in different locales. Ugh. I can't even find complete documentation about the format in different languages; I am using https://answers.microsoft.com/en-us/officeinsider/forum/all/indirect-function-is-broken-at-least-for-excel-in/1fcbcf20-a103-4172-abf1-2c0dfe848e60 as my definitive reference. This fix concentrates on the original report, using the INDIRECT function; there may be other areas similarly affected. As with ambiguous date formats, PhpSpreadsheet will do a little better than Excel itself when reading spreadsheets with internationalized R1C1 by trying all possibilities before giving up. When it does give up, it will now return `#REF!`, as Excel does, rather than throwing an exception, which is certainly friendlier. Although read now works better, when writing it will use whatever the user specified, so spreadsheets breaking in the wrong locale will still happen. There were some bugs that turned up as I added test cases, all of them concerning relative addressing in R1C1 format, e.g. `R[+1]C[-1]`. The regexp for validating the format allowed for minus signs, but not plus signs. Also, the relevant functions did not allow for passing the current cell address, which made relative addressing impossible. The code now allows these, and suitable test cases are added. * Use Locale for Formats, but Not for XML Implementing a suggestion from @MarkBaker to use the system locale for determining R1C1 format rather than looping through a set of regexes and accepting any that work. This is closer to how Excel itself operates. The assumption we are making is to use the first character of the translated ROW and COLUMN functions. This will not work for Russian or Bulgarian, where each starts with the same letter, but it appears that Russian, at least, still uses R1C1. So our algorithm will not use non-ASCII characters, nor characters where ROW and COLUMN start with the same letter, falling back to R/C in those cases. Turkish falls into that category. Czech uses an accented character for one of the functions, and I'm guessing to use the unaccented character in that case. Polish COLUMN function is NR.KOLUMNY, and I'm guessing to use K in that case. The function that converts R1C1 references is also used by the XML reader *where the format is always R1C1*, not locale-based (confirmed by successfully opening in Excel an XML spreadsheet when my language is set to French). The conversion code now handles that distinction through the use of an extra parameter. Xml Reader Load Test is duplicated to confirm that spreadsheet is loaded properly whether the locale is English or French. (No, I did not add an INDIRECT function to the Xml spreadsheet.) Tests CsvIssue2232Test and TranslationTest both changed locale without resetting it when done. That omission was exposed by the new code, and both are now corrected. * OpenOffice and Gnumeric OpenOffice and Gnumeric make it much easier to test with other languages - they can be handled with an environment variable. Sensibly, they require R and C as the characters for R1C1 notation regardless of the language. Change code to recognize this difference from Excel. * Handle Output of ADDRESS Function One other function has to deal with R1C1 format as a string. Unlike INDIRECT, which receives the string on input, ADDRESS generates the string on output. Ensure that the ADDRESS output is consistent with the INDIRECT input. ADDRESS expects its 4th arg to be bool, but it can also accept int, and many examples on the net supply it as an int. This had not been handled properly, but is now corrected. * More Structured Test I earlier introduced a new test for relative R1C1 addressing. Rewrite it to be clearer. * Add Row for This to Locale Spreadsheet It took a while for me to figure out how it all works. I have added a new row (with English value `*RC`) to Translations.xlsx, in the "Lookup and Reference" section of sheet "Excel Functions". By starting the "function name" with an asterisk, it will not be confused with a "real" function (confirmed by a new test). This approach also gives us the flexibility to do something similar if another surprise case occurs in future; in particular, I think this is more flexible than adding this as another option on the "Excel Localisation" sheet. It also means that any errors or omissions in the list below will be handled as with any other translation problem, by updating the spreadsheet without needing to touch any code. The spreadsheet has the following entries in the *RC row: - first letter of ROW/COLUMN functions for da, de, es, fi, fr, hu, nl, nb, pt, pt_br, sv - no value for locales where ROW/COLUMN functions start with same letter - bg, ru, tr - no value for locales with a multi-part name for ROW and/or COLUMN - it, pl (I had not previously noted Italian as an exception) - no value for locales where ROW and/or COLUMN starts with a non-ASCII character - cs (this would also apply to bg and ru which are already included under "same letter") - it does nothing for locales which are defined on the "Excel Localisation" sheet but have no entries yet on the "Excel Functions" sheet (e.g. eu) Note that all but the first bullet item will continue to use R/C, which leaves them no worse off than they were before this change. --- infra/LocaleGenerator.php | 2 +- .../Calculation/Calculation.php | 2 +- .../Calculation/LookupRef/Address.php | 7 +- .../Calculation/LookupRef/Helpers.php | 10 +- .../Calculation/LookupRef/Indirect.php | 9 +- .../Calculation/locale/Translations.xlsx | Bin 110548 -> 111436 bytes .../Calculation/locale/da/functions | 1 + .../Calculation/locale/de/functions | 1 + .../Calculation/locale/es/functions | 1 + .../Calculation/locale/fi/functions | 1 + .../Calculation/locale/fr/functions | 1 + .../Calculation/locale/hu/functions | 1 + .../Calculation/locale/nb/functions | 1 + .../Calculation/locale/nl/functions | 1 + .../Calculation/locale/pt/br/functions | 1 + .../Calculation/locale/pt/functions | 1 + .../Calculation/locale/sv/functions | 1 + src/PhpSpreadsheet/Cell/AddressHelper.php | 29 +++- .../LookupRef/AddressInternationalTest.php | 79 +++++++++++ .../Functions/LookupRef/AddressTest.php | 6 - .../LookupRef/IndirectInternationalTest.php | 132 ++++++++++++++++++ .../Functions/LookupRef/IndirectTest.php | 44 ++++++ .../Calculation/TranslationTest.php | 5 + .../LocaleGeneratorTest.php | 42 +++++- .../Reader/Csv/CsvIssue2232Test.php | 9 +- .../Reader/Xml/PageSetupTest.php | 18 ++- .../Reader/Xml/XmlLoadTest.php | 43 +++++- tests/data/Calculation/LookupRef/ADDRESS.php | 16 +++ tests/data/Calculation/Translations.php | 10 ++ 29 files changed, 436 insertions(+), 38 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index bb97754d..992bde17 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -146,7 +146,7 @@ class LocaleGenerator $translationValue = $translationCell->getValue(); if ($this->isFunctionCategoryEntry($translationCell)) { $this->writeFileSectionHeader($functionFile, "{$translationValue} ({$functionName})"); - } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions)) { + } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions) && substr($functionName, 0, 1) !== '*') { $this->log("Function {$functionName} is not defined in PhpSpreadsheet"); } elseif (!empty($translationValue)) { $functionTranslation = "{$functionName} = {$translationValue}" . self::EOL; diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index b5a26f62..94eadd85 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3110,7 +3110,7 @@ class Calculation [$localeFunction] = explode('##', $localeFunction); // Strip out comments if (strpos($localeFunction, '=') !== false) { [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); - if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { + if ((substr($fName, 0, 1) === '*' || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { self::$localeFunctions[$fName] = $lfName; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index c8cdf2dd..0d2db8b2 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; class Address @@ -72,6 +73,9 @@ class Address $sheetName = self::sheetName($sheetName); + if (is_int($referenceStyle)) { + $referenceStyle = (bool) $referenceStyle; + } if ((!is_bool($referenceStyle)) || $referenceStyle === self::REFERENCE_STYLE_A1) { return self::formatAsA1($row, $column, $relativity, $sheetName); } @@ -113,7 +117,8 @@ class Address if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { $row = "[{$row}]"; } + [$rowChar, $colChar] = AddressHelper::getRowAndColumnChars(); - return "{$sheetName}R{$row}C{$column}"; + return "{$sheetName}$rowChar{$row}$colChar{$column}"; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 7408a66e..76a194b3 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -13,12 +13,12 @@ class Helpers public const CELLADDRESS_USE_R1C1 = false; - private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1): string + private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1, ?int $baseRow = null, ?int $baseCol = null): string { if ($a1 === self::CELLADDRESS_USE_R1C1) { - $cellAddress1 = AddressHelper::convertToA1($cellAddress1); + $cellAddress1 = AddressHelper::convertToA1($cellAddress1, $baseRow ?? 1, $baseCol ?? 1); if ($cellAddress2) { - $cellAddress2 = AddressHelper::convertToA1($cellAddress2); + $cellAddress2 = AddressHelper::convertToA1($cellAddress2, $baseRow ?? 1, $baseCol ?? 1); } } @@ -35,7 +35,7 @@ class Helpers } } - public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = ''): array + public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = '', ?int $baseRow = null, ?int $baseCol = null): array { $cellAddress1 = $cellAddress; $cellAddress2 = null; @@ -52,7 +52,7 @@ class Helpers if (strpos($cellAddress, ':') !== false) { [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); } - $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol); return [$cellAddress1, $cellAddress2, $cellAddress]; } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index 417a1f79..91a14491 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Indirect @@ -63,6 +64,8 @@ class Indirect */ public static function INDIRECT($cellAddress, $a1fmt, Cell $cell) { + [$baseCol, $baseRow] = Coordinate::indexesFromString($cell->getCoordinate()); + try { $a1 = self::a1Format($a1fmt); $cellAddress = self::validateAddress($cellAddress); @@ -78,7 +81,11 @@ class Indirect $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); } - [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); + try { + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol); + } catch (Exception $e) { + return ExcelError::REF(); + } if ( (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || diff --git a/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx index d013aee0f1a2dea934a0a215e5aaa1e3202bbecb..7b9fb0dddfff921e9831507af3b0dfd53a8ee10a 100644 GIT binary patch delta 97055 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 z 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 aY;M+8O0usI{{4An delta 96406 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

kRS+FJs>|j1s|SB3^yySfvWerv?)OThZcTsFcT| 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 = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale, string $r, string $c): void + { + if ($locale !== '') { + Settings::setLocale($locale); + } + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerInternational(): array + { + return [ + 'Default' => ['', 'R', 'C'], + 'English' => ['en', 'R', 'C'], + 'French' => ['fr', 'L', 'C'], + 'German' => ['de', 'Z', 'S'], + 'Made-up' => ['xx', 'R', 'C'], + 'Spanish' => ['es', 'F', 'C'], + 'Bulgarian' => ['bg', 'R', 'C'], + 'Czech' => ['cs', 'R', 'C'], // maybe should be R/S + 'Polish' => ['pl', 'R', 'C'], // maybe should be W/K + 'Turkish' => ['tr', 'R', 'C'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode, string $r, string $c): void + { + Functions::setCompatibilityMode($compatibilityMode); + Settings::setLocale('de'); + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL, 'Z', 'S'], + [Functions::COMPATIBILITY_OPENOFFICE, 'R', 'C'], + [Functions::COMPATIBILITY_GNUMERIC, 'R', 'C'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php index 2b92030e..2a6ae883 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class AddressTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerADDRESS * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php new file mode 100644 index 00000000..7d6d0a65 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php @@ -0,0 +1,132 @@ +locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale): void + { + Settings::setLocale($locale); + $sameAsEnglish = ['en', 'xx', 'ru', 'tr', 'cs', 'pl']; + $sheet = $this->getSheet(); + $sheet->getCell('C1')->setValue('text'); + $sheet->getCell('A2')->setValue('en'); + $sheet->getCell('B2')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A3')->setValue('fr'); + $sheet->getCell('B3')->setValue('=INDIRECT("L1C3", false)'); + $sheet->getCell('A4')->setValue('de'); + $sheet->getCell('B4')->setValue('=INDIRECT("Z1S3", false)'); + $sheet->getCell('A5')->setValue('es'); + $sheet->getCell('B5')->setValue('=INDIRECT("F1C3", false)'); + $sheet->getCell('A6')->setValue('xx'); + $sheet->getCell('B6')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A7')->setValue('ru'); + $sheet->getCell('B7')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A8')->setValue('cs'); + $sheet->getCell('B8')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A9')->setValue('tr'); + $sheet->getCell('B9')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A10')->setValue('pl'); + $sheet->getCell('B10')->setValue('=INDIRECT("R1C3", false)'); + $maxRow = $sheet->getHighestRow(); + for ($row = 2; $row <= $maxRow; ++$row) { + $rowLocale = $sheet->getCell("A$row")->getValue(); + if (in_array($rowLocale, $sameAsEnglish, true) && in_array($locale, $sameAsEnglish, true)) { + $expectedResult = 'text'; + } else { + $expectedResult = ($locale === $sheet->getCell("A$row")->getValue()) ? 'text' : '#REF!'; + } + self::assertSame($expectedResult, $sheet->getCell("B$row")->getCalculatedValue(), "Locale $locale error in cell B$row $rowLocale"); + } + } + + public function providerInternational(): array + { + return [ + 'English' => ['en'], + 'French' => ['fr'], + 'German' => ['de'], + 'Made-up' => ['xx'], + 'Spanish' => ['es'], + 'Russian' => ['ru'], + 'Czech' => ['cs'], + 'Polish' => ['pl'], + 'Turkish' => ['tr'], + ]; + } + + /** + * @dataProvider providerRelativeInternational + */ + public function testRelativeInternational(string $locale, string $cell, string $relative): void + { + Settings::setLocale($locale); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell($cell)->setValue("=INDIRECT(\"$relative\", false)"); + self::assertSame('text', $sheet->getCell($cell)->getCalculatedValue()); + } + + public function providerRelativeInternational(): array + { + return [ + 'English A3' => ['en', 'A3', 'R[]C[+2]'], + 'French B4' => ['fr', 'B4', 'L[-1]C[+1]'], + 'German C5' => ['de', 'C5', 'Z[-2]S[]'], + 'Spanish E1' => ['es', 'E1', 'F[+2]C[-2]'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode): void + { + Functions::setCompatibilityMode($compatibilityMode); + if ($compatibilityMode === Functions::COMPATIBILITY_EXCEL) { + $expected1 = '#REF!'; + $expected2 = 'text'; + } else { + $expected2 = '#REF!'; + $expected1 = 'text'; + } + Settings::setLocale('fr'); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell('A1')->setValue('=INDIRECT("R3C3", false)'); + $sheet->getCell('A2')->setValue('=INDIRECT("L3C3", false)'); + self::assertSame($expected1, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($expected2, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL], + [Functions::COMPATIBILITY_OPENOFFICE], + [Functions::COMPATIBILITY_GNUMERIC], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php index 7601e336..accfc058 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php @@ -132,4 +132,48 @@ class IndirectTest extends AllSetupTeardown $result = \PhpOffice\PhpSpreadsheet\Calculation\Functions::flattenSingleValue($result); self::assertSame('This is it', $result); } + + /** + * @param null|int|string $expectedResult + * + * @dataProvider providerRelative + */ + public function testR1C1Relative($expectedResult, string $address): void + { + $sheet = $this->getSheet(); + $sheet->fromArray([ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ['a3', 'b3', 'c3'], + ['a4', 'b4', 'c4'], + ]); + $sheet->getCell('B2')->setValue('=INDIRECT("' . $address . '", false)'); + self::assertSame($expectedResult, $sheet->getCell('B2')->getCalculatedValue()); + } + + public function providerRelative(): array + { + return [ + 'same row with bracket next column' => ['c2', 'R[]C[+1]'], + 'same row without bracket next column' => ['c2', 'RC[+1]'], + 'same row without bracket next column no plus sign' => ['c2', 'RC[1]'], + 'same row previous column' => ['a2', 'RC[-1]'], + 'previous row previous column' => ['a1', 'R[-1]C[-1]'], + 'previous row same column with bracket' => ['b1', 'R[-1]C[]'], + 'previous row same column without bracket' => ['b1', 'R[-1]C'], + 'previous row next column' => ['c1', 'R[-1]C[+1]'], + 'next row no plus sign previous column' => ['a3', 'R[1]C[-1]'], + 'next row previous column' => ['a3', 'R[+1]C[-1]'], + 'next row same column' => ['b3', 'R[+1]C'], + 'next row next column' => ['c3', 'R[+1]C[+1]'], + 'two rows down same column' => ['b4', 'R[+2]C'], + 'invalid row' => ['#REF!', 'R[-2]C'], + 'invalid column' => ['#REF!', 'RC[-2]'], + 'circular reference' => [0, 'RC'], // matches Excel's treatment + 'absolute row absolute column' => ['c2', 'R2C3'], + 'absolute row relative column' => ['a2', 'R2C[-1]'], + 'relative row absolute column lowercase' => ['a2', 'rc1'], + 'uninitialized cell' => [null, 'RC[+2]'], // Excel result is 0 + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php index e2384460..ac1f15a1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php @@ -19,18 +19,23 @@ class TranslationTest extends TestCase */ private $returnDate; + /** @var string */ + private $locale; + protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); $this->returnDate = Functions::getReturnDateType(); Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); Functions::setReturnDateType($this->returnDate); + Settings::setLocale($this->locale); } /** diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php index e7429a25..ca9d5183 100644 --- a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -11,31 +11,59 @@ class LocaleGeneratorTest extends TestCase { public function testLocaleGenerator(): void { + $directory = realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/') ?: ''; + self::assertNotEquals('', $directory); $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) ->getProperty('phpSpreadsheetFunctions'); $phpSpreadsheetFunctionsProperty->setAccessible(true); $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); $localeGenerator = new LocaleGenerator( - (string) realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), + $directory . DIRECTORY_SEPARATOR, 'Translations.xlsx', $phpSpreadsheetFunctions ); $localeGenerator->generateLocales(); $testLocales = [ + 'bg', + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fi', 'fr', + 'hu', + 'it', + 'nb', 'nl', + 'pl', 'pt', - 'pt_br', 'ru', + 'sv', + 'tr', ]; - foreach ($testLocales as $locale) { - $locale = str_replace('_', '/', $locale); - $path = realpath(__DIR__ . "/../../src/PhpSpreadsheet/Calculation/locale/{$locale}"); - self::assertFileExists("{$path}/config"); - self::assertFileExists("{$path}/functions"); + $count = count(glob($directory . DIRECTORY_SEPARATOR . '*') ?: []) - 1; // exclude Translations.xlsx + self::assertCount($count, $testLocales); + $testLocales[] = 'pt_br'; + $testLocales[] = 'en_uk'; + $noconfig = ['en']; + $nofunctions = ['en', 'en_uk']; + foreach ($testLocales as $originalLocale) { + $locale = str_replace('_', DIRECTORY_SEPARATOR, $originalLocale); + $path = $directory . DIRECTORY_SEPARATOR . $locale; + if (in_array($originalLocale, $noconfig, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'config'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'config'); + } + if (in_array($originalLocale, $nofunctions, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'functions'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'functions'); + } } } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php index f9321102..429874dc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php @@ -2,11 +2,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; use PhpOffice\PhpSpreadsheet\Reader\Csv; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class CsvIssue2232Test extends TestCase @@ -16,14 +16,19 @@ class CsvIssue2232Test extends TestCase */ private $valueBinder; + /** @var string */ + private $locale; + protected function setUp(): void { $this->valueBinder = Cell::getValueBinder(); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Cell::setValueBinder($this->valueBinder); + Settings::setLocale($this->locale); } /** @@ -78,7 +83,7 @@ class CsvIssue2232Test extends TestCase Cell::setValueBinder($binder); } - Calculation::getInstance()->setLocale('fr'); + Settings::setLocale('fr'); $reader = new Csv(); $filename = 'tests/data/Reader/CSV/issue.2232.csv'; diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php index 97476ed5..ae79dd0e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php @@ -14,19 +14,25 @@ class PageSetupTest extends TestCase private const MARGIN_UNIT_CONVERSION = 2.54; // Inches to cm /** - * @var Spreadsheet + * @var ?Spreadsheet */ private $spreadsheet; - protected function setup(): void + /** @var string */ + private $filename = 'tests/data/Reader/Xml/PageSetup.xml'; + + protected function tearDown(): void { - $filename = 'tests/data/Reader/Xml/PageSetup.xml'; - $reader = new Xml(); - $this->spreadsheet = $reader->load($filename); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } public function testPageSetup(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageSetupAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { @@ -49,6 +55,8 @@ class PageSetupTest extends TestCase public function testPageMargins(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageMarginAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php index 29d81299..9846b861 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php @@ -4,18 +4,51 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xml; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Reader\Xml; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class XmlLoadTest extends TestCase { - public function testLoad(): void + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var string */ + private $locale; + + protected function setUp(): void + { + $this->locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + Settings::setLocale($this->locale); + } + + public function testLoadEnglish(): void + { + $this->xtestLoad(); + } + + public function testLoadFrench(): void + { + Settings::setLocale('fr'); + $this->xtestLoad(); + } + + public function xtestLoad(): void { $filename = __DIR__ . '/../../../..' . '/samples/templates/excel2003.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); @@ -71,7 +104,7 @@ class XmlLoadTest extends TestCase $reader = new Xml(); $filter = new XmlFilter(); $reader->setReadFilter($filter); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); self::assertEquals('Report Data', $sheet->getTitle()); @@ -87,7 +120,7 @@ class XmlLoadTest extends TestCase . '/samples/templates/excel2003.xml'; $reader = new Xml(); $reader->setLoadSheetsOnly(['Unknown Sheet', 'Report Data']); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Report Data', $sheet->getTitle()); @@ -102,7 +135,7 @@ class XmlLoadTest extends TestCase . '/../../../..' . '/samples/templates/excel2003.short.bad.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Sample Data', $sheet->getTitle()); diff --git a/tests/data/Calculation/LookupRef/ADDRESS.php b/tests/data/Calculation/LookupRef/ADDRESS.php index b9e170d5..14a3cf7d 100644 --- a/tests/data/Calculation/LookupRef/ADDRESS.php +++ b/tests/data/Calculation/LookupRef/ADDRESS.php @@ -48,6 +48,22 @@ return [ false, 'EXCEL SHEET', ], + '0 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!R2C3", + 2, + 3, + null, + 0, + 'EXCEL SHEET', + ], + '1 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!\$C\$2", + 2, + 3, + null, + 1, + 'EXCEL SHEET', + ], [ "'EXCEL SHEET'!\$C\$2", 2, diff --git a/tests/data/Calculation/Translations.php b/tests/data/Calculation/Translations.php index 604c6721..0965bac1 100644 --- a/tests/data/Calculation/Translations.php +++ b/tests/data/Calculation/Translations.php @@ -80,4 +80,14 @@ return [ 'nb', '=MAX(ABS({2,-3;-4,5}), ABS{-2,3;4,-5})', ], + 'not fooled by *RC' => [ + '=3*RC(B1)', + 'fr', + '=3*RC(B1)', + ], + 'handle * for ROW' => [ + '=3*LIGNE(B1)', + 'fr', + '=3*ROW(B1)', + ], ]; From b7fa4701382794dac85ed4caf4a2f760e1c92cde Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 16 Sep 2022 09:16:11 -0700 Subject: [PATCH 119/156] Scrutinizer Changes (#3060) * Scrutinizer Changes Scrutinizer appears to be working again. But the PRs that have used it have neither added new issues nor fixed existing ones. This PR should fix some exisiting; let's see what Scrutinizer does with it. * Address Some False Positives In Reader/Xlsx/Chart. --- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 15 ++++++++++----- src/PhpSpreadsheet/Writer/Html.php | 3 --- .../Reader/Xlsx/AutoFilter2Test.php | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 6bc6d7c5..c22334ca 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -14,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Chart\TrendLine; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; @@ -94,7 +95,7 @@ class Chart break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { - $chartDetailsC = $chartDetails->children($this->cNamespace); + $chartDetails = Xlsx::testSimpleXml($chartDetails); switch ($chartDetailsKey) { case 'autoTitleDeleted': /** @var bool */ @@ -113,8 +114,8 @@ class Chart $plotSeries = $plotAttributes = []; $catAxRead = false; $plotNoFill = false; - /** @var SimpleXMLElement $chartDetail */ foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { case 'spPr': $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); @@ -122,8 +123,8 @@ class Chart $plotNoFill = true; } if (isset($possibleNoFill->gradFill->gsLst)) { - /** @var SimpleXMLElement $gradient */ foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + $gradient = Xlsx::testSimpleXml($gradient); /** @var float */ $pos = self::getAttribute($gradient, 'pos', 'float'); $gradientArray[] = [ @@ -348,6 +349,7 @@ class Chart $legendLayout = null; $legendOverlay = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { case 'legendPos': $legendPos = self::getAttribute($chartDetail, 'val', 'string'); @@ -399,11 +401,13 @@ class Chart $caption = []; $titleLayout = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { case 'tx': if (isset($chartDetail->rich)) { $titleDetails = $chartDetail->rich->children($this->aNamespace); foreach ($titleDetails as $titleKey => $titleDetail) { + $titleDetail = Xlsx::testSimpleXml($titleDetail); switch ($titleKey) { case 'p': $titleDetailPart = $titleDetail->children($this->aNamespace); @@ -440,6 +444,7 @@ class Chart } $layout = []; foreach ($details as $detailKey => $detail) { + $detail = Xlsx::testSimpleXml($detail); $layout[$detailKey] = self::getAttribute($detail, 'val', 'string'); } @@ -472,8 +477,8 @@ class Chart $lineStyle = null; $labelLayout = null; $trendLines = []; - /** @var SimpleXMLElement $seriesDetail */ foreach ($seriesDetails as $seriesKey => $seriesDetail) { + $seriesDetail = Xlsx::testSimpleXml($seriesDetail); switch ($seriesKey) { case 'idx': $seriesIndex = self::getAttribute($seriesDetail, 'val', 'integer'); @@ -786,6 +791,7 @@ class Chart $pointCount = 0; foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) { + $seriesValue = Xlsx::testSimpleXml($seriesValue); switch ($seriesValueIdx) { case 'ptCount': $pointCount = self::getAttribute($seriesValue, 'val', 'integer'); @@ -858,7 +864,6 @@ class Chart private function parseRichText(SimpleXMLElement $titleDetailPart): RichText { $value = new RichText(); - $objText = null; $defaultFontSize = null; $defaultBold = null; $defaultItalic = null; diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index fca5ee89..0fef0f60 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1461,9 +1461,6 @@ class Html extends BaseWriter foreach ($values as $cellAddress) { [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum); - $colSpan = 1; - $rowSpan = 1; - // Cell Data $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 06d6b562..7a264394 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -93,6 +93,7 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; From ceb3bb2f38411d9efd7e606eaa27e4df0d325e68 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 16 Sep 2022 13:16:45 -0700 Subject: [PATCH 120/156] Changelog Catch-up (#3069) Added 5 changes. --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15e9d5b..ad275147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. -- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes. ### Changed @@ -49,6 +49,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) - Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) - Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) +- Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016) +- Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018) +- Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048) +- R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052) +- Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053) ## 1.24.1 - 2022-07-18 From c465b1c28390719831a889a9521951f0732a588f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 17 Sep 2022 07:32:12 -0700 Subject: [PATCH 121/156] Minor Changes for Cygwin (#3070) No source code changes. Mitoteam added a change to better accomodate Cygwin; pick up their new release. While testing, discovered one test that was already skipped on Windows and needs to be skipped on Cygwin as well. --- composer.json | 2 +- composer.lock | 14 +++++++------- tests/PhpSpreadsheetTests/Shared/FileTest.php | 6 ++++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index a4b033cd..ea7f57b1 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "10.2.3", + "mitoteam/jpgraph": "10.2.4", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 508733ee..af246273 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b5bdb9f96d18ce59557436521053fdd9", + "content-hash": "6d946e91cbe5d38e1cfb0208512ab981", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1342,16 +1342,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.2.3", + "version": "10.2.4", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "21121535537e05c32e7964327b80746462a6057d" + "reference": "9ce4d106a89f120c7e220ea22205ef7956a7027b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/21121535537e05c32e7964327b80746462a6057d", - "reference": "21121535537e05c32e7964327b80746462a6057d", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/9ce4d106a89f120c7e220ea22205ef7956a7027b", + "reference": "9ce4d106a89f120c7e220ea22205ef7956a7027b", "shasum": "" }, "require": { @@ -1382,9 +1382,9 @@ ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.2.3" + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.4" }, - "time": "2022-09-14T04:02:09+00:00" + "time": "2022-09-15T05:57:43+00:00" }, { "name": "mpdf/mpdf", diff --git a/tests/PhpSpreadsheetTests/Shared/FileTest.php b/tests/PhpSpreadsheetTests/Shared/FileTest.php index ddc54b5e..e65ec810 100644 --- a/tests/PhpSpreadsheetTests/Shared/FileTest.php +++ b/tests/PhpSpreadsheetTests/Shared/FileTest.php @@ -87,12 +87,14 @@ class FileTest extends TestCase public function testNotReadable(): void { - if (PHP_OS_FAMILY === 'Windows') { + if (PHP_OS_FAMILY === 'Windows' || stristr(PHP_OS, 'CYGWIN') !== false) { self::markTestSkipped('chmod does not work reliably on Windows'); } $this->tempfile = $temp = File::temporaryFileName(); file_put_contents($temp, ''); - chmod($temp, 0070); + if (chmod($temp, 0070) === false) { + self::markTestSkipped('chmod failed'); + } self::assertFalse(File::testFileNoThrow($temp)); $this->expectException(ReaderException::class); $this->expectExceptionMessage('for reading'); From 1746a5ac26bddcbce9521c0e8b91d2334da0b68e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 18 Sep 2022 16:58:56 +0200 Subject: [PATCH 122/156] Ensure that the sqRef stored for a DataValidation is updated on insert/delete rows/columns, together with the DataValidationCollection value --- CHANGELOG.md | 1 + src/PhpSpreadsheet/ReferenceHelper.php | 5 +++-- tests/PhpSpreadsheetTests/ReferenceHelperTest.php | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15e9d5b..b6010729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) - Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 08a38b3b..822b754a 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -253,10 +253,11 @@ class ReferenceHelper ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) : uksort($aDataValidationCollection, [self::class, 'cellSort']); - foreach ($aDataValidationCollection as $cellAddress => $value) { + foreach ($aDataValidationCollection as $cellAddress => $dataValidation) { $newReference = $this->updateCellReference($cellAddress); if ($cellAddress !== $newReference) { - $worksheet->setDataValidation($newReference, $value); + $dataValidation->setSqref($newReference); + $worksheet->setDataValidation($newReference, $dataValidation); $worksheet->setDataValidation($cellAddress, null); } } diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index 2640d80c..fbd60155 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -311,6 +311,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('E7')->hasDataValidation()); + self::assertSame('E7', $sheet->getDataValidation('E7')->getSqref()); } public function testDeleteRowsWithDataValidation(): void @@ -326,6 +327,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('E3')->hasDataValidation()); + self::assertSame('E3', $sheet->getDataValidation('E3')->getSqref()); } public function testDeleteColumnsWithDataValidation(): void @@ -341,6 +343,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('C5')->hasDataValidation()); + self::assertSame('C5', $sheet->getDataValidation('C5')->getSqref()); } public function testInsertColumnsWithDataValidation(): void @@ -356,6 +359,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('G5')->hasDataValidation()); + self::assertSame('G5', $sheet->getDataValidation('G5')->getSqref()); } private function setDataValidation(Worksheet $sheet, string $cellAddress): void From 1f5ae85d193cac15dd6e23069adf40831dbe883c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 18 Sep 2022 21:16:36 +0200 Subject: [PATCH 123/156] Minor documentation update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dd712bff..c3320d70 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ For Chart export, we support following packages, which you will also need to ins You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) +One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. + and then configure PhpSpreadsheet using: ```php Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph From 579d2f9f6987960a5ab55b14ec994b79e4445ec1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 18 Sep 2022 21:17:03 +0200 Subject: [PATCH 124/156] Minor documentation update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3320d70..ef04cd07 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,6 @@ For Chart export, we support following packages, which you will also need to ins You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) -One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. - and then configure PhpSpreadsheet using: ```php Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph @@ -85,6 +83,8 @@ Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::cla Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph ``` +One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). From a3921d20bc5c9a2ba6add05ad96fb41f873e36bd Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 19 Sep 2022 06:20:48 -0700 Subject: [PATCH 125/156] Upgrade ezyang/htmlpurifier for Php8.2 (#3073) They have just issued a new release. Using that should eliminate the last of our Php8.2 unit test problems, so we will be able to change that to non-experimental when it is convenient for us. --- composer.json | 2 +- composer.lock | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index ea7f57b1..858bd819 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", - "ezyang/htmlpurifier": "^4.13", + "ezyang/htmlpurifier": "4.15", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", diff --git a/composer.lock b/composer.lock index af246273..a5bc7d7b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,24 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6d946e91cbe5d38e1cfb0208512ab981", + "content-hash": "9a7fc81b4223c114749fc07401fb4b6f", "packages": [ { "name": "ezyang/htmlpurifier", - "version": "v4.14.0", + "version": "v4.15.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" + "reference": "8d9f4c9ec154922ff19690ffade9ed915b27a017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/8d9f4c9ec154922ff19690ffade9ed915b27a017", + "reference": "8d9f4c9ec154922ff19690ffade9ed915b27a017", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { @@ -53,9 +63,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.15.0" }, - "time": "2021-12-25T01:21:49+00:00" + "time": "2022-09-18T06:23:57+00:00" }, { "name": "maennchen/zipstream-php", From a2b29841041ca1aa2c5d54e921b8f1dae0098b3f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 19 Sep 2022 06:49:01 -0700 Subject: [PATCH 126/156] Document Charset Restriction for Html/Xml Reader (#3068) Fix #1681, although probably not to the originator's satisfaction. Html and Xml readers will handle documents only with a charset of UTF-8. This PR documents that restriction. No change to source code; see the original issue for explanation. --- docs/topics/reading-and-writing-to-file.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 0bcc1909..55929e85 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -282,6 +282,7 @@ versions of Microsoft Excel. **Excel 2003 XML limitations** Please note that Excel 2003 XML format has some limits regarding to styling cells and handling large spreadsheets via PHP. +Also, only files using charset UTF-8 are supported. ### \PhpOffice\PhpSpreadsheet\Reader\Xml @@ -701,6 +702,7 @@ extension. **HTML limitations** Please note that HTML file format has some limits regarding to styling cells, number formatting, ... +Also, only files using charset UTF-8 are supported. ### \PhpOffice\PhpSpreadsheet\Reader\Html From 53e0828d49c3a330d15d19f94053db1dbb23be8a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:37:00 -0700 Subject: [PATCH 127/156] Sync composer.lock (#3075) When I cloned this morning, composer gave me a message that the lock file was not up to date with the latest changes in composer.json. I do not understand why, but it suggested to run `composer update`, which I did. This led to a handful of problems with php-cs-fixer, all fixed with changes to doc-blocks, and phpstan (only Writer/Xls/Worksheet required a change to code). We would presumably have had these problems at the start of next month when dependabot did its thing, so fix them now. --- composer.lock | 146 +++++++++--------- phpstan-baseline.neon | 2 +- .../Calculation/Information/Value.php | 2 +- src/PhpSpreadsheet/Cell/Cell.php | 10 -- src/PhpSpreadsheet/Style/Color.php | 2 - src/PhpSpreadsheet/Worksheet/CellIterator.php | 1 + src/PhpSpreadsheet/Writer/Xls.php | 2 - src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 8 +- .../Functions/LookupRef/FormulaTextTest.php | 1 + .../PhpSpreadsheetTests/Helper/SampleTest.php | 2 + .../Worksheet/WorksheetTest.php | 3 + 11 files changed, 87 insertions(+), 92 deletions(-) diff --git a/composer.lock b/composer.lock index a5bc7d7b..22513999 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9a7fc81b4223c114749fc07401fb4b6f", + "content-hash": "5062910e2e463ba84b1c753c58624ca9", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1194,16 +1194,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.10.0", + "version": "v3.11.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe" + "reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", - "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/7dcdea3f2f5f473464e835be9be55283ff8cfdc3", + "reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3", "shasum": "" }, "require": { @@ -1271,7 +1271,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.10.0" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.11.0" }, "funding": [ { @@ -1279,7 +1279,7 @@ "type": "github" } ], - "time": "2022-08-17T22:13:10+00:00" + "time": "2022-09-01T18:24:51+00:00" }, { "name": "masterminds/html5", @@ -1534,16 +1534,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -1584,9 +1584,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "paragonie/random_compat", @@ -1957,16 +1957,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.2", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c" + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c53312ecc575caf07b0e90dee43883fdf90ca67c", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", "shasum": "" }, "require": { @@ -1990,9 +1990,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.2" + "source": "https://github.com/phpstan/phpstan/tree/1.8.5" }, "funding": [ { @@ -2003,16 +2007,12 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-07-20T09:57:31+00:00" + "time": "2022-09-07T16:05:32+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -2068,16 +2068,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.16", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { @@ -2133,7 +2133,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -2141,7 +2141,7 @@ "type": "github" } ], - "time": "2022-08-20T05:26:47+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2386,16 +2386,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.23", + "version": "9.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34" + "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", "shasum": "" }, "require": { @@ -2424,7 +2424,7 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -2468,7 +2468,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" }, "funding": [ { @@ -2480,7 +2480,7 @@ "type": "github" } ], - "time": "2022-08-22T14:01:36+00:00" + "time": "2022-08-30T07:42:16+00:00" }, { "name": "psr/cache", @@ -2901,16 +2901,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -2963,7 +2963,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -2971,7 +2971,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -3161,16 +3161,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -3226,7 +3226,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -3234,7 +3234,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -3589,16 +3589,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { @@ -3610,7 +3610,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -3633,7 +3633,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" }, "funding": [ { @@ -3641,7 +3641,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", @@ -3826,16 +3826,16 @@ }, { "name": "symfony/console", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", + "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.11" + "source": "https://github.com/symfony/console/tree/v5.4.12" }, "funding": [ { @@ -3921,7 +3921,7 @@ "type": "tidelift" } ], - "time": "2022-07-22T10:42:43+00:00" + "time": "2022-08-17T13:18:05+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4156,16 +4156,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd" + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6699fb0228d1bc35b12aed6dd5e7455457609ddd", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d67c1f9a1937406a9be3171b4b22250c0a11447", + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447", "shasum": "" }, "require": { @@ -4200,7 +4200,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.11" + "source": "https://github.com/symfony/filesystem/tree/v5.4.12" }, "funding": [ { @@ -4216,7 +4216,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-02T13:48:16+00:00" }, { "name": "symfony/finder", @@ -5047,16 +5047,16 @@ }, { "name": "symfony/string", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322" + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5eb661e49ad389e4ae2b6e4df8d783a8a6548322", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322", + "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", "shasum": "" }, "require": { @@ -5113,7 +5113,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.11" + "source": "https://github.com/symfony/string/tree/v5.4.12" }, "funding": [ { @@ -5129,7 +5129,7 @@ "type": "tidelift" } ], - "time": "2022-07-24T16:15:25+00:00" + "time": "2022-08-12T17:03:11+00:00" }, { "name": "tecnickcom/tcpdf", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3e2ccfc1..b0271f35 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2912,7 +2912,7 @@ parameters: - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index 0ac6b669..2e524db5 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -240,7 +240,7 @@ class Value * * @param null|mixed $value The value you want converted * - * @return number N converts values listed in the following table + * @return number|string N converts values listed in the following table * If value is or refers to N returns * A number That number value * A date The Excel serialized number of that date diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index bc02fbf0..d4d793d7 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -313,8 +313,6 @@ class Cell * Set old calculated value (cached). * * @param mixed $originalValue Value - * - * @return Cell */ public function setCalculatedValue($originalValue): self { @@ -352,8 +350,6 @@ class Cell * Set cell data type. * * @param string $dataType see DataType::TYPE_* - * - * @return Cell */ public function setDataType($dataType): self { @@ -447,8 +443,6 @@ class Cell /** * Set Hyperlink. - * - * @return Cell */ public function setHyperlink(?Hyperlink $hyperlink = null): self { @@ -556,8 +550,6 @@ class Cell /** * Re-bind parent. - * - * @return Cell */ public function rebindParent(Worksheet $parent): self { @@ -650,8 +642,6 @@ class Cell /** * Set index to cellXf. - * - * @return Cell */ public function setXfIndex(int $indexValue): self { diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index 9ab0c98f..922be803 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -387,8 +387,6 @@ class Color extends Supervisor * @param int $colorIndex Index entry point into the colour array * @param bool $background Flag to indicate whether default background or foreground colour * should be returned if the indexed colour doesn't exist - * - * @return Color */ public static function indexedColor($colorIndex, $background = false, ?array $palette = null): self { diff --git a/src/PhpSpreadsheet/Worksheet/CellIterator.php b/src/PhpSpreadsheet/Worksheet/CellIterator.php index 17286f9c..94877f66 100644 --- a/src/PhpSpreadsheet/Worksheet/CellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/CellIterator.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Cells; /** * @template TKey + * * @implements Iterator */ abstract class CellIterator implements Iterator diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index eadf0083..69457357 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -732,7 +732,6 @@ class Xls extends BaseWriter } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length // Null-terminated string $dataProp['data']['data'] .= chr(0); - // @phpstan-ignore-next-line ++$dataProp['data']['length']; // Complete the string with null string for being a %4 $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4)); @@ -750,7 +749,6 @@ class Xls extends BaseWriter } else { $dataSection_Content .= $dataProp['data']['data']; - // @phpstan-ignore-next-line $dataSection_Content_Offset += 4 + $dataProp['data']['length']; } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 847d772a..78fda517 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2405,10 +2405,12 @@ class Worksheet extends BIFFwriter for ($i = 0; $i < $width; ++$i) { /** @phpstan-ignore-next-line */ $color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); - foreach (['red', 'green', 'blue'] as $key) { - $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); + if ($color !== false) { + foreach (['red', 'green', 'blue'] as $key) { + $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); + } + $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } - $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } if (3 * $width % 4) { $data .= str_repeat("\x00", 4 - 3 * $width % 4); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php index ccd689b1..6c91f3fc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php @@ -11,6 +11,7 @@ class FormulaTextTest extends AllSetupTeardown { /** * @param mixed $value + * * @dataProvider providerFormulaText */ public function testFormulaText(string $expectedResult, $value): void diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index 2195155f..384011e8 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -9,7 +9,9 @@ class SampleTest extends TestCase { /** * @runInSeparateProcess + * * @preserveGlobalState disabled + * * @dataProvider providerSample */ public function testSample(string $sample): void diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 17de5c32..30da1d75 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -31,6 +31,7 @@ class WorksheetTest extends TestCase /** * @param string $title * @param string $expectMessage + * * @dataProvider setTitleInvalidProvider */ public function testSetTitleInvalid($title, $expectMessage): void @@ -89,6 +90,7 @@ class WorksheetTest extends TestCase /** * @param string $codeName * @param string $expectMessage + * * @dataProvider setCodeNameInvalidProvider */ public function testSetCodeNameInvalid($codeName, $expectMessage): void @@ -153,6 +155,7 @@ class WorksheetTest extends TestCase * @param string $expectTitle * @param string $expectCell * @param string $expectCell2 + * * @dataProvider extractSheetTitleProvider */ public function testExtractSheetTitle($range, $expectTitle, $expectCell, $expectCell2): void From 6e93505cf5e06143d469b74fdc26b84975fa5b9d Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 21 Sep 2022 14:43:24 +0200 Subject: [PATCH 128/156] Minor cosmetic changes --- phpstan-baseline.neon | 17 ++++++++++++++++- src/PhpSpreadsheet/Calculation/Calculation.php | 3 ++- src/PhpSpreadsheet/Reader/Xlsx.php | 6 +++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b0271f35..58206827 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1660,6 +1660,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls/RC4.php + - + message: "#^Cannot access property \\$r on SimpleXMLElement\\|null\\.$#" + count: 2 + path: src/PhpSpreadsheet/Reader/Xlsx.php + - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" count: 1 @@ -2750,6 +2755,16 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xls.php + - + message: "#^Offset 'length' does not exist on array\\{data\\: non\\-empty\\-string, length\\?\\: int\\<1, max\\>\\}\\|array\\{pack\\?\\: 'V', data\\: non\\-empty\\-string\\}\\.$#" + count: 1 + path: src/PhpSpreadsheet/Writer/Xls.php + + - + message: "#^Offset 'length' does not exist on array\\{data\\: non\\-empty\\-string\\|false, length\\?\\: int\\<1, max\\>\\}\\|array\\{pack\\?\\: 'V', data\\: 1252\\|786432\\|false\\}\\.$#" + count: 1 + path: src/PhpSpreadsheet/Writer/Xls.php + - message: "#^Offset 'startCoordinates' does not exist on array\\|null\\.$#" count: 1 @@ -2912,7 +2927,7 @@ parameters: - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 1 + count: 2 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 94eadd85..59bfcafe 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4793,7 +4793,7 @@ class Calculation if (isset($matches[8])) { if ($cell === null) { - // We can't access the range, so return a REF error + // We can't access the range, so return a REF error $cellValue = Information\ExcelError::REF(); } else { $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10]; @@ -4864,6 +4864,7 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $cellValue; } + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on if ($pCellParent) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 26cd1af3..e2fae121 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -487,7 +487,7 @@ class Xlsx extends BaseReader $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); break; - // Ribbon + //Ribbon case Namespaces::EXTENSIBILITY: $customUI = $relTarget; if ($customUI) { @@ -1703,8 +1703,8 @@ class Xlsx extends BaseReader break; - case 'application/vnd.ms-excel.controlproperties+xml': // unparsed + case 'application/vnd.ms-excel.controlproperties+xml': $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; break; @@ -1729,7 +1729,7 @@ class Xlsx extends BaseReader if (isset($is->t)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { - if (isset($is->r) && is_object($is->r)) { + if (is_object($is->r)) { /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { From b9ded919fc50d8c06971aebd7cb66298f9c657b8 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 21 Sep 2022 15:37:51 +0200 Subject: [PATCH 129/156] Minor cosmetic changes --- phpstan-baseline.neon | 12 +----------- src/PhpSpreadsheet/Calculation/Calculation.php | 1 - 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 58206827..c4b76cdd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2755,16 +2755,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xls.php - - - message: "#^Offset 'length' does not exist on array\\{data\\: non\\-empty\\-string, length\\?\\: int\\<1, max\\>\\}\\|array\\{pack\\?\\: 'V', data\\: non\\-empty\\-string\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls.php - - - - message: "#^Offset 'length' does not exist on array\\{data\\: non\\-empty\\-string\\|false, length\\?\\: int\\<1, max\\>\\}\\|array\\{pack\\?\\: 'V', data\\: 1252\\|786432\\|false\\}\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xls.php - - message: "#^Offset 'startCoordinates' does not exist on array\\|null\\.$#" count: 1 @@ -2927,7 +2917,7 @@ parameters: - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 59bfcafe..4f95af54 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4864,7 +4864,6 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $cellValue; } - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on if ($pCellParent) { From b7c025a183f5f774842ad5f6f5617778781897c0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 22 Sep 2022 01:10:53 +0200 Subject: [PATCH 130/156] Correct update to named ranges and formulae when inserting/deleting columns/rows --- src/PhpSpreadsheet/ReferenceHelper.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 822b754a..7ae7f1de 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -539,8 +539,17 @@ class ReferenceHelper // Update workbook: define names if (count($worksheet->getParent()->getDefinedNames()) > 0) { foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $definedName->setValue($this->updateCellReference($definedName->getValue())); + if ($definedName->isFormula() === false) { + $asFormula = ($definedName->getValue()[0] === '=') ? '=' : ''; + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $definedName->setValue($asFormula . $this->updateCellReference(ltrim($definedName->getValue(), '='))); + } + } else { + $formula = $definedName->getValue(); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } } } } From abcbac6f9a9e8dba989cecb40e294231a7ed4b06 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 22 Sep 2022 01:10:53 +0200 Subject: [PATCH 131/156] Correct update to named ranges and formulae when inserting/deleting columns/rows --- src/PhpSpreadsheet/ReferenceHelper.php | 31 ++++- .../ReferenceHelperTest.php | 109 ++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 822b754a..16323ed5 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -538,11 +538,7 @@ class ReferenceHelper // Update workbook: define names if (count($worksheet->getParent()->getDefinedNames()) > 0) { - foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $definedName->setValue($this->updateCellReference($definedName->getValue())); - } - } + $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); } // Garbage collect @@ -1163,4 +1159,29 @@ class ReferenceHelper { throw new Exception('Cloning a Singleton is not allowed!'); } + + public function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { + if ($definedName->isFormula() === false) { + $cellAddress = $definedName->getValue(); + $asFormula = ($cellAddress[0] === '='); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($asFormula === true) { + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } else { + $definedName->setValue($asFormula . $this->updateCellReference(ltrim($cellAddress, '='))); + } + } + } else { + $formula = $definedName->getValue(); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } + } + } + } } diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index fbd60155..ac686a29 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -2,9 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; use PhpOffice\PhpSpreadsheet\Comment; +use PhpOffice\PhpSpreadsheet\NamedFormula; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; @@ -538,4 +541,110 @@ class ReferenceHelperTest extends TestCase $printArea = $sheet->getPageSetup()->getPrintArea(); self::assertSame('A1:H10', $printArea); } + + public function testInsertRowsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->insertNewRowBefore(4, 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A8', $firstColumn->getRange()); + self::assertSame('=Data!B$2:B8', $secondColumn->getRange()); + self::assertSame(30, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(25, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(750, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + public function testInsertColumnsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->insertNewColumnBefore('B', 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A6', $firstColumn->getRange()); + self::assertSame('=Data!D$2:D6', $secondColumn->getRange()); + self::assertSame(30, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(25, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(750, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + public function testDeleteRowsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->removeRow(3, 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A4', $firstColumn->getRange()); + self::assertSame('=Data!B$2:B4', $secondColumn->getRange()); + self::assertSame(20, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(17, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(340, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + private function buildDefinedNamesTestWorkbook(): Spreadsheet + { + $spreadsheet = new Spreadsheet(); + $dataSheet = $spreadsheet->getActiveSheet(); + $dataSheet->setTitle('Data'); + + $totalsSheet = $spreadsheet->addSheet(new Worksheet()); + $totalsSheet->setTitle('Totals'); + + $spreadsheet->setActiveSheetIndexByName('Data'); + + $dataSheet->fromArray([['Column 1', 'Column 2'], [2, 1], [4, 3], [6, 5], [8, 7], [10, 9]], null, 'A1', true); + + $spreadsheet->addNamedRange( + new NamedRange('FirstColumn', $spreadsheet->getActiveSheet(), '=Data!$A$2:$A6') + ); + $spreadsheet->addNamedFormula( + new NamedFormula('FirstTotal', $spreadsheet->getActiveSheet(), '=SUM(FirstColumn)') + ); + $totalsSheet->setCellValue('A20', '=FirstTotal'); + + $spreadsheet->addNamedRange( + new NamedRange('SecondColumn', $spreadsheet->getActiveSheet(), '=Data!B$2:B6') + ); + $spreadsheet->addNamedFormula( + new NamedFormula('SecondTotal', $spreadsheet->getActiveSheet(), '=SUM(SecondColumn)') + ); + $totalsSheet->setCellValue('B20', '=SecondTotal'); + + $spreadsheet->addNamedFormula( + new NamedFormula('ProductTotal', $spreadsheet->getActiveSheet(), '=FirstTotal*SecondTotal') + ); + $totalsSheet->setCellValue('D20', '=ProductTotal'); + + return $spreadsheet; + } } From d682f2f95e2c1b9a6c663a8fb2a64fe6ea35e257 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 23 Sep 2022 14:03:38 +0200 Subject: [PATCH 132/156] Correct update to named ranges and formulae when inserting/deleting columns/rows --- CHANGELOG.md | 1 + src/PhpSpreadsheet/DefinedName.php | 2 +- src/PhpSpreadsheet/ReferenceHelper.php | 64 +++++++++++++--------- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 225ea2af..0f731ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077) - Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) - Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) diff --git a/src/PhpSpreadsheet/DefinedName.php b/src/PhpSpreadsheet/DefinedName.php index 3b874b43..464fa8e3 100644 --- a/src/PhpSpreadsheet/DefinedName.php +++ b/src/PhpSpreadsheet/DefinedName.php @@ -150,7 +150,7 @@ abstract class DefinedName // New title $newTitle = $this->name; - ReferenceHelper::getInstance()->updateNamedFormulas($this->worksheet->getParent(), $oldTitle, $newTitle); + ReferenceHelper::getInstance()->updateNamedFormulae($this->worksheet->getParent(), $oldTitle, $newTitle); } return $this; diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 16323ed5..1d2ab6a0 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -862,13 +862,13 @@ class ReferenceHelper } /** - * Update named formulas (i.e. containing worksheet references / named ranges). + * Update named formulae (i.e. containing worksheet references / named ranges). * * @param Spreadsheet $spreadsheet Object to update * @param string $oldName Old name (name to replace) * @param string $newName New name */ - public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void + public function updateNamedFormulae(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void { if ($oldName == '') { return; @@ -889,6 +889,41 @@ class ReferenceHelper } } + private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { + if ($definedName->isFormula() === false) { + $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } else { + $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } + } + } + + private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + $cellAddress = $definedName->getValue(); + $asFormula = ($cellAddress[0] === '='); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($asFormula === true) { + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } else { + $definedName->setValue($asFormula . $this->updateCellReference(ltrim($cellAddress, '='))); + } + } + } + + private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } + } + /** * Update cell range. * @@ -1159,29 +1194,4 @@ class ReferenceHelper { throw new Exception('Cloning a Singleton is not allowed!'); } - - public function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void - { - foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->isFormula() === false) { - $cellAddress = $definedName->getValue(); - $asFormula = ($cellAddress[0] === '='); - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - if ($asFormula === true) { - $formula = $definedName->getValue(); - $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); - $definedName->setValue($formula); - } else { - $definedName->setValue($asFormula . $this->updateCellReference(ltrim($cellAddress, '='))); - } - } - } else { - $formula = $definedName->getValue(); - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); - $definedName->setValue($formula); - } - } - } - } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 55327932..8235ccf1 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -923,7 +923,7 @@ class Worksheet implements IComparable $this->parent->getCalculationEngine() ->renameCalculationCacheForWorksheet($oldTitle, $newTitle); if ($updateFormulaCellReferences) { - ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle); + ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle); } } From eb91890f8bfc463685f3a29434054edb9fc7255c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 23 Sep 2022 15:34:18 +0200 Subject: [PATCH 133/156] Minor tweak --- src/PhpSpreadsheet/ReferenceHelper.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 1d2ab6a0..6ef87be2 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -906,11 +906,10 @@ class ReferenceHelper $asFormula = ($cellAddress[0] === '='); if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { if ($asFormula === true) { - $formula = $definedName->getValue(); - $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); $definedName->setValue($formula); } else { - $definedName->setValue($asFormula . $this->updateCellReference(ltrim($cellAddress, '='))); + $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='))); } } } From 77a064f58d1bb58e3685e144f8f19d269c264610 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 13:02:01 +0200 Subject: [PATCH 134/156] Minor tweak --- src/PhpSpreadsheet/ReferenceHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 6ef87be2..3f53ed1d 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -877,7 +877,7 @@ class ReferenceHelper foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getCoordinates(false) as $coordinate) { $cell = $sheet->getCell($coordinate); - if (($cell !== null) && ($cell->getDataType() === DataType::TYPE_FORMULA)) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { $formula = $cell->getValue(); if (strpos($formula, $oldName) !== false) { $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); From 86f3730ded48d0223fda78f3fb630e1e9ca59917 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 13:06:17 +0200 Subject: [PATCH 135/156] 1.25.0 - 2022-09-25 ### Added - Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions - Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. - Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes. ### Changed - Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) - Merge methods now allow an additional `$behaviour` argument. Permitted values are: - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell ### Deprecated - Axis getLineProperty deprecated in favor of getLineColorProperty. - Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated. - Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated. - ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed). - Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT. - Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead. ### Removed - Nothing ### Fixed - Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077) - Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) - Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) - Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) - Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960) - Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971) - Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950) - Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976) - Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) - Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) - Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) - Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016) - Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018) - Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048) - R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052) - Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f731ba4..a871d0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## Unreleased - TBD +## 1.25.0 - 2022-09-25 ### Added From 5f644c0144ffa81be9f67881a6df6d5f7e91522f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 13:29:42 +0200 Subject: [PATCH 136/156] Prepare Change Log for next release --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a871d0f8..bfad2d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## Unreleased - TBD + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + + ## 1.25.0 - 2022-09-25 ### Added From 46beabbefec56f7682fc0053866e90ab8b7189b8 Mon Sep 17 00:00:00 2001 From: BrokenSourceCode <59090546+BrokenSourceCode@users.noreply.github.com> Date: Sun, 25 Sep 2022 14:37:47 +0200 Subject: [PATCH 137/156] Fix `ezyang/htmlpurifier` caret version range --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 858bd819..e686788e 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", - "ezyang/htmlpurifier": "4.15", + "ezyang/htmlpurifier": "^4.15", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", From 8969b818868012bc1cc1ba5b6da7500d210d89fc Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 18:08:05 +0200 Subject: [PATCH 138/156] Minor tweak for phpstan and pathinfo return array --- composer.lock | 78 +++++++++++++--------------- src/PhpSpreadsheet/Helper/Sample.php | 2 +- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/composer.lock b/composer.lock index 22513999..900c3153 100644 --- a/composer.lock +++ b/composer.lock @@ -1124,24 +1124,24 @@ }, { "name": "dompdf/dompdf", - "version": "v2.0.0", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "79573d8b8a141ec8a17312515de8740eed014fa9" + "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/79573d8b8a141ec8a17312515de8740eed014fa9", - "reference": "79573d8b8a141ec8a17312515de8740eed014fa9", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c5310df0e22c758c85ea5288175fc6cd777bc085", + "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", "masterminds/html5": "^2.0", - "phenx/php-font-lib": "^0.5.4", - "phenx/php-svg-lib": "^0.3.3 || ^0.4.0", + "phenx/php-font-lib": ">=0.5.4 <1.0.0", + "phenx/php-svg-lib": ">=0.3.3 <1.0.0", "php": "^7.1 || ^8.0" }, "require-dev": { @@ -1172,25 +1172,17 @@ ], "authors": [ { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" - }, - { - "name": "Brian Sweeney", - "email": "eclecticgeek@gmail.com" - }, - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com" + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" } ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v2.0.0" + "source": "https://github.com/dompdf/dompdf/tree/v2.0.1" }, - "time": "2022-06-21T21:14:57+00:00" + "time": "2022-09-22T13:43:41+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -1795,21 +1787,21 @@ }, { "name": "phenx/php-svg-lib", - "version": "0.4.1", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "4498b5df7b08e8469f0f8279651ea5de9626ed02" + "reference": "76876c6cf3080bcb6f249d7d59705108166a6685" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/4498b5df7b08e8469f0f8279651ea5de9626ed02", - "reference": "4498b5df7b08e8469f0f8279651ea5de9626ed02", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/76876c6cf3080bcb6f249d7d59705108166a6685", + "reference": "76876c6cf3080bcb6f249d7d59705108166a6685", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^7.1 || ^7.2 || ^7.3 || ^7.4 || ^8.0", + "php": "^7.1 || ^8.0", "sabberworm/php-css-parser": "^8.4" }, "require-dev": { @@ -1835,9 +1827,9 @@ "homepage": "https://github.com/PhenX/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.0" }, - "time": "2022-03-07T12:52:04+00:00" + "time": "2022-09-06T12:16:56+00:00" }, { "name": "php-http/message-factory", @@ -1957,16 +1949,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.5", + "version": "1.8.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" + "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", - "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c386ab2741e64cc9e21729f891b28b2b10fe6618", + "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618", "shasum": "" }, "require": { @@ -1996,7 +1988,7 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.5" + "source": "https://github.com/phpstan/phpstan/tree/1.8.6" }, "funding": [ { @@ -2012,7 +2004,7 @@ "type": "tidelift" } ], - "time": "2022-09-07T16:05:32+00:00" + "time": "2022-09-23T09:54:39+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -2386,16 +2378,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.24", + "version": "9.5.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", - "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", "shasum": "" }, "require": { @@ -2417,14 +2409,14 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.1", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, "suggest": { @@ -2468,7 +2460,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" }, "funding": [ { @@ -2478,9 +2470,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-08-30T07:42:16+00:00" + "time": "2022-09-25T03:44:45+00:00" }, { "name": "psr/cache", @@ -5278,5 +5274,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 9f7563d6..0ac0c796 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -85,7 +85,7 @@ class Sample foreach ($regex as $file) { $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); $info = pathinfo($file); - $category = str_replace('_', ' ', $info['dirname']); + $category = str_replace('_', ' ', $info['dirname'] ?? ''); $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); if (!in_array($category, ['.', 'boostrap', 'templates'])) { if (!isset($files[$category])) { From b9bd55a07403b2b6b600545226c59ebc18d79ab5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 18:08:55 +0200 Subject: [PATCH 139/156] Minor tweak for phpstan and pathinfo return array --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index 900c3153..edd441de 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5062910e2e463ba84b1c753c58624ca9", + "content-hash": "61d142fc39389fd47139c4dc6f572d6b", "packages": [ { "name": "ezyang/htmlpurifier", - "version": "v4.15.0", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "8d9f4c9ec154922ff19690ffade9ed915b27a017" + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/8d9f4c9ec154922ff19690ffade9ed915b27a017", - "reference": "8d9f4c9ec154922ff19690ffade9ed915b27a017", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", "shasum": "" }, "require": { @@ -63,9 +63,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.15.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" }, - "time": "2022-09-18T06:23:57+00:00" + "time": "2022-09-18T07:06:19+00:00" }, { "name": "maennchen/zipstream-php", From 1c56fdc0f3078c9837cea56cf8cd53378e6e2325 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 18:42:06 +0200 Subject: [PATCH 140/156] Change Log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfad2d3d..4e62b180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing +- Composer dependency clash with ezyang/htmlpurifier ## 1.25.0 - 2022-09-25 From bfd6e640fac2160a316353e3cafaeb605112b9b5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 19:14:46 +0200 Subject: [PATCH 141/156] Change Log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e62b180..6a42e4af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## Unreleased - TBD +## 1.25.1 - 2022-09-25 ### Added From a317a09e7def49852400a4b3eca4a4b0790ceeb5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 19:21:01 +0200 Subject: [PATCH 142/156] Change Log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a42e4af..84bb504e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## 1.25.1 - 2022-09-25 +## 1.25.2 - 2022-09-25 ### Added From 341eb62afe77599c40c359dfa42b849848a3befe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 25 Sep 2022 19:38:30 +0200 Subject: [PATCH 143/156] Prepare Change Log for next release --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84bb504e..9bf3d568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## Unreleased - TBD + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Nothing + + ## 1.25.2 - 2022-09-25 ### Added From bb500e2c346e65a8f241f8c9316688ba509dedca Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 27 Sep 2022 15:52:44 +0200 Subject: [PATCH 144/156] Scrutinizer tweak --- src/PhpSpreadsheet/Calculation/Calculation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 4f95af54..5725795e 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4866,7 +4866,7 @@ class Calculation } } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on - if ($pCellParent) { + if ($cell !== null && $pCellParent !== null) { $cell->attach($pCellParent); } From a193f36f311985a33394db2c2b5c79615d6c660f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 27 Sep 2022 22:13:19 -0700 Subject: [PATCH 145/156] More Carets in composer.json (#3090) * More Carets in composer.json Following up on PR #3086, there are 3 additional items in the require-dev section that should have carets. I probably accidentally removed them for tcpdf and mitoteam, which point to the current release anyhow. Dependabot appears to be responsible for mpdf, which is not pointing to the current release, but I have tested with current successfully. * Minor Fix for Change Made Earlier Today "Scrutinizer Tweak" causes a Phpstan error. Fix baseline to correct it. --- composer.json | 6 +++--- phpstan-baseline.neon | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index e686788e..4f44b8bd 100644 --- a/composer.json +++ b/composer.json @@ -81,14 +81,14 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "10.2.4", - "mpdf/mpdf": "8.1.1", + "mitoteam/jpgraph": "^10.2.4", + "mpdf/mpdf": "^8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "6.5" + "tecnickcom/tcpdf": "^6.5" }, "suggest": { "ext-intl": "PHP Internationalization Functions", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c4b76cdd..b6c9deeb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,11 +5,6 @@ parameters: count: 3 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Cannot call method attach\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Cannot call method cellExists\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" count: 4 From 050a42db8e292ef9637ddc4dee6f572ac4aafc2b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 28 Sep 2022 00:14:37 -0700 Subject: [PATCH 146/156] Xlsx Reader External Data Validations Flag Missing (#3078) * Xlsx Reader External Data Validations Flag Missing Fix #2677. This PR supersedes #2679, written by @technghiath, which lacks tests, and probably doesn't solve the problem entirely. The code causing the problem appears to be the last remnant in Xlsx Reader which calls `children` using a namespace prefix rather than a namespace. That is changed, and tests are added where the tag is unexpectedly missing, and also where it uses a non-standard namespace prefix. * Scrutinizer Reports 1 "new" error. It isn't, but fix it anyhow. * Fix One Existing Scrutinizer Problem Only remaining problem in Reader/Xlsx. --- src/PhpSpreadsheet/Reader/Xlsx.php | 15 +++-- src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php | 4 ++ .../Reader/Xlsx/DataValidationTest.php | 58 ++++++++++++++++++ .../Reader/Xlsx/XlsxTest.php | 37 ----------- .../Reader/XLSX/issue.2677.namespace.xlsx | Bin 0 -> 10987 bytes .../XLSX/issue.2677.removeformula1.xlsx | Bin 0 -> 10988 bytes 6 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php create mode 100644 tests/data/Reader/XLSX/issue.2677.namespace.xlsx create mode 100644 tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index e2fae121..236ce83b 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -469,6 +469,7 @@ class Xlsx extends BaseReader $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); $propertyReader = new PropertyReader($this->securityScanner, $excel->getProperties()); + $chartDetails = []; foreach ($rels->Relationship as $relx) { $rel = self::getAttributes($relx); $relTarget = (string) $rel['Target']; @@ -929,13 +930,16 @@ class Xlsx extends BaseReader $xmlSheet->addChild('dataValidations'); } - foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) { + foreach ($xmlSheet->extLst->ext->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) { + $item = self::testSimpleXml($item); $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation'); foreach ($item->attributes() ?? [] as $attr) { $node->addAttribute($attr->getName(), $attr); } - $node->addAttribute('sqref', $item->children('xm', true)->sqref); - $node->addChild('formula1', $item->formula1->children('xm', true)->f); + $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref); + if (isset($item->formula1)) { + $node->addChild('formula1', $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f); + } } } @@ -1278,6 +1282,7 @@ class Xlsx extends BaseReader if ($xmlDrawingChildren->oneCellAnchor) { foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) { + $oneCellAnchor = self::testSimpleXml($oneCellAnchor); if ($oneCellAnchor->pic->blipFill) { /** @var SimpleXMLElement $blip */ $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; @@ -1285,8 +1290,6 @@ class Xlsx extends BaseReader $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; /** @var SimpleXMLElement $outerShdw */ $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; - /** @var SimpleXMLElement $hlinkClick */ - $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $objDrawing->setName((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name')); @@ -1363,11 +1366,11 @@ class Xlsx extends BaseReader } if ($xmlDrawingChildren->twoCellAnchor) { foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) { + $twoCellAnchor = self::testSimpleXml($twoCellAnchor); if ($twoCellAnchor->pic->blipFill) { $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; - $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); /** @scrutinizer ignore-call */ $editAs = $twoCellAnchor->attributes(); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index 57a88bb0..7f484c2f 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -58,6 +58,10 @@ class Namespaces const VBA = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject'; + const DATA_VALIDATIONS1 = 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'; + + const DATA_VALIDATIONS2 = 'http://schemas.microsoft.com/office/excel/2006/main'; + const DC_ELEMENTS = 'http://purl.org/dc/elements/1.1/'; const DC_TERMS = 'http://purl.org/dc/terms'; diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php new file mode 100644 index 00000000..ddff17d6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php @@ -0,0 +1,58 @@ +load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + + self::assertTrue($worksheet->getCell('B3')->hasDataValidation()); + $spreadsheet->disconnectWorksheets(); + } + + /** + * Test for load drop down lists of another sheet. + * Pull #2150, issue #2149. Also issue #2677. + * + * @dataProvider providerExternalSheet + */ + public function testDataValidationOfAnotherSheet(string $expectedB14, string $filename): void + { + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + + // same sheet + $validationCell = $worksheet->getCell('B5'); + self::assertTrue($validationCell->hasDataValidation()); + self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); + self::assertSame('$A$5:$A$7', $validationCell->getDataValidation()->getFormula1()); + + // another sheet + $validationCell = $worksheet->getCell('B14'); + self::assertTrue($validationCell->hasDataValidation()); + self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); + self::assertSame($expectedB14, $validationCell->getDataValidation()->getFormula1()); + $spreadsheet->disconnectWorksheets(); + } + + public function providerExternalSheet(): array + { + return [ + 'standard spreadsheet' => ['Feuil2!$A$3:$A$5', 'tests/data/Reader/XLSX/dataValidation2Test.xlsx'], + 'alternate namespace prefix' => ['Feuil2!$A$3:$A$5', 'tests/data/Reader/XLSX/issue.2677.namespace.xlsx'], + 'missing formula' => ['', 'tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php index 6b0ebf67..3050fa72 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\File; @@ -163,42 +162,6 @@ class XlsxTest extends TestCase self::assertInstanceOf(Style::class, $conditionalRule->getStyle()); } - public function testLoadXlsxDataValidation(): void - { - $filename = 'tests/data/Reader/XLSX/dataValidationTest.xlsx'; - $reader = new Xlsx(); - $spreadsheet = $reader->load($filename); - - $worksheet = $spreadsheet->getActiveSheet(); - - self::assertTrue($worksheet->getCell('B3')->hasDataValidation()); - } - - /* - * Test for load drop down lists of another sheet. - * Pull #2150, issue #2149 - */ - public function testLoadXlsxDataValidationOfAnotherSheet(): void - { - $filename = 'tests/data/Reader/XLSX/dataValidation2Test.xlsx'; - $reader = new Xlsx(); - $spreadsheet = $reader->load($filename); - - $worksheet = $spreadsheet->getActiveSheet(); - - // same sheet - $validationCell = $worksheet->getCell('B5'); - self::assertTrue($validationCell->hasDataValidation()); - self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); - self::assertSame('$A$5:$A$7', $validationCell->getDataValidation()->getFormula1()); - - // another sheet - $validationCell = $worksheet->getCell('B14'); - self::assertTrue($validationCell->hasDataValidation()); - self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); - self::assertSame('Feuil2!$A$3:$A$5', $validationCell->getDataValidation()->getFormula1()); - } - /** * Test load Xlsx file without cell reference. * diff --git a/tests/data/Reader/XLSX/issue.2677.namespace.xlsx b/tests/data/Reader/XLSX/issue.2677.namespace.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8ed2eaab0fdbef450f4cf29b00ddf78c72578b41 GIT binary patch literal 10987 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx b/tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b7f6cc5cebf77691c529937d8cf0c09f6d42fe35 GIT binary patch literal 10988 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 z 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 f043F5?)|3Q{21i6~#=fD34U@Jfk literal 0 HcmV?d00001 From b8201a79c56b546b5ff32979c32fffd7e0a43c21 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 28 Sep 2022 00:46:24 -0700 Subject: [PATCH 147/156] Sync mpdf in composer.lock (#3091) Composer was complaining locally. --- composer.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index edd441de..9a425e87 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "61d142fc39389fd47139c4dc6f572d6b", + "content-hash": "60da78cdd66329fbd4e0d50324363072", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1390,16 +1390,16 @@ }, { "name": "mpdf/mpdf", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "e511e89a66bdb066e3fbf352f00f4734d5064cbf" + "reference": "a8a22f4874157e490d41b486053a20bec42e182c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/e511e89a66bdb066e3fbf352f00f4734d5064cbf", - "reference": "e511e89a66bdb066e3fbf352f00f4734d5064cbf", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/a8a22f4874157e490d41b486053a20bec42e182c", + "reference": "a8a22f4874157e490d41b486053a20bec42e182c", "shasum": "" }, "require": { @@ -1463,7 +1463,7 @@ "type": "custom" } ], - "time": "2022-04-18T11:50:28+00:00" + "time": "2022-08-15T08:15:09+00:00" }, { "name": "myclabs/deep-copy", @@ -5274,5 +5274,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } From 35b42cc180e0119fc19917f5f8ef1ac394fc99a5 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 30 Sep 2022 09:00:13 -0700 Subject: [PATCH 148/156] Phpstan Baseline Fixes 2022-09-21 (#3080) * Phpstan Baseline Fixes 2022-09-21 Eliminate about 200 more lines from Phpstan baseline. For Helper/Sample and Helper/Html and others, many properties are declared as protected despite the fact that the classes do not extend any other class, and there are no classes which extend them. They are changed to private; this could be a breaking change in circumstances for which I cannot think of a use case (user extends class for some reason). * Slightly Botched Merge Commit Hope this fixes phpstan. --- phpstan-baseline.neon | 194 +----------------- samples/Calculations/Financial/FV.php | 2 +- src/PhpSpreadsheet/Helper/Html.php | 173 +++++++++------- src/PhpSpreadsheet/Helper/Sample.php | 17 +- src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php | 8 +- src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php | 8 +- .../Reader/Xls/Color/BuiltIn.php | 8 +- src/PhpSpreadsheet/Reader/Xls/ErrorCode.php | 8 +- src/PhpSpreadsheet/Reader/Xls/RC4.php | 6 +- src/PhpSpreadsheet/Shared/TimeZone.php | 4 +- .../Reader/Html/HtmlTest.php | 5 + .../Reader/Xls/ColorMapTest.php | 34 +++ .../Reader/Xls/ErrorCodeMapTest.php | 33 +++ 13 files changed, 200 insertions(+), 300 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b6c9deeb..3927d672 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1110,156 +1110,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/DefinedName.php - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:startFontTag\\(\\) has parameter \\$tag with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$bold has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$color has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$colourMap has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$endTagCallbacks has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$face has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$italic has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$size has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$stack has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$startTagCallbacks has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$strikethrough has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$stringData has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$subscript has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$superscript has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$underline has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Sample\\:\\:getSamples\\(\\) should return array\\\\> but returns array\\\\>\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Sample\\:\\:log\\(\\) has parameter \\$message with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#1 \\$directory of class RecursiveDirectoryIterator constructor expects string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#1 \\$filename of function unlink expects string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#1 \\$path of function pathinfo expects string, array\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\IOFactory\\:\\:createReader\\(\\) should return PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\IReader but returns object\\.$#" count: 1 @@ -1619,47 +1469,12 @@ parameters: message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 8 path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\Color\\\\BIFF5\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\Color\\\\BIFF8\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\Color\\\\BuiltIn\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\ErrorCode\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/ErrorCode.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\RC4\\:\\:\\$i has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\RC4\\:\\:\\$j has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\RC4\\:\\:\\$s has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/RC4.php - + - message: "#^Cannot access property \\$r on SimpleXMLElement\\|null\\.$#" count: 2 path: src/PhpSpreadsheet/Reader/Xlsx.php - + - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" count: 1 @@ -2145,11 +1960,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLERead.php - - - message: "#^Static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:validateTimeZone\\(\\) is unused\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/TimeZone.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Trend\\\\BestFit\\:\\:calculateGoodnessOfFit\\(\\) has parameter \\$const with no type specified\\.$#" count: 1 diff --git a/samples/Calculations/Financial/FV.php b/samples/Calculations/Financial/FV.php index 31ddb630..40b3366f 100644 --- a/samples/Calculations/Financial/FV.php +++ b/samples/Calculations/Financial/FV.php @@ -32,5 +32,5 @@ $worksheet->getStyle('B8:C8')->getNumberFormat()->setFormatCode('$#,##0.00'); $helper->log($worksheet->getCell('B8')->getValue()); $helper->log('FV() Result is ' . $worksheet->getCell('B8')->getFormattedValue()); -$helper->log($worksheet->getCell('C6')->getValue()); +$helper->log($worksheet->getCell('C8')->getValue()); $helper->log('FV() Result is ' . $worksheet->getCell('C8')->getFormattedValue()); diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index 632efebc..b8ed386a 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -12,7 +12,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font; class Html { - protected static $colourMap = [ + private const COLOUR_MAP = [ 'aliceblue' => 'f0f8ff', 'antiquewhite' => 'faebd7', 'antiquewhite1' => 'ffefdb', @@ -532,25 +532,34 @@ class Html 'yellowgreen' => '9acd32', ]; - protected $face; + /** @var ?string */ + private $face; - protected $size; + /** @var ?string */ + private $size; - protected $color; + /** @var ?string */ + private $color; - protected $bold = false; + /** @var bool */ + private $bold = false; - protected $italic = false; + /** @var bool */ + private $italic = false; - protected $underline = false; + /** @var bool */ + private $underline = false; - protected $superscript = false; + /** @var bool */ + private $superscript = false; - protected $subscript = false; + /** @var bool */ + private $subscript = false; - protected $strikethrough = false; + /** @var bool */ + private $strikethrough = false; - protected $startTagCallbacks = [ + private const START_TAG_CALLBACKS = [ 'font' => 'startFontTag', 'b' => 'startBoldTag', 'strong' => 'startBoldTag', @@ -563,7 +572,7 @@ class Html 'sub' => 'startSubscriptTag', ]; - protected $endTagCallbacks = [ + private const END_TAG_CALLBACKS = [ 'font' => 'endFontTag', 'b' => 'endBoldTag', 'strong' => 'endBoldTag', @@ -584,16 +593,18 @@ class Html 'h6' => 'breakTag', ]; - protected $stack = []; + /** @var array */ + private $stack = []; - protected $stringData = ''; + /** @var string */ + private $stringData = ''; /** * @var RichText */ - protected $richTextObject; + private $richTextObject; - protected function initialise(): void + private function initialise(): void { $this->face = $this->size = $this->color = null; $this->bold = $this->italic = $this->underline = $this->superscript = $this->subscript = $this->strikethrough = false; @@ -633,7 +644,7 @@ class Html return $this->richTextObject; } - protected function cleanWhitespace(): void + private function cleanWhitespace(): void { foreach ($this->richTextObject->getRichTextElements() as $key => $element) { $text = $element->getText(); @@ -647,7 +658,7 @@ class Html } } - protected function buildTextRun(): void + private function buildTextRun(): void { $text = $this->stringData; if (trim($text) === '') { @@ -655,37 +666,40 @@ class Html } $richtextRun = $this->richTextObject->createTextRun($this->stringData); - if ($this->face) { - $richtextRun->getFont()->setName($this->face); - } - if ($this->size) { - $richtextRun->getFont()->setSize($this->size); - } - if ($this->color) { - $richtextRun->getFont()->setColor(new Color('ff' . $this->color)); - } - if ($this->bold) { - $richtextRun->getFont()->setBold(true); - } - if ($this->italic) { - $richtextRun->getFont()->setItalic(true); - } - if ($this->underline) { - $richtextRun->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - } - if ($this->superscript) { - $richtextRun->getFont()->setSuperscript(true); - } - if ($this->subscript) { - $richtextRun->getFont()->setSubscript(true); - } - if ($this->strikethrough) { - $richtextRun->getFont()->setStrikethrough(true); + $font = $richtextRun->getFont(); + if ($font !== null) { + if ($this->face) { + $font->setName($this->face); + } + if ($this->size) { + $font->setSize($this->size); + } + if ($this->color) { + $font->setColor(new Color('ff' . $this->color)); + } + if ($this->bold) { + $font->setBold(true); + } + if ($this->italic) { + $font->setItalic(true); + } + if ($this->underline) { + $font->setUnderline(Font::UNDERLINE_SINGLE); + } + if ($this->superscript) { + $font->setSuperscript(true); + } + if ($this->subscript) { + $font->setSubscript(true); + } + if ($this->strikethrough) { + $font->setStrikethrough(true); + } } $this->stringData = ''; } - protected function rgbToColour(string $rgbValue): string + private function rgbToColour(string $rgbValue): string { preg_match_all('/\d+/', $rgbValue, $values); foreach ($values[0] as &$value) { @@ -697,100 +711,103 @@ class Html public static function colourNameLookup(string $colorName): string { - return self::$colourMap[$colorName] ?? ''; + return self::COLOUR_MAP[$colorName] ?? ''; } - protected function startFontTag($tag): void + private function startFontTag(DOMElement $tag): void { - foreach ($tag->attributes as $attribute) { - $attributeName = strtolower($attribute->name); - $attributeValue = $attribute->value; + $attrs = $tag->attributes; + if ($attrs !== null) { + foreach ($attrs as $attribute) { + $attributeName = strtolower($attribute->name); + $attributeValue = $attribute->value; - if ($attributeName == 'color') { - if (preg_match('/rgb\s*\(/', $attributeValue)) { - $this->$attributeName = $this->rgbToColour($attributeValue); - } elseif (strpos(trim($attributeValue), '#') === 0) { - $this->$attributeName = ltrim($attributeValue, '#'); + if ($attributeName == 'color') { + if (preg_match('/rgb\s*\(/', $attributeValue)) { + $this->$attributeName = $this->rgbToColour($attributeValue); + } elseif (strpos(trim($attributeValue), '#') === 0) { + $this->$attributeName = ltrim($attributeValue, '#'); + } else { + $this->$attributeName = static::colourNameLookup($attributeValue); + } } else { - $this->$attributeName = static::colourNameLookup($attributeValue); + $this->$attributeName = $attributeValue; } - } else { - $this->$attributeName = $attributeValue; } } } - protected function endFontTag(): void + private function endFontTag(): void { $this->face = $this->size = $this->color = null; } - protected function startBoldTag(): void + private function startBoldTag(): void { $this->bold = true; } - protected function endBoldTag(): void + private function endBoldTag(): void { $this->bold = false; } - protected function startItalicTag(): void + private function startItalicTag(): void { $this->italic = true; } - protected function endItalicTag(): void + private function endItalicTag(): void { $this->italic = false; } - protected function startUnderlineTag(): void + private function startUnderlineTag(): void { $this->underline = true; } - protected function endUnderlineTag(): void + private function endUnderlineTag(): void { $this->underline = false; } - protected function startSubscriptTag(): void + private function startSubscriptTag(): void { $this->subscript = true; } - protected function endSubscriptTag(): void + private function endSubscriptTag(): void { $this->subscript = false; } - protected function startSuperscriptTag(): void + private function startSuperscriptTag(): void { $this->superscript = true; } - protected function endSuperscriptTag(): void + private function endSuperscriptTag(): void { $this->superscript = false; } - protected function startStrikethruTag(): void + private function startStrikethruTag(): void { $this->strikethrough = true; } - protected function endStrikethruTag(): void + private function endStrikethruTag(): void { $this->strikethrough = false; } - protected function breakTag(): void + private function breakTag(): void { $this->stringData .= "\n"; } - protected function parseTextNode(DOMText $textNode): void + private function parseTextNode(DOMText $textNode): void { $domText = (string) preg_replace( '/\s+/u', @@ -804,7 +821,7 @@ class Html /** * @param string $callbackTag */ - protected function handleCallback(DOMElement $element, $callbackTag, array $callbacks): void + private function handleCallback(DOMElement $element, $callbackTag, array $callbacks): void { if (isset($callbacks[$callbackTag])) { $elementHandler = $callbacks[$callbackTag]; @@ -815,20 +832,20 @@ class Html } } - protected function parseElementNode(DOMElement $element): void + private function parseElementNode(DOMElement $element): void { $callbackTag = strtolower($element->nodeName); $this->stack[] = $callbackTag; - $this->handleCallback($element, $callbackTag, $this->startTagCallbacks); + $this->handleCallback($element, $callbackTag, self::START_TAG_CALLBACKS); $this->parseElements($element); array_pop($this->stack); - $this->handleCallback($element, $callbackTag, $this->endTagCallbacks); + $this->handleCallback($element, $callbackTag, self::END_TAG_CALLBACKS); } - protected function parseElements(DOMNode $element): void + private function parseElements(DOMNode $element): void { foreach ($element->childNodes as $child) { if ($child instanceof DOMText) { diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 0ac0c796..a0063bd3 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -77,6 +77,11 @@ class Sample { // Populate samples $baseDir = realpath(__DIR__ . '/../../../samples'); + if ($baseDir === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException('realpath returned false'); + // @codeCoverageIgnoreEnd + } $directory = new RecursiveDirectoryIterator($baseDir); $iterator = new RecursiveIteratorIterator($directory); $regex = new RegexIterator($iterator, '/^.+\.php$/', RecursiveRegexIterator::GET_MATCH); @@ -84,6 +89,11 @@ class Sample $files = []; foreach ($regex as $file) { $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); + if (is_array($file)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('str_replace returned array'); + // @codeCoverageIgnoreEnd + } $info = pathinfo($file); $category = str_replace('_', ' ', $info['dirname'] ?? ''); $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); @@ -172,12 +182,17 @@ class Sample public function getTemporaryFilename($extension = 'xlsx') { $temporaryFilename = tempnam($this->getTemporaryFolder(), 'phpspreadsheet-'); + if ($temporaryFilename === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException('tempnam returned false'); + // @codeCoverageIgnoreEnd + } unlink($temporaryFilename); return $temporaryFilename . '.' . $extension; } - public function log($message): void + public function log(string $message): void { $eol = $this->isCli() ? PHP_EOL : '
'; echo date('H:i:s ') . $message . $eol; diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php index 743d9387..15d0b733 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BIFF5 { - protected static $map = [ + private const BIFF5_COLOR_MAP = [ 0x08 => '000000', 0x09 => 'FFFFFF', 0x0A => 'FF0000', @@ -72,10 +72,6 @@ class BIFF5 */ public static function lookup($color) { - if (isset(self::$map[$color])) { - return ['rgb' => self::$map[$color]]; - } - - return ['rgb' => '000000']; + return ['rgb' => self::BIFF5_COLOR_MAP[$color] ?? '000000']; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php index 5c109fb0..019ec79e 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BIFF8 { - protected static $map = [ + private const BIFF8_COLOR_MAP = [ 0x08 => '000000', 0x09 => 'FFFFFF', 0x0A => 'FF0000', @@ -72,10 +72,6 @@ class BIFF8 */ public static function lookup($color) { - if (isset(self::$map[$color])) { - return ['rgb' => self::$map[$color]]; - } - - return ['rgb' => '000000']; + return ['rgb' => self::BIFF8_COLOR_MAP[$color] ?? '000000']; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php b/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php index 90d50e33..b6a96af8 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BuiltIn { - protected static $map = [ + private const BUILTIN_COLOR_MAP = [ 0x00 => '000000', 0x01 => 'FFFFFF', 0x02 => 'FF0000', @@ -26,10 +26,6 @@ class BuiltIn */ public static function lookup($color) { - if (isset(self::$map[$color])) { - return ['rgb' => self::$map[$color]]; - } - - return ['rgb' => '000000']; + return ['rgb' => self::BUILTIN_COLOR_MAP[$color] ?? '000000']; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php b/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php index 7daf7230..0b79366b 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php +++ b/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; class ErrorCode { - protected static $map = [ + private const ERROR_CODE_MAP = [ 0x00 => '#NULL!', 0x07 => '#DIV/0!', 0x0F => '#VALUE!', @@ -23,10 +23,6 @@ class ErrorCode */ public static function lookup($code) { - if (isset(self::$map[$code])) { - return self::$map[$code]; - } - - return false; + return self::ERROR_CODE_MAP[$code] ?? false; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/RC4.php b/src/PhpSpreadsheet/Reader/Xls/RC4.php index 691aca7c..b7c7c900 100644 --- a/src/PhpSpreadsheet/Reader/Xls/RC4.php +++ b/src/PhpSpreadsheet/Reader/Xls/RC4.php @@ -4,11 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; class RC4 { - // Context - protected $s = []; + /** @var int[] */ + protected $s = []; // Context + /** @var int */ protected $i = 0; + /** @var int */ protected $j = 0; /** diff --git a/src/PhpSpreadsheet/Shared/TimeZone.php b/src/PhpSpreadsheet/Shared/TimeZone.php index 734c076d..324e3424 100644 --- a/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/src/PhpSpreadsheet/Shared/TimeZone.php @@ -35,7 +35,7 @@ class TimeZone */ public static function setTimeZone(string $timezoneName): bool { - if (self::validateTimezone($timezoneName)) { + if (self::validateTimeZone($timezoneName)) { self::$timezone = $timezoneName; return true; @@ -67,7 +67,7 @@ class TimeZone { $timezoneName = $timezoneName ?? self::$timezone; $dtobj = Date::dateTimeFromTimestamp("$timestamp"); - if (!self::validateTimezone($timezoneName)) { + if (!self::validateTimeZone($timezoneName)) { throw new PhpSpreadsheetException("Invalid timezone $timezoneName"); } $dtobj->setTimeZone(new DateTimeZone($timezoneName)); diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php index cabac403..ad395ace 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php @@ -80,6 +80,7 @@ class HtmlTest extends TestCase Blue background Unknown fore/background + Unknown fore/background '; $filename = HtmlHelper::createHtml($html); @@ -93,6 +94,10 @@ class HtmlTest extends TestCase self::assertEquals('000000', $style->getFont()->getColor()->getRGB()); self::assertEquals('000000', $style->getFill()->getEndColor()->getRGB()); self::assertEquals('FFFFFF', $style->getFill()->getstartColor()->getRGB()); + $style = $firstSheet->getCell('C1')->getStyle(); + self::assertEquals('f0f8ff', $style->getFont()->getColor()->getRGB()); + self::assertEquals('eedfcc', $style->getFill()->getEndColor()->getRGB()); + self::assertEquals('eedfcc', $style->getFill()->getstartColor()->getRGB()); } public function testCanApplyInlineFontStyles(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php new file mode 100644 index 00000000..977b852b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php @@ -0,0 +1,34 @@ + [0x00, '000000', '000000', '000000'], + 'non-default builtin' => [0x02, '000000', '000000', 'FF0000'], + 'system window text color' => [0x40, '000000', '000000', '000000'], + 'system window background color' => [0x41, '000000', '000000', 'FFFFFF'], + 'same biff5/8' => [0x09, 'FFFFFF', 'FFFFFF', '000000'], + 'different biff5/8' => [0x29, '69FFFF', 'CCFFFF', '000000'], + + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php new file mode 100644 index 00000000..e857d84f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php @@ -0,0 +1,33 @@ + Date: Sat, 1 Oct 2022 07:05:54 -0700 Subject: [PATCH 149/156] Cleanup 3 LookupRef Tests (#3097) Scrutinizer had previously suggested annotations for 3 LookupRef tests, but it no longer accepts its own annotation for those cases. This PR cleans them up. ColumnsTest and RowsTest are extremely straightforward. IndexTest is a bit more complicated, but only because, unlike the other two, it had no test which executed in the context of a spreadsheet. And, when I added those, I discovered a couple of bugs. INDEX always requires at least 2 parameters (row# is always required), but its entry in the function table specified 1-4 parameters, now changed to 2-4. And, omitting col# is not handled the same way as specifying 0 for col#, though the code had treated them identically. (The same would have been true for row# but, because it is now required, ...) --- .../Calculation/Calculation.php | 2 +- .../Calculation/LookupRef/Matrix.php | 6 +- .../Functions/LookupRef/ColumnsTest.php | 5 +- .../LookupRef/IndexOnSpreadsheetTest.php | 39 +++ .../Functions/LookupRef/IndexTest.php | 15 +- .../Functions/LookupRef/RowsTest.php | 5 +- .../LookupRef/INDEXonSpreadsheet.php | 234 ++++++++++++++++++ 7 files changed, 297 insertions(+), 9 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php create mode 100644 tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 5725795e..066ed0ee 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1450,7 +1450,7 @@ class Calculation 'INDEX' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, 'functionCall' => [LookupRef\Matrix::class, 'index'], - 'argumentCount' => '1-4', + 'argumentCount' => '2-4', ], 'INDIRECT' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index a447e203..d7d15d4e 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -76,13 +76,14 @@ class Matrix * If an array of values is passed as the $rowNum and/or $columnNum arguments, then the returned result * will also be an array with the same dimensions */ - public static function index($matrix, $rowNum = 0, $columnNum = 0) + public static function index($matrix, $rowNum = 0, $columnNum = null) { if (is_array($rowNum) || is_array($columnNum)) { return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $matrix, $rowNum, $columnNum); } $rowNum = $rowNum ?? 0; + $originalColumnNum = $columnNum; $columnNum = $columnNum ?? 0; try { @@ -102,6 +103,9 @@ class Matrix if ($columnNum > count($columnKeys)) { return ExcelError::REF(); } + if ($originalColumnNum === null && 1 < count($columnKeys)) { + return ExcelError::REF(); + } if ($columnNum === 0) { return self::extractRowValue($matrix, $rowKeys, $rowNum); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php index e8790cbf..67135af0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php @@ -12,10 +12,11 @@ class ColumnsTest extends TestCase * @dataProvider providerCOLUMNS * * @param mixed $expectedResult + * @param null|array|string $arg */ - public function testCOLUMNS($expectedResult, ...$args): void + public function testCOLUMNS($expectedResult, $arg): void { - $result = LookupRef::COLUMNS(/** @scrutinizer ignore-type */ ...$args); + $result = LookupRef::COLUMNS($arg); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php new file mode 100644 index 00000000..de3832b9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php @@ -0,0 +1,39 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray($matrix); + $maxRow = $sheet->getHighestRow(); + $maxColumn = $sheet->getHighestColumn(); + $formulaArray = "A1:$maxColumn$maxRow"; + if ($rowNum === null) { + $formula = "=INDEX($formulaArray)"; + } elseif ($colNum === null) { + $formula = "=INDEX($formulaArray, $rowNum)"; + } else { + $formula = "=INDEX($formulaArray, $rowNum, $colNum)"; + } + $sheet->getCell('ZZ98')->setValue('x'); + $sheet->getCell('ZZ99')->setValue($formula); + $result = $sheet->getCell('ZZ99')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerINDEXonSpreadsheet(): array + { + return require 'tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php index fa3f4848..3fe5d041 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix; use PHPUnit\Framework\TestCase; class IndexTest extends TestCase @@ -12,10 +12,19 @@ class IndexTest extends TestCase * @dataProvider providerINDEX * * @param mixed $expectedResult + * @param mixed $matrix + * @param mixed $rowNum + * @param mixed $colNum */ - public function testINDEX($expectedResult, ...$args): void + public function testINDEX($expectedResult, $matrix, $rowNum = null, $colNum = null): void { - $result = LookupRef::INDEX(/** @scrutinizer ignore-type */ ...$args); + if ($rowNum === null) { + $result = Matrix::index($matrix); + } elseif ($colNum === null) { + $result = Matrix::index($matrix, $rowNum); + } else { + $result = Matrix::index($matrix, $rowNum, $colNum); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php index fd9902f4..33c9dc45 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php @@ -12,10 +12,11 @@ class RowsTest extends TestCase * @dataProvider providerROWS * * @param mixed $expectedResult + * @param null|array|string $arg */ - public function testROWS($expectedResult, ...$args): void + public function testROWS($expectedResult, $arg): void { - $result = LookupRef::ROWS(/** @scrutinizer ignore-type */ ...$args); + $result = LookupRef::ROWS($arg); self::assertEquals($expectedResult, $result); } diff --git a/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php b/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php new file mode 100644 index 00000000..eff13722 --- /dev/null +++ b/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php @@ -0,0 +1,234 @@ + [ + 1, // Expected + [ + [1], + ], + 0, + ], + 'Row Number omitted' => [ + 'exception', // Expected + [ + [1], + ], + ], + 'Negative Row' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + -1, + ], + 'Row > matrix rows' => [ + '#REF!', // Expected + [ + [1], + [2], + ], + 10, + ], + 'Row is not a number' => [ + '#NAME?', // Expected + [ + [1], + [2], + ], + 'NaN', + ], + 'Row is reference to non-number' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + 'ZZ98', + ], + 'Row is quoted non-numeric result' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + '"string"', + ], + 'Row is Error' => [ + '#N/A', // Expected + [ + [1], + [2], + ], + '#N/A', + ], + 'Return row 2 only one column' => [ + 'xyz', // Expected + [ + ['abc'], + ['xyz'], + ], + 2, + ], + 'Return row 1 col 2' => [ + 'def', // Expected + [ + ['abc', 'def'], + ['xyz', 'tuv'], + ], + 1, + 2, + ], + 'Column number omitted from 2-column matrix' => [ + '#REF!', // Expected + [ + ['abc', 'def'], + ['xyz', 'tuv'], + ], + 1, + ], + 'Column number omitted from 1-column matrix' => [ + 'xyz', // Expected + [ + ['abc'], + ['xyz'], + ], + 2, + ], + 'Return row 2 from larger matrix (Phpspreadsheet flattens expected [2,4] to single value)' => [ + 2, // Expected + // Input + [ + [1, 3], + [2, 4], + ], + 2, + 0, + ], + 'Negative Column' => [ + '#VALUE!', // Expected + [ + [1, 3], + [2, 4], + ], + 0, + -1, + ], + 'Column > matrix columns' => [ + '#REF!', // Expected + [ + [1, 3], + [2, 4], + ], + 2, + 10, + ], + 'Column is not a number' => [ + '#NAME?', // Expected + [ + [1], + [2], + ], + 1, + 'NaN', + ], + 'Column is reference to non-number' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + 1, + 'ZZ98', + ], + 'Column is quoted non-number' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + 1, + '"string"', + ], + 'Column is Error' => [ + '#N/A', // Expected + [ + [1], + [2], + ], + 1, + '#N/A', + ], + 'Row 2 Column 2' => [ + 4, // Expected + [ + [1, 3], + [2, 4], + ], + 2, + 2, + ], + 'Row 2 Column 2 Alphabetic' => [ + 'Pears', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 2, + ], + 'Row 2 Column 1 Alphabetic' => [ + 'Bananas', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 1, + ], + 'Row 2 Column 0 (PhpSpreadsheet flattens result)' => [ + 'Bananas', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 0, + ], + 'Row 5 column 2' => [ + 3, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 5, + 2, + ], + 'Row 5 column 0 (flattened)' => [ + 8, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 5, + 0, + ], + 'Row 0 column 2 (flattened)' => [ + 6, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 0, + 2, + ], +]; From 66695881e45cda2eb5b8b4b650bca50a9a83f397 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 1 Oct 2022 08:40:27 -0700 Subject: [PATCH 150/156] Calculation suppressFormulaErrors - Minor Break and Deprecation (#3092) Fix #1531. This is a replacement for PR #3081 (see last paragraph below), which I will close. Calculation has a property `suppressFormulaErrors`, which really doesn't work as one might expect. If a calculation throws an exception, the setting of this property might prevent the Exception from being thrown, but it will still trigger an Error. I do not think this makes sense, and will change it so the calculation will return `false`, which is part of the original design but which would essentially never happen. This allows the user to save a corrupt spreadsheet, but this was already possible through the use of `setPreCalculateFormulas(false)` on the Writer, so this doesn't really open any new exposures. It nevertheless might be considered a breaking change because of the difference in behavior. Deprecation - the visibility of the existing property is public, which means it can be changed directly. A new private property is added with a public setter/getter. The new property will be used when the existing property is null (default), which will allow the existing property to be deprecated. Function getFunctions is changed to static - the array which it returns is static. Existing callers using it as non-static will still function correctly. Although I am enabling this ability, I don't necessarily think it's a good idea to make use of it. See the original issue for a discussion of why. It is not mentioned in the official documentation, and I will not be adding documentation for it. The originator discovered it by reading the code, and I think that is sufficient for what will often be an ill-advised choice. Many of the large number of problems with Calculation.php in phpstan baseline are addressed. PR 3081 ran afoul of something in phpstan. The changes in this ticket are more limited, adding a number of doc blocks but leaving executable code unchanged. --- phpstan-baseline.neon | 135 ------------------ .../Calculation/Calculation.php | 104 +++++++++++--- .../Calculation/CalculationErrorTest.php | 40 ++---- .../LocaleGeneratorTest.php | 6 +- .../Writer/Xlsx/CalculationErrorTest.php | 64 +++++++++ 5 files changed, 158 insertions(+), 191 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3927d672..3d6ede90 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -25,71 +25,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToEnglish\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToEnglish\\(\\) has parameter \\$formula with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToLocale\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToLocale\\(\\) has parameter \\$formula with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:dataTestReference\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:dataTestReference\\(\\) has parameter \\$operandData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:getTokensAsString\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:getTokensAsString\\(\\) has parameter \\$tokens with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:localeFunc\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:localeFunc\\(\\) has parameter \\$function with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:validateBinaryOperand\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:validateBinaryOperand\\(\\) has parameter \\$operand with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:validateBinaryOperand\\(\\) has parameter \\$stack with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Offset 'value' does not exist on array\\|null\\.$#" count: 5 @@ -105,81 +40,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Parameter \\#1 \\$str of function preg_quote expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Parameter \\#2 \\$worksheet of static method PhpOffice\\\\PhpSpreadsheet\\\\DefinedName\\:\\:resolveName\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$cellStack has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$comparisonOperators has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$controlFunctions has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$cyclicFormulaCell has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceFromExcel has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceFromLocale has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceToExcel has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceToLocale has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$localeFunctions has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$operatorAssociativity has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$operatorPrecedence has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$phpSpreadsheetFunctions has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$returnArrayAsType has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$spreadsheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 066ed0ee..abeebf39 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -58,6 +58,7 @@ class Calculation const FORMULA_CLOSE_MATRIX_BRACE = '}'; const FORMULA_STRING_QUOTE = '"'; + /** @var string */ private static $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE; /** @@ -136,9 +137,14 @@ class Calculation * If true, then a user error will be triggered * If false, then an exception will be thrown. * - * @var bool + * @var ?bool + * + * @deprecated 1.25.2 use setSuppressFormulaErrors() instead */ - public $suppressFormulaErrors = false; + public $suppressFormulaErrors; + + /** @var bool */ + private $suppressFormulaErrorsNew = false; /** * Error message for any error that was raised/thrown by the calculation engine. @@ -161,6 +167,7 @@ class Calculation */ private $cyclicReferenceStack; + /** @var array */ private $cellStack = []; /** @@ -172,6 +179,7 @@ class Calculation */ private $cyclicFormulaCounter = 1; + /** @var string */ private $cyclicFormulaCell = ''; /** @@ -205,6 +213,7 @@ class Calculation */ private static $localeArgumentSeparator = ','; + /** @var array */ private static $localeFunctions = []; /** @@ -230,7 +239,7 @@ class Calculation 'NULL' => null, ]; - // PhpSpreadsheet functions + /** @var array */ private static $phpSpreadsheetFunctions = [ 'ABS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, @@ -2814,7 +2823,11 @@ class Calculation ], ]; - // Internal functions used for special control purposes + /** + * Internal functions used for special control purposes. + * + * @var array + */ private static $controlFunctions = [ 'MKMATRIX' => [ 'argumentCount' => '*', @@ -3243,10 +3256,17 @@ class Calculation return $formula; } + /** @var ?array */ private static $functionReplaceFromExcel; + /** @var ?array */ private static $functionReplaceToLocale; + /** + * @param string $formula + * + * @return string + */ public function _translateFormulaToLocale($formula) { // Build list of function names and constants for translation @@ -3279,10 +3299,17 @@ class Calculation ); } + /** @var ?array */ private static $functionReplaceFromLocale; + /** @var ?array */ private static $functionReplaceToExcel; + /** + * @param string $formula + * + * @return string + */ public function _translateFormulaToEnglish($formula) { if (self::$functionReplaceFromLocale === null) { @@ -3298,7 +3325,6 @@ class Calculation if (self::$functionReplaceToExcel === null) { self::$functionReplaceToExcel = []; foreach (array_keys(self::$localeFunctions) as $excelFunctionName) { - // @phpstan-ignore-next-line self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2'; } foreach (array_keys(self::$localeBoolean) as $excelBoolean) { @@ -3309,6 +3335,11 @@ class Calculation return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ','); } + /** + * @param string $function + * + * @return string + */ public static function localeFunc($function) { if (self::$localeLanguage !== 'en_us') { @@ -3937,9 +3968,13 @@ class Calculation return $formula; } - // Binary Operators - // These operators always work on two values - // Array key is the operator, the value indicates whether this is a left or right associative operator + /** + * Binary Operators. + * These operators always work on two values. + * Array key is the operator, the value indicates whether this is a left or right associative operator. + * + * @var array + */ private static $operatorAssociativity = [ '^' => 0, // Exponentiation '*' => 0, '/' => 0, // Multiplication and Division @@ -3949,13 +3984,21 @@ class Calculation '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison ]; - // Comparison (Boolean) Operators - // These operators work on two values, but always return a boolean result + /** + * Comparison (Boolean) Operators. + * These operators work on two values, but always return a boolean result. + * + * @var array + */ private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true]; - // Operator Precedence - // This list includes all valid operators, whether binary (including boolean) or unary (such as %) - // Array key is the operator, the value is its precedence + /** + * Operator Precedence. + * This list includes all valid operators, whether binary (including boolean) or unary (such as %). + * Array key is the operator, the value is its precedence. + * + * @var array + */ private static $operatorPrecedence = [ ':' => 9, // Range '∩' => 8, // Intersect @@ -4441,6 +4484,11 @@ class Calculation return $output; } + /** + * @param array $operandData + * + * @return mixed + */ private static function dataTestReference(&$operandData) { $operand = $operandData['value']; @@ -5007,6 +5055,12 @@ class Calculation return $output; } + /** + * @param mixed $operand + * @param mixed $stack + * + * @return bool + */ private function validateBinaryOperand(&$operand, &$stack) { if (is_array($operand)) { @@ -5218,14 +5272,11 @@ class Calculation { $this->formulaError = $errorMessage; $this->cyclicReferenceStack->clear(); - if (!$this->suppressFormulaErrors) { + $suppress = $this->suppressFormulaErrors ?? $this->suppressFormulaErrorsNew; + if (!$suppress) { throw new Exception($errorMessage); } - if (strlen($errorMessage) > 0) { - trigger_error($errorMessage, E_USER_ERROR); - } - return false; } @@ -5360,7 +5411,7 @@ class Calculation /** * Get a list of all implemented functions as an array of function objects. */ - public function getFunctions(): array + public static function getFunctions(): array { return self::$phpSpreadsheetFunctions; } @@ -5461,6 +5512,11 @@ class Calculation return $args; } + /** + * @param array $tokens + * + * @return string + */ private function getTokensAsString($tokens) { $tokensStr = array_map(function ($token) { @@ -5527,4 +5583,14 @@ class Calculation return $result; } + + public function setSuppressFormulaErrors(bool $suppressFormulaErrors): void + { + $this->suppressFormulaErrorsNew = $suppressFormulaErrors; + } + + public function getSuppressFormulaErrors(): bool + { + return $this->suppressFormulaErrorsNew; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php index 23ee5ba8..3ebc9888 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php @@ -5,50 +5,26 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException; use PHPUnit\Framework\TestCase; -use Throwable; class CalculationErrorTest extends TestCase { - public function testCalculationException(): void + public function testCalculationExceptionSuppressed(): void { - $this->expectException(CalcException::class); - $this->expectExceptionMessage('Formula Error:'); $calculation = Calculation::getInstance(); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $calculation->setSuppressFormulaErrors(true); $result = $calculation->_calculateFormulaValue('=SUM('); + $calculation->setSuppressFormulaErrors(false); self::assertFalse($result); } - public function testCalculationError(): void + public function testCalculationException(): void { $calculation = Calculation::getInstance(); - $calculation->suppressFormulaErrors = true; - $error = false; - - try { - $calculation->_calculateFormulaValue('=SUM('); - } catch (Throwable $e) { - self::assertSame("Formula Error: Expecting ')'", $e->getMessage()); - self::assertSame('PHPUnit\\Framework\\Error\\Error', get_class($e)); - $error = true; - } - self::assertTrue($error); - } - - /** - * @param mixed $args - */ - public static function errhandler2(...$args): bool - { - return $args[0] === E_USER_ERROR; - } - - public function testCalculationErrorTrulySuppressed(): void - { - $calculation = Calculation::getInstance(); - $calculation->suppressFormulaErrors = true; - set_error_handler([self::class, 'errhandler2']); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $this->expectException(CalcException::class); + $this->expectExceptionMessage("Formula Error: Expecting ')'"); $result = $calculation->_calculateFormulaValue('=SUM('); - restore_error_handler(); self::assertFalse($result); } } diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php index ca9d5183..5acee2a0 100644 --- a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -5,7 +5,6 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheetInfra\LocaleGenerator; use PHPUnit\Framework\TestCase; -use ReflectionClass; class LocaleGeneratorTest extends TestCase { @@ -13,10 +12,7 @@ class LocaleGeneratorTest extends TestCase { $directory = realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/') ?: ''; self::assertNotEquals('', $directory); - $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) - ->getProperty('phpSpreadsheetFunctions'); - $phpSpreadsheetFunctionsProperty->setAccessible(true); - $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); + $phpSpreadsheetFunctions = Calculation::getFunctions(); $localeGenerator = new LocaleGenerator( $directory . DIRECTORY_SEPARATOR, diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php new file mode 100644 index 00000000..d4b4e6ea --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php @@ -0,0 +1,64 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->reloadedSpreadsheet !== null) { + $this->reloadedSpreadsheet->disconnectWorksheets(); + $this->reloadedSpreadsheet = null; + } + } + + public function testCalculationExceptionSuppressed(): void + { + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $calculation = Calculation::getInstance($this->spreadsheet); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $calculation->setSuppressFormulaErrors(true); + $sheet->getCell('A1')->setValue('=SUM('); + $sheet->getCell('A2')->setValue('=2+3'); + $this->reloadedSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xlsx'); + $rcalculation = Calculation::getInstance($this->reloadedSpreadsheet); + self::assertFalse($rcalculation->getSuppressFormulaErrors()); + $rcalculation->setSuppressFormulaErrors(true); + $rsheet = $this->reloadedSpreadsheet->getActiveSheet(); + self::assertSame('=SUM(', $rsheet->getCell('A1')->getValue()); + self::assertFalse($rsheet->getCell('A1')->getCalculatedValue()); + self::assertSame('=2+3', $rsheet->getCell('A2')->getValue()); + self::assertSame(5, $rsheet->getCell('A2')->getCalculatedValue()); + $calculation->setSuppressFormulaErrors(false); + $rcalculation->setSuppressFormulaErrors(false); + } + + public function testCalculationException(): void + { + $this->expectException(CalcException::class); + $this->expectExceptionMessage("Formula Error: Expecting ')'"); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $calculation = Calculation::getInstance($this->spreadsheet); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $sheet->getCell('A1')->setValue('=SUM('); + $sheet->getCell('A2')->setValue('=2+3'); + $this->reloadedSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xlsx'); + } +} From c6b095b626a44f6da2133cafea094a19a6cdabf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rodr=C3=ADguez?= Date: Sat, 1 Oct 2022 21:12:21 +0200 Subject: [PATCH 151/156] Reduces extra memory usage on `__destruct()` calls (#3094) * fix: Reduces memory usage on __destruct() call * docs: CHANGELOG.md Co-authored-by: Jose Rodriguez --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Collection/Cells.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bf3d568..76832f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing +- Reduces extra memory usage on `__destruct()` calls ## 1.25.2 - 2022-09-25 diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 82c9ae1a..6183e733 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -465,7 +465,7 @@ class Cells */ private function getAllCacheKeys() { - foreach ($this->getCoordinates() as $coordinate) { + foreach ($this->index as $coordinate => $value) { yield $this->cachePrefix . $coordinate; } } From cd9811cbd32b51168eeec69286fb95dd515c2eac Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 2 Oct 2022 18:02:34 -0700 Subject: [PATCH 152/156] Xlsx Reader Accept Palette of Fewer than 64 Colors (#3096) Fix #3093. PR #2595 added the ability for Xlsx Reader to accept a custom color palette. With no examples other than the one at hand, which included a full complement of 64 colors, that PR required 64 colors in the palette. It turns out that Mac Numbers exports to Excel with a palette with less than 64 colors. So, with an example of that at hand, relax the original restriction and accept a palette of any size. --- src/PhpSpreadsheet/Reader/Xlsx.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 7 ++-- .../Reader/Xlsx/Issue2490Test.php | 36 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3093.xlsx | Bin 0 -> 43188 bytes 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 tests/data/Reader/XLSX/issue.3093.xlsx diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 236ce83b..e248a623 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -2162,6 +2162,6 @@ class Xlsx extends BaseReader } } - return (count($array) === 64) ? $array : []; + return $array; } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index f84aaa68..8d380907 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -365,11 +365,12 @@ class Styles extends BaseParserClass return (string) $attr['rgb']; } if (isset($attr['indexed'])) { - if (empty($this->workbookPalette)) { - return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; + $indexedColor = (int) $attr['indexed']; + if ($indexedColor >= count($this->workbookPalette)) { + return Color::indexedColor($indexedColor - 7, $background)->getARGB() ?? ''; } - return Color::indexedColor((int) ($attr['indexed']), $background, $this->workbookPalette)->getARGB() ?? ''; + return Color::indexedColor($indexedColor, $background, $this->workbookPalette)->getARGB() ?? ''; } if (isset($attr['theme'])) { if ($this->theme !== null) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php index 182581e1..f65185e7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php @@ -12,6 +12,11 @@ class Issue2490Test extends TestCase */ private static $testbook = 'tests/data/Reader/XLSX/issue.2490.xlsx'; + /** + * @var string + */ + private static $testbook3093 = 'tests/data/Reader/XLSX/issue.3093.xlsx'; + public function testPreliminaries(): void { $file = 'zip://'; @@ -23,6 +28,7 @@ class Issue2490Test extends TestCase self::fail('Unable to read file'); } else { self::assertStringContainsString('', $data); + self::assertSame(64, substr_count($data, 'getCell('B1')->getStyle()->getFill()->getStartColor()->getArgb()); $spreadsheet->disconnectWorksheets(); } + + public function testPreliminaries3093(): void + { + $file = 'zip://'; + $file .= self::$testbook3093; + $file .= '#xl/styles.xml'; + $data = file_get_contents($file); + // confirm that file contains expected color index tag + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString('', $data); + self::assertSame(15, substr_count($data, 'load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('ffc0c0c0', $sheet->getCell('B2')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('ffffff00', $sheet->getCell('D2')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('ffdfa7a6', $sheet->getCell('F2')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('ff7ba0cd', $sheet->getCell('H2')->getStyle()->getFill()->getStartColor()->getArgb()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/data/Reader/XLSX/issue.3093.xlsx b/tests/data/Reader/XLSX/issue.3093.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..eb141ec9114c6abe4a040a900924e868db2eb8a8 GIT binary patch literal 43188 zcmbq(byOVR(jWwPceeq8y9R~;Mcwj*iCWdrqPqQc@z44m>*0q5fyamIrjD5>k9wb;Vfo1I?vh`h7;*@uQ3>~{epCe ztX!3NDg04``Ws(m+)mkjw zu5x5Y`x?hYyU8x@C3*duyS9*){vv(;H@m>o%n`Iebti*`fx&^=#oWp4vx}3nD~Fkr ziv{~v2mAk*bsS=q>_R?bOP{@>rnNRWQp4-qB&!p?0~%LJEl-(bVHJyzeDep?rYgkB zw0qBbtsLw6tD8d%s+L+*8HBFEAlE9fwVZNBKFod+sc^AryX(Mw;w|noa!+FK`iKzB z01!=i^LWVUPDz}WLM$jO_oCP;NPR{$h+zs$h|$^eo?O;lTlp(fueQEtcgRN;9haWT zIkx8-$*IfE;C7**m_HIlNr%_ft`Dkc^P{87{vF)0G9vT{QWtRluKZnW5=S&t`FZGp z{oj>0ad!Ssx@V35LG9thhTrhXq1|39_Qqy@X~v(R{CSsXzZ9^DF|2#IOornN6W7a0 zHG|puEds;TB_vB!qaHsXJ-W9D+<2gty)#L4!lxE6gqb)5rRSLm(8f@<^Cp2O40#m>~p$?iWX6Qd%b+{=mGc1JL`R%W2_6TANufyNujoBsmP zK)RgZApd|ygem_VfY%8dX|+-OsXt+6eC%|5TkPB~db)k~E-O@B?(}jA4h1AzZYF$ z-KAXY``&g&PO#-=kmVM(=sPLb;;FSo5s3Py)K8yaDg2Unq))eckR&R>?YL~lwlQs=a1?)_jsiR6%nP;VPS->C z6LW$qN~r$wHEc**gQ7jqx>4Ng$x&V26gdwmr!TsWCnsL(=nV8ZC$%iD`=e%3E@#ui zbso=ohflLlkbC==_AO(b*!VL%<}?)ZE}jyrieWEuHUch__$>b2soJ5H1M}52kJ1#2 zhtewx#96Rny#xBTgL>c+1@0@ONQ1+fGv4Z8L|~PM^(DXA)qoDW6t2B%xV^C{Q6lQ2 z@R*Ahu^9SSAZWJn@NDmwRa(F6lj>{QU$(GtA(9T$HL%<^&*1Q*ADfOcr}rh~upjH+ zO{Fu`8gz&N19Ptl1B3T3Q@L4NI9PD}7v%a+PuYq7vg5KKw%?gHvbRB9qWy46t}+gz zom*fXUn1*mS~9a9hKvnRSn2Xcr0_I3vruW;Bd`Y6N1oDUXmj&3xv|mXjzl;8>W3w&_~F;A7@jCQ@13MS%i>>aI6@Sc2pLhb z7LCv+#n?q27z5MGpv}iOw%yHd1VR&)lbIc>d!IWH*U~_#BIi%$^ihxS>S&GRmt}E~ zH1pE}nh`7MIq42>DtLbH5IYX{l8*olCUqFW+eKm3f|r(NLrl#>N|PM-h-X!`BAi_H z5}7&)>s{uDEa2ETe{|CQl4dTy zt8X{Q2rVV@oeVohPmn2Y-fYdGca-CCn3*@4nU}Pt6FZgsa@zP4EMcriGi>Pr-vQf4 zAAC!{zCdU~ws0l&1MB>vzDZ!qM!Z~fUfld9`MaXLyX-c9mBsfFjQIH%D)^3u-cj_f zhbcpJ#x;)P(BIE{^1wbwLp zrsJX22{qo`&E0BPO*m5YNlTU4aqi$;cW<#ri^8mpXm6Uv-j^Sh9YLo&UnN&+MCB~w zM?#DCD=&k?O9+nxM!${O;D_g)Ise+0FBIu;6n-o=n<*eBEZX_1b@1rO%uB(y%9<7$ zMNBHrsKypaou2+kUeVDd*QZ7-rz_u3uhj2n)G^SjO8m*;C}R@x?`ai(myPlW}nj6S4nbRSsgHN-tte@P^S&s`U6+ zQLe|B!g0o;O7-<;-(HeB-TT`2HgJSZFs>Dsk+g|DbQF9jy_T4oU2Y74mre{@c0Whi zW8j61KTeE6Du21OD{Rrfq|a{)%lB;gJk~V)c-=0u^aIa?&l|H$P6gG^cPm>FNv(C5XkM+s2|N{A*+vdq0G;El(x)oK6Sp0xvW@n2@_Wge(+9egGLN-5nzS*QjZC z2>xHA!rdYKe~oxqc4Q;8lixvzcx$(A985Nz(@n%VwC5y((#l260`hbI{xNo!=(=B1 zlCXepgIVZ=36K%WOiqFE=e#Z6k@=`vYnv>3B^gliC`tXeL-cOn;5B>=0q$BJ2Yr%G zj`L$2c{j`l7kIeqp?^mR1;Ki651oh24fNecM!bY7jDx-nWgHJ>{80?b_~E}JUa#Ig$ULtdyk=xy8aq8t2%$*_}4DtWjkhoe~Sh%@z{MX-qj2T(&qltj#9~A=PDymv=G4#ns zFi6gQyIe>cINw&z=5;8z7gh1nQMf~~x?Cths z^`!d^zw?FT+r3KX+umT#>+;06hx5%8C#@%{Z?6;LFUyVJ9(Gcob7vh-4{^}B#>DPey;se1FCUGI3?9CUg+e-eMG7V|rL6%hBkzv}c8 zxqI^x0v#-$y`Dd*%-xCm-tSFRy}mg$Xg^d@iN7|Uh{wKOJFQnbJzp7#yMKve)VF=G>Xl;;(CTkn(m@MJ4vKVj;&Y_a}xV{i>BaXTJ zcD4M-d788F&5o-01+OF8{Bd>CbJ*_$@0O0F$LUxqf8rsL|#>Kfv7B6lVxZuVI5vVVtug!HEO#HE;%gY(T!JnTcu zBiHksf3%P9P32R?i*wGN=v~Ie=IuSPp_1>}^+xMArS_cN=f@Y0^|#GCalhw1ft-PDZ=o+o?-hdQxg8tEzKuolmm-YwDk)*@H*C+POgZQa+)Jew!zM&Z@( zbCs5ap~&8haduZ{lL3h5(R$(Gd~8yU&8yJy&`;`3f@<2i6git3p+tAeN{G>??IuA} zew>P&Eph*#hB>?U#h~j}z4+}p+gzcb!jU{y=aX zQG#M{(YO>MNQUoDZWUY*3niL`5+(mbq&@-S|9Z$ZzPM2~C}K?M#(MQjgjdtbwO3F3f|G$w?RnK`{pd4iX z;qZ?z>Hl2jA31EcpqEkl=Q5H15d9+~;J*=J%?*UWy;zfg3UgTLFmHZagw#|*L3iaM zcaQbFDTzswU41m?ntM$&=f+JJcTdE;lwD3ZPL;?gGM!-@E1l!-I5xW2ruq*%+12S} z8qkr>8?%Bi+PzY3F)rvj4@H)M>}s-0=3IboKd@| z8nUp2@oz6FGVATBeen_J2mq7Im7f%Csn<_bmvACJ_om!!`?zH}`}b#tiUUMKc~P-c zkW#(_DMj<4i>OE+)%fZ7eTy7k0Aw=sWB5&--=|~nUiKrp3W^CW>$nMbVFJ%BLQbt8 zTxnnha6hXA0Q#FAZ9sZptKSG%os?5n7dTOPiuV@gYpeiv=ErtG49uog%COwf&h)^b zFII}8J9&JOkW}Qb!MQ>e8s{lrNuxQe;r=@kmB^_niqyRUfY3Su4xGufN`Cl{0s5Bi zs3i5}l3zIiN0)7NFDGiz`I+P!S-#}RKOf@HPUKX5JRIVls2zKJXVd%Kl}pCmqm58| z_hpditxY$sIU6&N4X(j$e@RS!W9IG$bUC|F+zq%YjEXh6_iVYcUu2q4d_2@=;|g~S z6|VpBKss@t6&KTqVB5ls5j3rL?L}U&AL;pZ{4iqJ^>k|QFKq-SGTFM!XG}@ zd3%gwb4TGs73U<7-?Y(W87tgw@*Q_>C@zQt#Zys~@UhCE#GH3>1yQORT?6L2DF)mh z8u4#zh=Z&HPOvI?`1hvTAirirRAC-ux7y6H>yD&0{5f)r-xC8`Mw@r6%xE!T2}_0Hjh8+p2S*Z;7I)=>$!T2frW+|Wsp0#) zzdv$IRpkqZ+Ldh%nz%n^7EDirUZk`34cI73+^IO;kI@(|jcVo9xn-^#GJg)VIS`C# zAEVqM7A3WK&%#Eg9lkP?!csykybjuAwjxf6d6>OlDv>^Gj$>sbn`hws{J_pewz_ex zUR$-NNu!k})P0YF%6S(b&>_A)kLC`MdVs zo}8IcqZ)K9(^_f_X<1OmtTcu!>16Y2R!>!k2zKx;3P#g@GI1bBWUM_RG4y_!_AN+` zIoigRPH3e*LGp&b!Tqg~D*gQ(*($MgR4c&5hf+hc8uT;?-^=!`^osUx;Ec_du)!H@ znES-=X;93Oe*~Tm0$Dqnxfuf4JX_uMPTEW!gl3=HY(FTy&OTF%L%yz7@>rJ3W|4Sq z@I2sa_fpdz4t!LsCS{dN{A!T9RP#BT$aN8#{(Ul?@4(+EE2G%WtzWT;@}Q5a3!T&7 zNNF6Op$Bh>;G-0TC9UGj^t^VKpYWw+bGsowwo0?%&kHuJiH*{uyBFOU*1){v%3)#E zm0)PoMLl+-BckcHMje?^x&4dqju7`|DIZBIF*6%wTnt)AW!{QIR&)h=(qmV6W&_|T zgmL7OMDcNh87?bsQcBBuGo0NRcdiQA*RZxVYludh6O=V}HNWquE1d}%lcu|zvva9&nS^CdHz~$c zp8#NbQ-3-YioVcbT24TUghtsznja3Iz{I_*A^%LX#nuuyAo|#Lj4?utoJdGVHOiWbB@hRwHcmimaBkB1_m<#f@9R3amD2Mm1D0I1`>(+ zVH3H@2K!xSQfufd*|S6u6insu?tYLcY_+HtXe2b#d*GQRU&bg(aJv~7pj8s3VHC7> zfQRg#^OU!zky71gb@t>iv(iPo%@a|?#NpPK6L2i}Q-5Wkv}C8PQVa*E4Ha}@0L#A2 z&Qax~ED&c6!R-XBGdZuZ>64Wh232|Zj!+*GEi`$Aj=Bi}fQXT0v}6J~aol8%#&q zkVFO-cj&WSxpO&<d% zTkz?unvwG;=u}w>KMg~imtGYC>q5$)&`A5B%!BSF#k;SlO$C=xu5)~JKYJrQtgANw zmA}~iX%qxY2I1uDDm8V;oJ$LA`HRplrBiama4|is5F7(itG`Y+5{~@hDb*88`>;CQ z2h^uuXcE@T_pi4n7x|D{iiL_O26aVKNRc6Mob#)~HBt3kx41jMjUDtC_vsO1Un|23 z17nW1_UT_^I4JImh^j$XTr!ru>OlkL2{Di%jN^CwmF1)*xd`@dN1laYvixDOBA21| zZdIP!F=M+OvX-u4W3uaG?XjLmPxK`%$rl`n+6x0)cx-s2515^hc+GePl{ zmfK0=_>Wty1Ev(njEjcVhc4sFUMvx>15?mxzv8hfvX^0{F^vYu?$F<*G0?u zT4Lr{*63ZOVX-6RO2^ z*YD~8F(9=1DBH>$IF5FlTz%%W9VTkG612~QJA{iXLL;A)aN1jeVIrOIP~ecSGIO3?CbuQeIpb$7)DGp9w%otYWO~0H)3| zXWdZ?@J$TOP}AtKp#;yfl}hD=+c80bwOVlXB*6K5}caY zukA=)AblD87URE*@e)m$=+rYiO|1%FRDW)G`lh+wm+QVA!+kX3hj}*|dcTWR-RC_9XLvyB}`K3xQlF zd|Q{Q^bD_WS0I)NeJuuGe|58~2{nfdd#Hyr%6zybjOEgs?{H`L%Pnw+tS^tPJ!(1Y zdP=r2FX`G(bVaQAz+(;RbO#S2)W%Zw7a$SQrOY$Np*2uwj_%Eww&H$Vv~yHyS-Xm` z7>|iG@(fGosN_r2I@6Gx^<~*ARMm(>pt4d-XRYJ|74$8QhgS?2n_R_s5HG8SwiV2o zyR+_u$6h2+Q}+7?O)1qzPn7JRE`WGMaxRzyDeqQXocouV{A_@N_8B}uo48+fN`TEI zpEx6Ygc@1hW^#XSV029?kl>lwaJ|3pSYZD%(8b%gi3zS=qd z{MDs%8C0$$dowigt|5lXmx+s z;85H14 z%ueNm6Reb*ewNin)PqDoNu#;hYFWrdd(W~i5gN_>vdt)}-LjplZN(#q;K^Y!gDaj} zZa00hV6a=Dv6;T-qVIu7C9I(-Of&!c5|%mHlmNs%fguQf61>ibhv@CzB> zIJJwv8GBF-9C~!=p&~W%+mSWN=zt#Rp)r^n*TS#%H^RT#Z5roJkjgxa7})1>LmqZbLf?c>QBiJXnZJ zYwN}9TKWS4jr*g5k>c(GAQo!S!5yB3)3De%Cymly;-(Y-%+i}7p}*>O+`p=n=C??U zye&GJ2ze;+OtevS~Gz0=|3z)Ow-?ruE5yy$pRxG)nP{g0~W zFe>v{Rhygci3J|A*}KUlHnT80E7NW_>I`_Qjyf$e`3fH8E46MiW#Ikt6W*NxcZ03@ME#=N?IN49++2>=l!LDm7G?uxX7OO>tZ#nH$y+Q&SjM zB1p){WD_uO#wE&enoMyYG4a&1e5?~bf1dTc#7Akg_d4i}Z*mxBW6PS!$b7&700EGd z#A&Y|)-I#NY&}w^HT6kuIf*xF(RNC*N>aMIlCd)$NI|CLySyJBn9+*!YtU#1Ok|gR zrM`nQLv|Kgj;(2}Zg-FY!G&Y&gTyI+k+#%z`D3{-)kZh)#b*^rQ>JS{=VTqwH&|^ zp3F^V7WOixdi#}p^PTKtgvKx6RQ=0v;e_kTa}uBaQNFunL-y>ojiPpN$E2O6d#sy*U)?sZQAgyI1J@c+4@=!8t zVlBCbNF{x@5XyhDBs?V_88T;1+(4{`?2^|`Rk*jw2I+m^=$=V zh2o1gd$Iz?iQ|}p+nkhL4Q`IerPLaZ3)q{=Bvw5!T9vNl@<_T2N4uC+r?DC;`?nWR@(G! z&fw_s0{ZXMI4!vJJZgv}gTRqI^UCi9+ukE3&oqCBs#z=b$eJO`iCh)sRY@PMQ_gLY zvojyR2iUv=9(A8s z#ubuu6}lG`?rm*IhzBLE8|gOqfDDS~P7Jwt4j5Fj`HiE@Vq^4;io+djb&(^aA9)YY z>aH_-3T2ewruib&7HT;TEZU|MIST2$7cgY$foS0~5w;x)cR0*;nIm07IPV4BcF%ShuKrwF0-@8#yq_>57mq+~amuelCpDsy= z7A}VjZk7_qnXuKWIG00J*`pEl-Qt&jupP|x>zKkgU9h->SFZ(f>ZalpW^9|vF&xk< zmx{U6&cJ{Batw$(t7JQ9&}&NSt6r!;1HdI$FJLI=_l)?AZmQKdkM4wpVp_%5_K(8K z(aSFhPL2b&zzCRo!aoh+d6d&=`6y>H4(bzODFQCE9U}&+5=QfBQl?U(s`{(DZ(QsKi|pHRP!x*9Eh$7N5tqiC|ASn9rP~rRk^bq-#2^KO!Z7_@u1~A zmGbFmyDV-{$kD$`rEh)!+SO*wUzDEk((by6kcAi%;(T6G5Z-Z3Eo9rm3?fmS+q8<_ zWM~n&XS%S%Y3>pRdV2pUu?G&)q{<{KrV=lK$d-BTc`wax*&7%k!SDm?a?<(8G*nAQ zi?lBn{BcmLw(C=Uv;D^45XwcJQa|y9B1zfhS3Th9Q&}jw>5+b{%V*w{xjqr*c;5Lr z^Hn?r*g%GS{N%!jEG*jhM{eon6@DWvp()6v22az$$t)RhOWhd3nGAo}u)=2#2>O!4 zdmmwue>CrGZVXaudrh26={+#x$QRr!FduF3_al4eU>_o{6}HdTWla{n1Yjf*A)$|c8sY)L zPxxN6><_Eiba++rCSxSQ%cBR9!<-| zg0#cqR-e+YOoIU~ykXBZK~Lms4hlQMXFQZu=)F0LFxAM{@GTh&LI5GF=KIU{8pi6q zw{EH&SBt~p%>;1Z659|nbQJ`JnWgZ;l36(j?Nl;eMt)`N%yx-UrHEeclQ0F?D)cs~ z>GbhpIwpRHNy*eMZ1Zd1D)zvR!4=i_;7yE-w@CodL8u0RBaw zVCw9$_GL4xva)uY%9nPE-Z4KYiUp5>D2+983&Q>dSB_CG6m4i9vDu`*v3I^!=RSzS{OFR-k#k#xzgX~w-;YF1qB#(5u9#~Bh&I+q2!M}`6S?bMd30BL5_;y z#{>rReQJqVxBPXA2B+D(#9E6rhQg3^bNyIc+>=529keAXR9&ElqgN9_TS4oGMrnO* zY$dBXZqQ*qeSfAN;4xK$tmUH-09Yl~sHk5uB+u;WnAWs2LLhNWx|&75KI7Xfkf?AA z{c3ghi%rZu!8G3Y`SBO#%s>7UAy$Cr=i>%_eHuoYa?5y7M#Ih$%ZXgm zld^&V#ll69PO9=6}3zt z-h4isH*;!gfUbJEZ22qdXb5N%ogAY_BTM;b&&O?Ef#Mt*M7iY0JLNjAscURih*zl_ zuP%AknT9aJNdN`g7OW)Vg#w+Qy4B|rz-T$n%!rY4%+SgR+1@~De2hOn4Yoab;!j_Z z^wLK}e#hw!c{`NBiY=ahN)x2?)hPD7a|6&+r(vu4kVpwIy-?O#(!QY7H08XMZY=%^1`S1B^NOHIy=ep# zH3hZz!JV#s?eg($kinHH`L#Ijo&&k@J(ko3hGt5miHH??991^F-otlOTR{u_r{1X9 z^fVNu`)$X3?;E2GKY9Np6aGk-lJ7k_?2=^6qgF#SuKjr`ZfjLTjVnT5lj^4EA&D_d z|31meUXhWRh!d|FkE5GShkr@ml28paJDu~RJtjYMO3t9&r1@}|kNz@?sPa3z;BY$t z(;63qp^_%dj1J1A-^yA2kbrXY!(%!x zaURk>>|fqGoik-uF$~a}VG|4;#+OTcLI?P9_#C=kTcF~pG;rvG-5JiWc{nbT#(@iC z1d84SxRn=DN1n;YcAROM$6-!-c#S(oX1Dco=3hRDP?oJW0$!3uqZjATlT4BXljKAh z1m)yEnCR!5A9@#u7Lw#xr8ac~i{PU5XBF}g07ng(7qSGwBEyyDhSs!HAhCd9YQ5P9 zY(SR&dbTI|Hv*ejU9jqz@7N%30xA{HP!oGT?l}UtBbX%|(|qOAr^q=(ypqK~?8vJ? z(;oIB4i}xW?Fq}9X=-m9LZ{DPgH;k1Vt^xS zZQAoq@jd1Ucdm$ZG zZla;OQ`88Tco#Ip1hjl(i%x?y@TYuoNk$Y2g=7mO(OWg^-^wxg?p7sTf|VIBOhRTO9|k#r z@vdI+cZtCVw7eLG{zt$^p6{o3W3TtX9zRPlE7U%dKip=qiLyDoO5T2jF0e7ma~Unj znDx`!u$?Kp(-qqXEqh=waLH=R-^6s}+7oQUS+mV2dhhMXYaLTL7OxgzL$z^rEwzF$ zK*}`R^XH!RZ66@$_*1qc(fOj)rJKRiPDAy%+b1=^t?Yo@%p{7+ynFnowJcFN58@x9 zC#_8RY>gT09<;?`F8<2F*AKwm5c(VWrpy7R8op6h$|Nnuq2~&T24#f=wjc#H7Kv=S z{@i4l>i1mCXkIN@hm&EVCqYRqIvyiCsv06`dp?oqbSf|Os_>(czIXFNk-D3eZ|^rN zwKtV(i|u7|EShVgz6V^(G3+sKMH-(jAD5Bi~ISuEaN?KgR{ug z$r9Dl=~9$^c!`@nnEPJP=Asl`%#o#8|6^$6%wuJ(*rz|e+*jfB=f&}{H7Od_xn}#` zC`je{&l*#36yOYcHt~HynPwc#kXXD4Alm-E4|A;H$3dGL)f=2W$)?u;_*t*>}hy;obXnG~41_{_N;im953gWut+bcanr6hO@3^ zlGLDSW^$P(zYR5RdXyx*6or6?zC1KtqQIl}B-^Urf(p7suy|+GN78JWn>RgF&;n(- z%ceWdy40gf*B{*^Tv%fWVcmNoRU29omiG&S(O@ubQSD`$e?ed@CO6Y>~V^s+k z4;r}VAKT?Y_b|#%`8|bE`lT!avBBnLzpl0Bv!d`wDy?GE=-=y4D9i>&IDfU__$zq& zKvHDx?6;v1rK?VL!~DLhrWJhX^uYx^!k!;*joXLA+$>4Hujm|3kV8PVxnT1BJG4O= zWm&>nP=p%xO?+l-*s1JbWr}1Gxj}P(2l=Q#Vn?R4+Y1X;?cbHKs_7L(Dc6FTIubvW zWIZdK;b3SzV=4sxF{(c*w}C)_Dc?wAD59 z6{MX{-u2KD#SLtc*80WTw&pCN!jA?3w1n>Bwv_&>mAcH`H>?D`T|Y!{P+dzdtoA-M zHQJ4n{X(h(tnZ`!b!2w*+8YCJuZaH^BWHRza)eEhKd+`>cg{aR0!+GE(YDm9=RFsKM+o*Q{UL z(G9SF4~}qxbA5^o^F+=VE=8)gbC}LWxrjYaRjo>#$lcN6mU^EZl=70R1G}s4A%w2x zyAeG>Vv+J8$MtL7_zAjW>@YGIUT9FsbZ);HRu#K_))VGK_#^^w2FV{&uR4H;0&|@f z0~p#gH(*1P@nD!u*Eqe4v;^OrDkP3OF2SBSuHviA@vvt#S$Ag$-kk8Q+Lk67uDbw^ zga;Lukrf`o>MT`Y9m>R$QGb!oB$p?hBTBI=e`a8e6yp(EvaxNUW48wDcfiXOe1drh ziIV3wJy<#r2c~DC`?oOJ+3k)jjv_pxdsIR=bY4D=yC>swU438u+k%+8yD!Jhv+n$d zjJ^ee>J%^uKMJag2;jo4z*h_;)E-Hi&d2CLOTk?mn{h!;k6Tm0!-q`PvQFYj%y*v8RAb zU7eSwBy2;MQv#{V5>tgyw6S?^g33@02Yw(X0%9SFg=_nNLIPUsPW{IuIjG4hm-zRP~sEZhgINiA9EqYxaOLaLngU*xTxi$Bh%9f4@l<40z zxQ{rNQ8{n2r37wCspXEpi#`q!C@a*H9+~62V~TzcndP3T z!KGv~HzUmt8!*8*W}u;16qEHuJ?hRC_?a4}I6_huS_?7i>Iz+COj(6v$98z%*K5?Ug&kEU*6tz+k z(*Ja?6|Sd5$Mc%}vjvSBI4c{EQR-+LEP& zS2$5iMF+zFLIqcZwGO$Q$SH!R66&roXHZZu@#8rMUP9n)KZSRY-&F6Gjb_kz5X7@XIJqSMagF=}#BI|3*t1{nT-J74n4)Du3qnC4M|WY>4+#niltjJ7 zrPPz}HsnT2%1VuJmO=G!5!H^h5Z>oS#B?fD^()LR_GPZ676$Hlj;CofVi9qlMc^{h zA0z%)6f7S_u++hBw^rNA9MO@(*{gH)mih5o4()Po5|hy6XCKGCzhgS(_w3#Q>VJcH z91f9ZsYOW`>R#8d;-#jz6#LRO%d4^kGV`gvU)<-N-TyeJ2s~v#+v2rGC(k0x7M-W- zu7r0P4ifwt6Id`z-A`=ZN#`d7PQmo1B3PFX#^x~B$-3r09Qwp*sKGxzrY5qbt4L27 zXYW>9Q?Zq8`wW0K>d1K!+|_|On~`+3i#|bno-)X~AL$)iwamJDm{5e1bpTFP(9R|j zwUk_B{rs>l(6d~-pD=fGNLZJNH;$Dz4!3tR<|l6D_MtF(Gh!JFS`x7Sf}Zj<-pf+G z`kf|i29Gp<%U;Hm70MrX<(_jBY68xqsO~8Lh9V>gas#1R5^o)nPt>bSaQwzrZ+m|S zrIeiR*)1VQKjCAnUR}W)tu)Zrx2#<%Rg?1WFBP4YPlZxq;p7WU<4-u2Ge{Uu;a`OB+3ogXyU|o!ADL zyk04FqfQ1QN=kQ~eL@qLM%P~&*Zt-u3vIGv8tm%`3Mxn)qNY5I>nKPaL<&xZtVQ^j zl%pHe3r@zYMFf`EqZJ71B0FQ%MI+1W9_hxcwYxLKWojxC7>`hpl#fR2wh@k!m1V z=TIm1GZRG@;s%(-`l`a5N9Vo^km|~|f763g6>SQ-buqfs)sl;;&I=uw7bM4lU%ZDH z$jtT}HapT4zAF%=8?MYT6B{n3B2?SE(D9dnv)lLx^!WNw-=z#}89cu##wrWMz(Nzk zuR`+0E9Nc@=QeDa@%$D4p3Lt;&?4uIsb`cwXJXPojDq)%@D6r7ot|&_5dPvuuf<>L z^f_wVVrEmh_iXP(+RcMf;)*wUF!PLREB3y)*Om_wB~am1qP%w1(-Vl~tTEu0?hfBu z>^{#AJ5u6#G5S0-q~Hpvt@}(_Y)0AsLI;>AQU4`9$2MPVAG{4^J<7C zFz*1pVg#wu)TjSs3>0>V&GFV#uW4cxKe)FEJS1-BupB~@?~bxX5!=k!wI0dSDE{#fHx_33^`8FOYSiW-TQ@>eFkboG2;#5 zIuB@?fWB8pY}<`p8~-wv2RvIonrkf15UH{GDb*Wjf{E+cPl$vPX(O>$*k_A~QBo>! z)72XPa^ir2y_gSFn>LgSX)#HdBsu3&%HQ7e ziH%Nb8}E_hz*7X!v~Cy34u1WF0k-rA|3f&&jKiI2!E5J3NQSb+77EVCV~F_WR@=@N zHASXGumfVy++xAp;cC}*`1P`-OzVS|BD)v;>-4?|IAWND_nJ8C$V9!XRSI+}tj{_q zqV$91ny>ete6s!m?a38Rk!KXr8X-Wk-01!S%Qmkm_ueZpxf>V+n086T&z7+fvd%@v zrW0};g?j-T%HAcY9EkjZoF+s&ex5>yH5U%^d;@?xJc?O~hy_8hYcaUKbt8-WJfU{T%cAPLkg?6(Y&649QL4K3Ai^ z+I68`<*TiO<1nil&KEuXgXK+uF&gse`yltcyEjD)z}SBYmgyUrHUXIH|ZSVwz0X`hfVv_rG;Iuq-Nk|6U2V7VMaagdR@frI8W>z+1= zoaqNlqG9s_!0>K|WCHlyOrf3zOsS*PNMrRoCDRmM+qJW<)YE;PVHy$#79JUbJ8a4x zbL>8)9HSC5zmiI1ZL&W#mUd0#2~*{Dmt*-V40U<&ecsV>0i!)xs^OiU5t$zsC6o%7 z+XY(CIm)BgwYFhPZ|&>_`;v~^Z#GlM&`uFkzF#RX5Wd7DV*l>ocV7mZ!sDGP_v{7n z`LBNcZaI9K`fE-n>@aFju zDRCY^|0&~pGQ1OOpkMCwr#DP7fSflg)9eZ78d5ldU*bt8>0whMf+On5XlSl3Fod31 z2|$~E&bv#?3n`emQOgjJDDV}e04hOKA~yR0(JA4lM$D9NeutJj!@Bx;fPUx1EbM)a zVnd5VY!N$OWs;x5LUXmM|2!yv=q9ab?eeGJOWNlCo3=U~x})=5g;J#p#HRxr^+}*$ z#u4KJrUr{(ac7w0ndm~TAAw2h9WS_3)IVj%H&d1q+ivhIu`0~6 z215xIW?eINY#m>6tGf{9$Y`7OL3kygK5Ufj$bH_0-bS|)|zvsMmzP2Trbf#=jkw%}KR(2rnvkh=$2amp|1NA<>}3ZC@jaN%&^E;su= zf6JndY$BgE(Tei||6`(lOyf1pcnF7h9$S6g_x=#BWHJ*r!zTofMHG@tD1Lf%i1GSPu5;2l$NW ze^y8s?(d-6cw>*4y|mUx3fGM9SY-)@Wew91Wu@d#zP>nU&H4C=Kcz4GK}L(dcXCCV za5>i?t4i`GB>02LPgP?apS<#Hjw5d`vEPL8oNmHsS-Lq_jiwSo!`%II-wFwX(MAi- zU{$S{SI_l|j?!k(IJT#vablCKUF~<-R@iUPG^$Jc zqQIi`D_&L|4zH&Z_C=p0Oeo$kAwEU|7-vLWm$u z<>5$lfWpL?D8ceg7?LC4>jBqD_tAGU)yciWH%M=bw3ycr)W?$|aZRkKcOVbQ7)*5E zGLulrloF8odc4herB326hQ`Y&U_?^Y5suTy!<%F_7iy5adN;Y)qRbn5Lv^iC>~2YvL2N-4zqctMI5w5)aXPf=#OVa=!Y8 z2y9Hsca=wl%%ONULfzXmklU6qAdJK$ z8KV7SsMlX`n&}Y3;y9Ye0|Us?hUam_JFw zVUX^Cjq2fGS5|Qd2UWp~Va+*l(R9$(x;<0gDgkGurrv#wtI_!K0kvKfsn|iMxK+L@ldy+z8jy#UU8f|Ct>K2m&s5!&+dGdfc5@ocE;EKK>in6J*?Y4A9X2# zc(%=~TrC7Yyj85P`J(sUyl_DB9YH!dS9MN*eHQovUw|=3Y8}`LXkOF-R2?>=np^08I8faNyRFs7dSrI5y) z&r&Ipw>))5v2VyU6`ru-IUQdfU>(Ke+0KnZ0VP7!={(wA{BT(KN_W`>lDa zlSb7j2p>c+|}__l3kW6=rH ze(*u0N%WqK2zg&MyzYC^g6`^G68sHKt&fgtA@(0j7MBCbbm?alW#4Hse6&`YvKS@t z5hxIVhkPY;t3k$?y6&=JG{L_5@@-*ki8X!X28Y}fzbPsT)HX^b+@ zRQlCwIG&D!qmd#nZNCL*(r{B<;yfz_l#XvYxjVEqvgE3KnZC?oFCoh$kwKqfCwKB_lf;gwJ`tDv;~4FZ z_7!Pe0lwdKcv;@W-=WKKN7?$KT689=p8DgQqQ@Tl~krPW#0cym+3l32H8z3x=mzX%#-RWMwfKTX}KHT_B2)3A8bxL&bzCz6_KD zSEyg<8BdPHdDy|@7T|pExzoof*dLGtEOdJz}@B~^{dea468b($&*Gjh?q znnvGsg#iTjG{#E;Cu{sB({?6|Gu{^BbPV^R3f23Q#WQljp2vx`KS(^n-;uteI)WCL zYh5qKO?b*xizsFJNVt1LOj8u;mOV*sGvON+Hn40qQLOz; zItKFFq`;9=%8}%HONowO&WKKcXyChJJ1t*d-@VG_=r*0 z@w?fJS)#T4F)Q`Y9b{A?e?czByU~u_&#nmSVTOQC%`GK&Alt5mrZ5Fl)C-J+ zefyI7c0o3Ws|jq55>pwk(Qyt8WK#|+UjL78cA~6>H~gQ}=gF(&mG);{jzmWJTR~o5 z3qD3Uwkf#kNh!p1MU_@1SmDC{p3@g>0v;}yxNL+m@@Q{96I(x!P=J6B4;Y9l%7`5Ip56vD-tfN6}dYIDYQ;UUNd6dxQ$9P;*Fl!4{MTy0{hK8 zll`!`AsK=IG{+XK!n6Zy;;LiZ%bCD-iO(1asqUQxxD7m&Aj(jMD0T+=VZy$lDF_-@ z#mw&mGm@a1H(LBBw`v0qvqt10ILx?J{0h;j(NK}i+hi|5mWlJEkN-NVnrTnw%1GOx z1E9msd646`attt3-jdeN`zy;-5Sn6$ylgua*A|N*lDcz6BxQ7Iz%=mj zmwl`yoRt>$=Cg8@{6=9P;Ze6SX}NeOVSI~OFfPWt0>D)=fWG)UE!<9HNY_9rHN!PAnE61;NKn4h4klb93-D+edSAf zKGQOHbqQ7_?7fcZ;<>&YciF@ngpAW30OClPh&JQ0bSww6M)>zDK{QcORzIKeIX{*5 z)E*JUwKHn{1vc2bg@ta^wVh#hA$ePSUpd+p#0=zqPMS??Qz^91EJ6-v)xwriSl5S@ zG>qTAh4$|VgyE!{b^Q=Y`06tln9gKhXvb8{VsA(F5An=5D(n6&tKB4FKxtSU$TZKV z7vN9Y6t|gZM&@;ZqSuy>XyH%Ic68pF<}S}UBo35cmFAba!~foB*im?UI223~Me~4OTOla( ziJqd@CcxwmQu61)uPItZ554&bpbuj>4%A;*E2d!Uy*2mh5OzL=IlG?8#9hE0LHe)u zZc=P+tGmPbUfePD@aWHkMa_;)RYNW-L>=fj2m1{HBL5Q4hb-_K!+`!`1x3C!EjKJ@ zK=;w`^?PSkw&|=q=djNzspPvvP{U2pou~~1@Y7hn{I3WI#eSux8#g%EpOq^E^hm}P zo_P=}7q)A7S-_#FuKjuf^>Q-IjC-0k{fquBRXa0*(49UP>=v#h0fvZsPo`(_h=fS$D9`8| znQ(n{cAWzPe*>dI#Y}JXndVz-I=l&OZMnYLN0FuT=_rVHfD6 zH6zIub8%3eyNQ6Xr7Xv4q*#7DQC}O7#1x{6?HWPKYD;R7C)wAXnX~8Q7LIIWu zPpp#@OL33Wu$$~(X7Gn*g{re~3g|WB7U8)c)g-gKyo&{Q2hgO6+}g%A{LNbuB?LBC zKWo==e~@Z-bBBuooc`$-0(_h_r(w3+lGP#?@-WJ%!uu?&>z-57yCwTQcfVq(BH=pc z!%F@E)hhKGe9pt9KE;d5Pa(IP5>tMsXp8jMcMAXf?|%$ZFjdi>yPB~EL}#3P$~6TH zfdrogd7W9*?Ruh+4KJF^&eLY*0mP8YT_T!SN?^yOw;V6;T4?LNe-Cg*szX|>H|W8~ zcGf*F&y#c8-^D_q8CEZXeqOIO&>S`n`e&F&G>N=5{FX-7${SkDkIoZKA1^SxOS|txZp=40{GiT*T74|FG$Hv(nxKAY)5_ zN}I+g%JD+G6IA|S7~)q7qHNk@*z^l??IhB4ScxSLrUueM*QU`_D&|B+%ZG8P)bU#H(MD!q~Pj>)pf`2NaRbFqj zuRZ9mUMqr)Pa<15Lusnp^F2NSn;@=t!m6{AM|y~$-B8auLY2p;Pq=VR0*qXuA28|u zHNHCWloQvq}0=taizcKumO5#EYR8Lo8#@Cq*VC;V+eXpTZeS4f#}tm$ef^ z9uhNuS`i4=lK}()|LA0zga(apjM9FCI9?f`ySa$q4h1p!#HCxiq2Si1mV+%*kCqQV zZ%pqNUG`r2ZK8!^o4Fkt)>GWDc)^9(?t5wXYbr1mw>>Fai|VXv6;=EMC98=$HPF9v zwX-6D`dC$9;S8i8VBPS z&*PJSZL}6-7ru}&0Lz(q+rcZ23PgqcyrLS3!FhRG((3G+0X4{@)8xdy;Tkp!rP4Dd zasWjFw@|#Fbu9)R1;t^CilDUSfTyaGM#+!NteqHMOselhMRCZp+5X{G<~CJ zcAeL8izj_a{bRRn$5d6NQigAVyMs~+gSUnVh*)N8_?LSpeZNy7-t)nJ|CujMQXBUb zD0L|uXDoO^QfW7RT=j<#U&_Pt!n4;**6A9`{p$d1n71KQZY$-Q+vLcnqa{IiQ%-7g znf)WZ@sZpI-9^5f*6~HFpB*O8MY(>$?FM7`_*OXEUm0#F_fl~UMaHFGP;hE61Y}q> zN5yFXqvSfgO4DC^n9G6Fu9bf*>{ILenT~58Qfh)Kgx|jV%yLP>ozEuC(WNAOxlB7b zfye^xIe&JA?EVw8F)@!499SDtV_v zTf;XlSz`hserH5zzsL!RJ8LiQ>}e zc&DRf4a1{B`SyC9ogwPZoKZ$VIb9BS0)w;2#23~;Zr`j-+_!xIYWX4g_N+UeABbyl zt7LHTH~P^e%PaC;bhWNrdSKp))|fiiDk}p$kn{SEh8-mF9y2w^+DRU%UK1$J{FUqj zdxpb2T%w4+Be{+6g*5nQMws@MT>Hm;vL*iQC#U?6NSGM&c_%jc&}m~vCSm0t%YCZK z847Cp%0Cn&xm~i-+Q139D>YD0sG`~(#yl|gyeN#8dZ3nFyNv0@Zf`7)`2FVwaR*VY z1g$jBOs&?Z)5K^$alA#bf-lbN(3Hw|aQy&8>3&ZhEMEB5_tk>?3f({vt;#4HX1U3z zqXBnTk7ZIizbBBEusv+Qu+{ZkECL2F%I*Qz?Fjjj<#8S=-KDqnrvOWOsp*MmD5?S{ z?DkEuwrY|Q?|`#1$DpS@;-Dzx2VPh$pN($Tpkp!PWk<2v5ktG@e&I^R9k$ntW1?r1 zDa@u1-Qobk1^rd^8cSKGYjC)(rB?C`e!FXF@W^R0(&tren)Thv0+^1OcX!)q-#QV` z?@}nD`nKB04w`(}aO zsHrI^0W&GJ9vIcfN+0*(nx??OqU>2-Jx$NY+ay_-<}DDX89t?Y&Y#z#s^f0kiJ)9K zp`ba;;G^Z8A{|-|BPJBK93~!66T4pPX1}nGJH8Qu9IZcF7m=kfSWK|`)M84&M;QG$x+ANZyK?| z0^=%vLhC#(CNJDKK)2*O;4xjX2_NR{A^8?(acHn5?z>T)^G5sCvL=tNwz?Au=nqYj zh`AP#5_t2jRg}xDwIislKRTG^6K}drjQog5IBEeZ}ae6dgdw+OT)IYbQ z0B`!a$b+^~*2s?|b0>&Cg}x(1|6q8V`3HRFxyTE;&-ORey!6`RuGkn5&pin$qE~z> z?%lwlOpWo&jL`fqc`E{g=J0>p-{1Obu-7o=2uj-hhzxClp9~Izi#ktZ!rThNQ5_MeTou@-l z8UXk)oiyB+@0Ken^aw4prkk;aCZE}uY;Vez^1(C#A6HXj`pS6k`ew+p2s!5WkF@UU z7>;6_m`%4;l>nLP@J~2;6DK*7`X)l-&AIt3k9Z8bm*WdHC~J|ge?mb!Aj6h;a zCMwW2;$Jx*ZNCY`k2%qxn!ZoufMP~tTuHIlQD6s`&F=Z=0Jkh{Bxl0-<@g(mmhoT?xqou*YMi@KJdkc+%g-C0P_%Xz3WJr& z!Un({d)C=|GT{P9cqZtDV-9rI770gcSZV>wq3vl9wVHP}d%VRxv6;}p(QPfo5r8(# z*}>beRXOg)-Oy&noYw~W*>4KCjK;cF(S2fq88O#%M$N)S7fW{*{N54@%rj>JNIu!R z+j!cXbGg~fxL1wbg5INF_C@o`%oi0!^Dp55{*ANMmp=+VnjCRg^<|@;j`QZH16-%* z^F;?8?tuktpJBAq#&McT+V}DZ@CnYStS1(;BOj__DS_LWWEoAI`yurHYF2Cb`N;Y27kOPMizDbdILLF*d` zdo$Ar>=S4Q5xaxA74Irri_hsQm)=qmi2O>}aa1TqtD)PyhR)gr(|YSq@QTYU=p3Bt zj;9cLj-ud5`SEL1m0=bIO-9U7`U!hiih-D1GP|!HParp+xjXCSzfPGUf)~m%XUl{) zJJBUGUbG_kJWrPaESf+Rd%21=9j;!{n$Qlr9J6oXh4!muTzn0=;&+rzghKBLtg5;S zTg~KVjX)So&_r)+D#CW#mq;b{Tr+o8{K-z?vDttDML%{I45;Bn<^EXqCsoh=25WDf z&EOGn@-6kzr*4un=JyStIKGpxWXoGhD4xa4CsDqv@gj3{0qyT^gD-+&DqeU)Bu%~? z6@wn&llElz?%kVOoNOaNO(5rWKQx*DHQVPc;y^RI7&nOGFfu~0jDBb!JBIX$A zz^7L02~~vPo{W zpkdHuw{$o=1buTO-jfnWE)>>ToLp>VLp!*>7d(d?o1-dDO(CWtD8ANWv~t(A3Bhja z?3`*S7sk1luz|w(a%=H?GAAo1QD+23AMU-qM8ilNVd(8a(`a}#Qv*#A^hxbjdtZdo z0e4Fj-BR8+Dr5Orb(SK)shgliId3>g5^MQqA(wQx;|YPcN;AfHv@wW{sdgX-Se@v( z*E#Kd9JM(TM*tdIqSd*N|Af2Od17%5`LM87lJ9@GX;hQeo*R4{<_wK_jct~g$Zn1e zr_@%gUdesa;3rF7`IK;$6!D_VQJW!`+A`91Gn7OG-MH;H87BYF;$7y|7n@i+9OxD* z=@uA)k+SKPOU^{L9N(6lwa$y^dl`!r*9Cf^paXzF;a4m@?ub6VdoTE)uY{>-f znMMLefl8ah?@Ju^df+O{62Vs_%(`>^tKmob>R{~uut_;&4Iav;+ zVc3ji8d_QZnQ!v_47<=ktpLO{xr#tfcz&0I#C<+P!_COY!9Jassj?qF5ds%KE$62z4@E{x)AyBc@UG1AQwBHqG4rp3LstF0M1D zO*mywbEc$WSl_!r113C-FaZ;CXq2b+ zGoA&f{Ln$L3-|}C^fiK~AHo?;7dMyP-f1enqH-mq$=j)VuRb{_kXl}0wWtkd?%>`Z z;JKrbGRiP?|7bULo|;`8LOe!rf~9BrljeXKH0mesXVW~MVk^wH=~1+^(oGUDJ)F96 z90sH!`?gxohK7BYJrSxbR(N+y&$G zW0HOA)?0%y2;1L7cxhBrzp)FM>QdxV*Tqh{$CH0z&W~I@r_bQsCZXX!W^SlnbnArI zK0C!L2Vdev_s)Fa4mQOf>HZ`;R!>bHTAt9z-nh(=MNK|fVrX%8q&M8QPS5w8sZ+J6 zhs0II$7glJ!)XZqVUXIBnr4}#Ok#J4X?Xn;5y=;7Mte2dN3C$^6 zoj%HnYCk;;dgv^EE5-1iD`H`NJJqWOuloK?58|cpB+7M6@_ivpL3_tW$Fb|Ttn`hA zqABZkJ7|iNcB4w$dQY>|&>~wAG!0x23ev0NJdDz^w$F@b>IUi$SjceG_ciwqS?9F$ z4{`1jjaLa}*(xS`*)0#ERwi8&1?AO*NNk4!$zzj}(5&igJxNRjqP9V+n`%o~x+8_p zmq&d13e77AGFL+1!q<84m&Fom=l=ErzM9Q%(H=mDyBPQVwHi)IBBl3yN!xh>7! zd^iFPJ+)UKaQoLDXW#HR!(@4M==#KLIdZH3+en|?u0g3V2enLl%5Kx3I1lJHUB=M+ zM7=!7OJ4O>8Lbumrm(+gh@@>Gpdzp0Yut@Mm^7?;QRN+MpZr}u>EJQWexnZS#@bQ; z7y>nJXj6Di4X0QC5b|eY6FZUWlB<}HcS}7k#6e6@I2YP(yiHj_pTrv|oi^ zYP*M91JhW}nhNl!|3&9CEUMoz@g9Nw46%J+~#6;k!Lg^bad59UoZ}L&eh#8{2(Q6+vc)Hnw&g z0Z7jz7Aw`Z7|*V4D31?X0)P zB5+t5XnLq5=uM+a#}ls6FzEHfteO59k;068bSzXn!S{vB_$c8EqKJ(6(wHqR{cq;= zX@5%GU)-QG|Elt8R8>}%gO?R@sjVi>9bSTWtW$TSzxR8a6E)>IT2x|K!+=9>=ys4; z_~H4^>uRl{yYt<8Mwm67stxG`gy3I5;o7|@9JvOXF1!FYK@^Yu>e=#Dh!W||PF!yG`w`^P+^r9lL zL0BA>OIS(lf`m!Xa-kF6ZDR!?Ck+d^Q&|I@2KD>XOHRQDjDemn%2=zZAqJ$6X=Lo) z)h7rptM}MBHu^4XUWDV|x_n5#rU=DXcv38F3(seo+CBCCF-9Tm7#)4;qIuwIj`yNUWtOMpN^**EH?o_W?2?~%cHr3E@5`9BAAf7Xrzu(sw|GeS56Q*q5)2k$oKRD3-_L@z-VnP} zd#^Ku)or@f2XtLU?Hb0}ThFE@x@(65RJWmf-LYN}P5@eShvkn)!0Ty>V0YN1K^hdA zcgZxE16yz!3b_!BB=B*8owkyUrtO&rC*!WadtJy-=5tSbQS6rT1xww80iwXrXkp0h zf1y8pOlf>sZ(8IbxslVwNTOLox@~m7Yov)Nel&F=po44C?dm@!Jd}wa(9Y=ZSb>Vm@94@IcryT{7}dwK z{2A?x!fvo_-@ff5qaO|+NSgC4XpZd^M`^DC1zJ;`D{1S~bhcsjr%W?n=GnoD$L@;k z=j75sF-7AL!sx+Y(|LZ-f!OP+K7wb-eV8m$zV**;@s98FEu$xmM}y;3(k#WqSvHV= zjod|DnGd3*_a)EUHrb3$18gk`*G7KcVF;k2F2nH9g-n49mdP0k!HRBzD?FUvRHV6O z*N9q!hlr%2J;#TRa3;rw)W>e&3R#Fl+TZVC>|0EihWhP4X8^p4_Ma2PJ<(vtcZ?Xu zZP_KF{wiVqYnD^~_LbS7b#c^p`Ez=sxQB!-wsb7kJSd?hJ<=C59WFj#Vr2Q&&sHK8 zO&B-!Vz)rbLaMj28pF^D@`n}_T;q@>Ju5?dGnjbPR_9lq8vwS#*qWc zt_hC8FrAlg(l&J-sDU4FcH?;JaHLPh{BA$4nHi<5kjEOmNPFvi@m3*=@)T8Z#>!P*tmytQp$?8yW5i)?E~i?Y1npbS^(|!TpT0whNqZ@9mk*1wLb2 zQ%@A$plA5o>922OG3S0ueRUBx`tha7g&;S0-DkjgFOT?2O(UUF5LLk1A9d=dNr7EEfI=`O+|}X}jfXwXggN&G^uEv>LmXP(1p!rJUGyFQV{( z+<(zMDI@%T7=^nsb!cxe5XC0Ry+t?rc-ufitfb;fw8{E6GF&uhss?ewiSv{u4_PGl zA78Zh0FFz7W~p_oBwHUZS|tnr$SCtg2V!gh)8@EBvvI2=*I%V#d4d9A6Ae2VG?+aE)i?YU;IOfL?Sy_td9@kvua~+-uMo}O=U@7N z4}KB3W?7kk0N`7ATWMwz@yseoaX2pl?S$|#Pt@W$WXQ9KoV5>9(R1l1nvG*~MH}$> z6Hm&h!z`Yd;S=PL+E0P=Q`MqkBL1|w3&L+G{*bd4hTtV|TQoS6O(e-G6b#p3O|{yy zwHS=!n+@q;Y*tlfShLWPPMN$W^YRZaeoC4D`PxFc%}1y8>2-bD*^@JP z=BGTBHh)v7>ekDtq@+fk#K{LR_P;mHu zrtzjpzK_O&vS%(l2{xTH-)9Jevp(D*U^YY9+In2FBeqDPhxnD$C)-Gu0E{h!$u-wT zt4gfUouzF|^~TYXp~3Gy9x?NKEW^0Y#&y64!93cz<~_EAUdc9_NL<}d67L0>fnbLb zkAz_>%whk;*4Ty2S+=k4!DW;!V&z zE}L%o0Uwi7mA#pBj*?$Ni{8z91Rv4t1EqV=OpQhX(&wJ{3h0)r)C=4cL~B_reojR= zHsJV0(aNTR{RM8O^c@AByr!Zi#DR=NThW#e#5AolHN3URKm=_ZT)H)SVppDh!K$1h ztKF;j(kQqrud2p0-=4S6K$Kth*|WUemKA2Rtcrcjr}Q5Iz>X67iS}X_7j}?64a-)) zF>3+5o8$uXS;NMi4CD;z=C@cht3HBqvFgJ63KRBqA+Us}Gy=%DDeOom0t$qrSc1ei z=p=%LQ^Wm@f#Xxr@E+&E%W|t1GA)%MnuO>i?!;NzfiY>V7Nq_Gk5G^494iToUZ5(a z{YRijNIg zVRlajd_J0W;;v=+D%9BgX`6EQ@m zL)H3ip}iG$k@`D+7DBkkD^fy@-?W z!hdAIZ^PWhDW5j#Y*FbhCR}I*oHM~>g&zw?=!@&+ZN^1iZfIysx&|mkx#mjtAc^Ou z`#!R2Kf6`AFZ~G$$R)zu!>p*PE~FH~qeD6kVIgW4l6`ZPa)`8VY6|TAbOd($uG-zZ z+M43p#c%F%qG_JdE?AmY2efCwIJYIth%=#Nh1$5rBzSy}IzVJ)x6D+zACwk!Kp|{a zFSM?yaiL=4Y508|IG8rwl<5|5DZZ8K9$Dmb5JCtMax zG7x{V(P`EBkx(35&@5{PSxLD*6B7;Y^BtB%-Nv2NYLgR~rX znBzz6ZvcE7u5XTIH8M7bl8W!`Rnp(ti^LPmMm5efbG;|CnR{h!R6T=mD`fhVIGZUn{Wz6R`%ofuk(hk7rz?YGUZ%YR4DbtW8ad3UlR zNXcl`|8!Aa^gpvMXH6ts!ECSDCDY*y`QUEqY8ZM-SQM`igby~-k=WByxPJYs$O?f@ zWz2M_QDhBltb;($-dEMQ$OlEOP8QAZ6OiuUOa-12f>F%L-zfcekA!CLu1c#fFeTrV zKk6GD;Z1yCKd?V6G(TVctDmJ~N~KbI^o~k?Q=9AM_%h4>L!NY7(C(TYg){qB+hc6? zzH?H{pQ}Gz03lS?Arx7(JUO z^#xQ9q5N@s&)V|rU}Wvu&2XA9ph^XB^Pksv^(6h!zhg*+5f@+o++!c$x%*V`cy^kp4A5YZhA4CvNt@Q)6-m3Zq!o$pUEn0@XN4UsHIc3DGvH7TY{b=o z7xq5pp{|ZeaBQTXtDe6|r{n}A18fR3|AK@mC?cW)HS^-(KmLDA6~&jvQyK}2#*~eu(lUtw+l)m=^Df`TmyT1UFsXh% zQI}FqCw@-#YKa$zAFJYS{Q@vH*{+C8s zXx-fB={EZC)nVNXL|uI(?bwfwhes!FX#I+pJ6WHv!Xo2EK~L#6SN|+i-hC%wCOTOA zyJu}Q&)oGg^YGh1Xi_*2pTVhtWpqR)pL}WUUWv-EN2hA54@r`303~f$T+k*gEN-{t zPFo;o*hlb06?;35Cc|*Au_4|Fc~UJosrv1@s!Qf{@bgeUkaU9Eb}D_wH%uIC6f)?1k*kQs;*s{pR|HJC ztzYH0zx@E0phrCkck}9(1uA<*s$B`OTGB~^k7HhnepUFiTA3pq?Lj{GX=04;>u|GWTpnu>vk@GjjAVSSnn`!(aNL0_wd{o8#=;m zKQFSUNAM7164}Q@wf*prm)?;zXFEPoDm{_%N5Og8@IO=1p>vM=b<{&!o_z>VQIi%2 zs2KVE1834Vwt?LvKw0VTj1{i`w)mIaUso49Q@TK^T@cCWJ4ibhMy4E4q+m5OcQ~Q> zImCWE%Yac|=`-j5KdOAvpEd}5(dKr46%sr{~Cg~js#GWKtMIU{dt^TOP zI;!^}uPG?RS(X=&0gubjGJ2o+tgz%R=+j}=aRE~qBpktFMY^Xum~*=(4&lsB|?&SSV*Rf3< zUEbCF&(3QV0Pot}xDsws1|0U|)*qIW)B%SXcwu7+Z0AMVt=&FuAJsH5wWa*sPFuzR z@-FJL9=Vr6kSlI&;>spQPr4&?{Z?E799s^82g@TkV^@ta2z`33imuVmmG~LNLpSRW1baCq`k}oxIxP30194F0-XX%oW zBk@vq7?YDXL8hLTc!{xWt+_`e*uy1pw<>{jrpD2Rl9K-!e3F@xU!m-oIB<*H+(ar$ zaQ?>-Cq*6Yf{zjGq^^VKr7=9A{Cm+gH|}lBBZ3plT9#Ijt&N$UxYn&rsdmYi=^P_} zG9hzvTIEklToX+a(K^Gs8?S$lF`WybqK&F>JH;MMP??M-DGoV#_ovV`9ep}>wl;PsuH5g-1}2I6(!AOc=pVI6&iO_s?kxIx{N*V8e;)X+Ak}{dV)}o8RR3GFX@v5J z+X!WfAgOuE*q-U_i`SLHM{9Cmp40Pd;Vj7@%hkW(na%4rceV1tEl$rq6`iMwO?!IP zPG~KTs06=a)jS%w|8Y{Ia3Rn6!DHI0oO}6ip6I^w(HiIFDDdwi0HcfC{4L5`zR~py zGWkuI_}&>qBnu~ljOrXqFds=Y>reb;bL2z#+FzS66mFYC8pm0D#iteO#(()g{;mAA zA)uTPm*oLI5hqH16H|p@C}mEU+{*ZdILa)g&&rLad+|Ue?>F((d;Ng-N6LXAWU1VH zDnX!pP9W}wG6SyN^&^A9yhmS>%o65guao9Z9&n?a9l3WjJo;5393I_-Bles%>th>s zc|F6BLO~jeNgg5d<+sh&C))SUWIDo^(H0St2zu63Si5`JpJ4-`-)h343pcMJ(L}HJ z4dt@uLIq*(xUqh=H{xV$9mg9N=A$q9RfX#t0I`12?jsZGOfYOe-`gi2ZDDz2K3}0C zRY(5wizDswUr;ISP*)}3e(}nRj?p5FPyrA3JCrFVS|%i&X=a@jcjIgf$#R=>u^?7^ zHWsrUJNmNB660IFC*OjA{*Kc|ViE0-Lm}E!>ib128c*p|`R_tX~rv`=mvfj&tSQ25c; zhsZe7_v~h;JRlhHZoabpH$1>X)>a0I! zO}vIfyzR!RhVuusXJdu=+WLBykL(>ennfAL(XTOkMfO#K?>QS%-NVFlop@K&M!-N9 z7F3yhhretsQ=8iR?KC+MkbeL(1Sk+Wuzr&#+>-XZCM?lRUfYiI8K;^&f}@^(qfLG& zIC%QcIdXR!JTU^zhjA+i8-&>AU$n{Z|NlIoGXs@UaG#N9^!Vw*uVE#*L{oIs=_2>P zBs%Q+=gsN2vu)?CnTh_Uf|L(RjCPSyivQk({rmZ(^9`%L6srg$T{bb)t1~f&*1uj+v$IYhn-D$J`6yDbNm!1gO3!yfN zBBJrPH>mu>gR)DGw|m!0vu?B8s8GbN&~^q^I7Bb$I`s=?#d59EDoh4hvagSrv!30| zoIY~n%`^?AyX+39B3d`A-ke7!Aw`0>^VwUC63FN>xmOy8La0G0%%SyRqK~L+-o|Ab z@T=$aqH6(B>a5hG**kS}bP~Z~>=^k?>Dfp2^ZwBZ3XRCC?(v47sVQ-N#~S!FV(4y1 zbu+|#j#oU@mDDhDs@rYuxalw2g>52Q59xyVapXJ9XG3~sQ@XRnsZY-JFi{NN4&r^w zG&h^lH0vK`e&Z69vQ0TY(d?7bQL>39{0ThaNU`WBJ6uruu~@=Jlai5qFk{r7lMf7!VHNsIc6P5wRq#~1tB z!L9!z+1@h@`F1_k!`J74XL1NFnJMxd`N$73m1ZI*MX-$(aViO!=92|SKIUC(KM1-v zn#stsbnuExEcyBedA=#StT z`onJ$I)LxIum*${zEC>KWj1|oX*(8sSLhK9qm`FdBJcfjnEf>4OpeQEHHE9g?u`#1 zl?`=>m;D-xc~q&+`RQ!DG>NeI`YlMA#?EG0AQuFaJT?O1$TDJ*;;<7Td0xN5wR#X@;1_|-5=`C4p?`^zk~EPs=AgF{G? z6YiWy_gKH>L;vVyhxdF_TGki zs>5DR6hjw-%0zoxS95Vd1=X;{Xm8ueB?^lIs$ zVekI_5OIH5q`!G5H_qT2Kd6~jBuJ_uQ;{_rW4qJh0zA|9UGH| zhq=jLLivRG9^QN^8LyJ>-+Z`+E%y%9K!G6CZ=}Fi3R8)`6^(MNzNm%`MSI&SH|ocL@~lKD52aHjD3R`1{Q+?A_Pv@i(e7tIGak#+Vgh={`-B% zfBf@Y{hh+ktb%;e*^Ym9Sbr_GfFB!J5ZOD5*WWv8w=9%;?`7R63yksZVpxl&eq>6^ zst@yx8i!CN^SO}lCZ(2VE=jfdu3FS1t%A$jlzX5Heue5~ZHqjbYe`PI-mF#L1MDG#cfUyV7ajAc>)&?xCc#C8wzRe=evNYi3A`wa$o87Ou=-B;VC>-cZOh5_iHw zBmd8P&3ScJ_j(7|KZS2rTCF|vq0uej=Ke^#?TcyRR^AzvJ0z%a1X$q|b02hTl-?;_ ze$5p$Il*e$IEeO9 z-R$i77HRLR6L0x^tq`*@TnYai{A?ctdm$tOhv^*rkSdF2mSw3*7aD?-j&e8WF$rUC zA|22L;VA_ME^b9ya!~LtF3^L52Mau)_5B5!(E5P_Culvoz!+LTSl|!!>n~7; z`VAD=K_A(Dy~;8fAf>SD9I*9nXSr&9+-|15uQMe(2BOF@86>4ZbPm{mw?n8}-?t+N zRfUGU3u$#WH<$WF)lj+@Y=FW;CpRi2<29!tbj;(Ihd0gP z*tRuP_!N|?9ZMHc!$spF$2#FH+PL!1iNCW;5N2Db83SJ|dIM#IdZ)UD%JswQHmwv* zc;9r9E2-fc#>Or})&Em=z2{96aEUONeoAbKrh2_^fsA z)VY67{HgNy4>5%z8+K^%;?8#Jz^#{ed?KGvYelg-B?JD3aXkhzp+;iQ8tN0B~;mm>w)}!>< z#6Ar?E|@q;S|aTP6IV$qq`gT}9}zzp=)1gqOhi$jrF?H~6e#pHeJ~+tKPfl>j;B)C zC#7U_H?Iv_66!0bGP;ZSEX5Cd@ix7$i87RI5^6tTL;l>F>{7)Dk~DOQN$l_cXD zNI)#+t~i}|V#)ORBeFxq9z~cJP)kwW6V#5^mJ6t@uENvOD)vt3^d(v7DX{Bh?wCAe@tCJtgXHo3<-wQu&K&8)QN{=h zwRvQZ!6uI0WAjn*`L52~<6A~=Qt$Pq&X4U?q>4!|iC3DP>nx(FPBYkV=1LkG6i^eddJiz&UQ&eD_nf0w=`qTzCTmMV=>$oud!1 z#id{?d3H^K0{3?r+%N41adJ4^{ST>oSqf=`J=$n0KY=8#$ta|vlRy^&tUgSdBU2pk zNW*mto_lledO_v8K0Q4%Q-#KTY!u0xFe(iHbnV&bx$!_R0!a?Xp$z67usTR)4ia)V zB`&5#vV%nF!lDJcBOe!o!%I({JjAb_SMm^dfkge+>Y}*#2lFNL><&+NA0Gf?v(NQ; zWQH#BNA9kvUOu&`ausnsu>Y#ZFjb6%!H)o#`&0=%r(+tUYT!-|H}e)>o=Sm&eO}#S zxZ0y9KLE(3>s*Ja=#ba%=O_f|PBQ!jq`3RL3^)XOQNCvCB^;Q5MF1Qg*#72Dwm_9q7b z5deg)008B$K+XX`MygfI|N~Zq82P3Dz-+XmG3=vHVBQvTZ^|4S`rr!uuKVv+y1h%r` zKP)M=>5arRO`m6w7=5B$v6GyQkuylIEW2j4{?fR4*5}!Ff|-B8uHp$xmdI^~-;kfe z4U`{66)G@m=Z}=a{Pn9(w)ixc?G{$l-7-iWfB$qg{7s&Tj6R+>ZP`VDLs0M*v`w6^ zWCaTM>2BxY$UoK>i7YXBN|i3MFkM>FBJz3BqtDpLOD@TUr?O`C_Co#$Ca{4x^rW|> zOs<81DK1aRmgc$OgNsKBY7>>7?e~ozyNN1QJlWtgHvQn59zDKdBjz?8(QbJI=dndw zvE)n0-gJ~^bS;E+Rk~H+CWzBbBhO6{@hGR#oQvjki{^0d6YEW8NNV`t4r&3eiZv~VhP@eETfd{%yY}H1C9^rZXxguv?j8G9 z>vtH}RWg%KA9%}ri9#*44 z()^yJ)oIsyYltGxRX*LEc;qX~9Ev@~AfA>L8EtsO1vWUJm&)O|$H6$IEAaD|UPVz8 z;aP#T5jE?oZ@ZaKLp+qv=+HIZe}CuI;%Q|qupmE7SXH%lU^Yj%;^uh$6)pV}8IqV{ z>$7W(=gy1GiPC+}$w;asD>ig3u56ABX$pLOswm{GIsVk?;{;`yVzKbEbh=C2h`RSC z506xb$v6{3SbA>Wfut9P-DlUydcd#i_6!+4lVF(MhkVQif8IzY-eVXxXeNM&sb&_D z;ap8i&Lg9rm4-9%&$~o!C6%+Ba){J#Jp7Kw@l&q^l{~895vrrSdA>kMna#hfn;9sg zwESV-y!bE~bkA46xYE7N*s}9uh?nk663_JrIf*dz_1v=`rEy~)ST%$?j-C5-cF4m2 zw~34wct`@jj7BnAqcS?~r6tUlW?Sd{-7qQ(04h;LDX(Qffaza};`kL{gDO z#R|L0jek(sD1Pjdi4mn~40b}|CtnlSMH9m9!<1h3(ZqY|w;@j$376jw28I_7k=@$ub;ye9$aNDS zOQm(PE)sS@`2>C~j;m|6mr(-L5u@O{) zI%^d|3+--!hKPo%T*}UzN{!b-75L-chpYAQ8qQyQZLO4+H@2-qx^ShVCwW{f1br<- zv@$JUF4=%?<@3uxY+2Ncth5p}ip??c6z{hqZ=Gsvv*Q+Ucv#HH%IhlnHU*xGlZCQb z^ire3T8p?*x1uUevnibL8~v~*332}MlB2f`j(L%?J6M9rzLlx5vB=GbWFSy2B?xo^ zkSSvo7nr9F%+u_qud9uR$&U=FvZU9r=b{X%`2EPU*z;nZy86s;R&qM z2eCw(}Nmc*a9Qm*lkE!*F&Q~S~*shmhh(PSanSOktY_KIz!n^E*kc8B@~BSers zw?jKT#(&zC z8L;y#P139rd#AY7UG}^t969yMgtBxI4%1aws_vv{^G_5-l-XjocF%*YwMy@VCqo~`(%92h+TVzju$<`h>pv8 z+@I!9L-9t2UF%KM@q2mQeKg}6y-LzaP?&o;24sVxaLu`fZK&COF5T@)iOyihYaCu= zw4PUUh+3Gug1LjmOg;7kqt8-wy2)@4o2ox-o>Fn(~@YvI^2A2NFEI02RrHFkmcvY~FM_Y0Y^X-9&2%nF; zMZrbk)rvVU+eLIV$qp$1Ec|aHIWJwm6tIs$a)&^mW8d1rz>%4|jkAY{@Im@#L<+tU z=6c`~4&c5x{tY54kYnv)rQ`16>LFs~;%;+rfBYF8ANx=w8bBKbe&3ql%s@@Ppj)`Q zeh>R}s%^?PfC&eRqW`Aw82}qF^nG01oh)5koW6%l)?>^)2_O{%m$H9@%uew;@*l1K zn#6Z|z^J<+Y!R4W?C2jz#n+oEkPB48)7}PZ^G7Q7y^7rQtiE+Q4g|tqCkL^9jSun! zSn;n`c-Y(6czOUc7vS%^RKA_&d`)KW(eG6LZPN2+UVJ-beobNh@gEev44{8T{Z=!4 djjEal`ktQ9(L4+&5fJDou!jH);4U~g`Y-mX(D?uW literal 0 HcmV?d00001 From 88bbac9849cea9642c874e6ca2bd29ee81f99910 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 2 Oct 2022 18:37:55 -0700 Subject: [PATCH 153/156] Use Locale-Independent Float Conversion for Xlsx Writer Custom Property (#3099) Fix #3095. Issue is not a problem for Php8, but is for 7.4. Code used to write Xml for float custom property uses locale-dependent (in Php7) cast. Change to use locale-independent (all Php releases) `sprintf('%F'...)` instead. --- src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 2 +- .../Writer/Xlsx/LocaleFloatsTest.php | 36 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index cb8758c2..02259c09 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -216,7 +216,7 @@ class DocProps extends WriterPart break; case Properties::PROPERTY_TYPE_FLOAT: - $objWriter->writeElement('vt:r8', $propertyValue); + $objWriter->writeElement('vt:r8', sprintf('%F', $propertyValue)); break; case Properties::PROPERTY_TYPE_BOOLEAN: diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php index 13a13bc9..7a4b9ca2 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php @@ -2,9 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; -class LocaleFloatsTest extends TestCase +class LocaleFloatsTest extends AbstractFunctional { /** * @var bool @@ -16,6 +17,12 @@ class LocaleFloatsTest extends TestCase */ private $currentLocale; + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var ?Spreadsheet */ + private $reloadedSpreadsheet; + protected function setUp(): void { $this->currentLocale = setlocale(LC_ALL, '0'); @@ -34,6 +41,14 @@ class LocaleFloatsTest extends TestCase if ($this->localeAdjusted && is_string($this->currentLocale)) { setlocale(LC_ALL, $this->currentLocale); } + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->reloadedSpreadsheet !== null) { + $this->reloadedSpreadsheet->disconnectWorksheets(); + $this->reloadedSpreadsheet = null; + } } public function testLocaleFloatsCorrectlyConvertedByWriter(): void @@ -42,18 +57,17 @@ class LocaleFloatsTest extends TestCase self::markTestSkipped('Unable to set locale for testing.'); } - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $this->spreadsheet = $spreadsheet = new Spreadsheet(); + $properties = $spreadsheet->getProperties(); + $properties->setCustomProperty('Version', 1.2); $spreadsheet->getActiveSheet()->setCellValue('A1', 1.1); - $filename = 'decimalcomma.xlsx'; - $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); - $writer->save($filename); + $this->reloadedSpreadsheet = $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); - $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); - $spreadsheet = $reader->load($filename); - unlink($filename); - - $result = $spreadsheet->getActiveSheet()->getCell('A1')->getValue(); + $result = $reloadedSpreadsheet->getActiveSheet()->getCell('A1')->getValue(); + self::assertEqualsWithDelta(1.1, $result, 1.0E-8); + $prop = $reloadedSpreadsheet->getProperties()->getCustomPropertyValue('Version'); + self::assertEqualsWithDelta(1.2, $prop, 1.0E-8); $actual = sprintf('%f', $result); self::assertStringContainsString('1,1', $actual); From 339a5933c7285f68428bb17067ee5ed8f245ecd6 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 Oct 2022 12:42:01 +0200 Subject: [PATCH 154/156] Allow single cell AutoFilter range --- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 8 +++++--- src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php | 4 +++- .../Worksheet/AutoFilter/AutoFilterTest.php | 16 ++++++++++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 05b2e9a0..3ad5f9bd 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -9,6 +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; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; @@ -104,7 +105,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 +116,7 @@ class AutoFilter if ($range !== '') { [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); } + if (empty($range)) { // Discard all column rules $this->columns = []; @@ -123,8 +125,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; 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..54af9e0f 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(); From 720b98a97f9098c1f74230bb35b1367d00fdb4c1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 8 Oct 2022 13:23:30 +0200 Subject: [PATCH 155/156] Calculate row range for a single-row AutoFilter range --- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 32 ++++++++++++++++--- .../Worksheet/AutoFilter/AutoFilterTest.php | 22 +++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 3ad5f9bd..d6041985 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -10,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception; -use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; @@ -176,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]; @@ -249,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); @@ -1033,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; @@ -1055,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/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index 54af9e0f..ebec3686 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -510,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'); + } } From 7127af7740a4eac4b8e1312b6aee06004369ea5c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 11 Oct 2022 10:23:01 +0200 Subject: [PATCH 156/156] Change Log Updates --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76832f6f..3de5250b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Xlsx Reader Accept Palette of Fewer than 64 Colors [Issue #3093](https://github.com/PHPOffice/PhpSpreadsheet/issues/3093) [PR #3096](https://github.com/PHPOffice/PhpSpreadsheet/pull/3096) +- Use Locale-Independent Float Conversion for Xlsx Writer Custom Property [Issue #3095](https://github.com/PHPOffice/PhpSpreadsheet/issues/3095) [PR #3099](https://github.com/PHPOffice/PhpSpreadsheet/pull/3099) +- Allow setting AutoFilter range on a single cell or row [Issue #3102](https://github.com/PHPOffice/PhpSpreadsheet/issues/3102) [PR #3111](https://github.com/PHPOffice/PhpSpreadsheet/pull/3111) +- Xlsx Reader External Data Validations Flag Missing [Issue #2677](https://github.com/PHPOffice/PhpSpreadsheet/issues/2677) [PR #3078](https://github.com/PHPOffice/PhpSpreadsheet/pull/3078) - Reduces extra memory usage on `__destruct()` calls