diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index a393043d..8a8886c2 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -26,7 +26,7 @@ $config 'combine_consecutive_issets' => true, 'combine_consecutive_unsets' => true, 'combine_nested_dirname' => true, - 'comment_to_phpdoc' => true, + 'comment_to_phpdoc' => false, // interferes with annotations 'compact_nullable_typehint' => true, 'concat_space' => ['spacing' => 'one'], 'constant_case' => true, @@ -171,7 +171,7 @@ $config 'phpdoc_separation' => true, 'phpdoc_single_line_var_spacing' => true, 'phpdoc_summary' => true, - 'phpdoc_to_comment' => true, + 'phpdoc_to_comment' => false, // interferes with annotations 'phpdoc_to_param_type' => false, // Because experimental, but interesting for one shot use 'phpdoc_to_return_type' => false, // idem 'phpdoc_trim' => true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff894ab..c7ae1854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Implementation of the ISREF() information function. +- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions +- Implementation of the ISREF() Information function. - Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. (i.e a value of "12,345.67" can be read as numeric `1235.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled. @@ -17,6 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. - Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) +- Limited support for Xls Reader to handle Conditional Formatting: + + Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color. ### Changed @@ -28,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is determined by the Calculation Engine locale setting. (i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.) +- Allow `psr/simple-cache` 2.x ### Deprecated @@ -39,6 +44,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689) +- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) +- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. +- Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. +- Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. - Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) - Fixed behaviour of XLSX font style vertical align settings. - Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. diff --git a/composer.json b/composer.json index c0664de3..a0064df2 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" + "psr/simple-cache": "^1.0 || ^2.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", diff --git a/composer.lock b/composer.lock index 5434aa3a..7b76a2c4 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": "9dadb265f548dd4ddbfd2ee97d5a6aa6", + "content-hash": "ed42c40a4281d97171980367f24d0a25", "packages": [ { "name": "ezyang/htmlpurifier", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 84af87e7..669c512a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2300,16 +2300,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$errorStyle of method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\DataValidation\\:\\:setErrorStyle\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$operator of method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\DataValidation\\:\\:setOperator\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#1 \\$showSummaryBelow of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:setShowSummaryBelow\\(\\) expects bool, int given\\.$#" count: 1 @@ -2320,11 +2310,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$type of method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\DataValidation\\:\\:setType\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#2 \\$row of method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\IReadFilter\\:\\:readCell\\(\\) expects int, string given\\.$#" count: 1 @@ -3165,31 +3150,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/ReferenceHelper.php - - - message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\RowDimension\\:\\:setRowIndex\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - - - message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(\\(int\\|string\\), \\(int\\|string\\)\\)\\: int, array\\{'self', 'cellReverseSort'\\} given\\.$#" - count: 4 - path: src/PhpSpreadsheet/ReferenceHelper.php - - - - message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(\\(int\\|string\\), \\(int\\|string\\)\\)\\: int, array\\{'self', 'cellSort'\\} given\\.$#" - count: 4 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" count: 1 path: src/PhpSpreadsheet/ReferenceHelper.php - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\ReferenceHelper\\:\\:\\$instance \\(PhpOffice\\\\PhpSpreadsheet\\\\ReferenceHelper\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\:\\:\\$font \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 @@ -4240,26 +4205,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php - - - message: "#^Cannot call method getCell\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - - - message: "#^Cannot call method getDrawingCollection\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - - - message: "#^Cannot call method getHashCode\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\BaseDrawing\\:\\:\\$shadow \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\CellIterator\\:\\:adjustForExistingOnlyRange\\(\\) has no return type specified\\.$#" count: 1 @@ -5270,36 +5215,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - - message: "#^Parameter \\#1 \\$coordinates of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:indexesFromString\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getChartByIndex\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$chart of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Drawing\\:\\:writeChart\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#" - count: 20 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 10 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#" count: 1 @@ -5554,3 +5469,4 @@ parameters: message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Xlfn\\:\\:addXlfn\\(\\) should return string but returns string\\|null\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php + diff --git a/samples/Basic/44_Worksheet_info.php b/samples/Basic/44_Worksheet_info.php index 57822369..406c7be2 100644 --- a/samples/Basic/44_Worksheet_info.php +++ b/samples/Basic/44_Worksheet_info.php @@ -1,18 +1,19 @@ getTemporaryFilename(); -$writer = new Xlsx($sampleSpreadsheet); +$writer = new Writer($sampleSpreadsheet); $writer->save($filename); $inputFileType = IOFactory::identify($filename); -$reader = IOFactory::createReader($inputFileType); +$reader = new Reader(); $sheetList = $reader->listWorksheetNames($filename); $sheetInfo = $reader->listWorksheetInfo($filename); diff --git a/samples/Basic/48_Image_move_size_with_cells.php b/samples/Basic/48_Image_move_size_with_cells.php new file mode 100644 index 00000000..abf7ee34 --- /dev/null +++ b/samples/Basic/48_Image_move_size_with_cells.php @@ -0,0 +1,78 @@ +log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); +$sheet = $spreadsheet->getActiveSheet(); +$sheet->getCell('A1')->setValue('twocell'); +$sheet->getCell('A2')->setValue('twocell'); +$sheet->getCell('A3')->setValue('onecell'); +$sheet->getCell('A6')->setValue('absolute'); + +// Add a drawing to the worksheet +$helper->log('Add a drawing to the worksheet two-cell anchor not resized'); +$drawing = new Drawing(); +$drawing->setName('PhpSpreadsheet'); +$drawing->setDescription('PhpSpreadsheet'); +$drawing->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be two-cell because Coordinates2 is set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL); +$drawing->setCoordinates('B1'); +$drawing->setCoordinates2('B1'); +$drawing->setOffsetX2($drawing->getImageWidth()); +$drawing->setOffsetY2($drawing->getImageHeight()); +$drawing->setWorksheet($spreadsheet->getActiveSheet()); + +// Add a drawing to the worksheet +$helper->log('Add a drawing to the worksheet two-cell anchor resized'); +$drawing2 = new Drawing(); +$drawing2->setName('PhpSpreadsheet'); +$drawing2->setDescription('PhpSpreadsheet'); +$drawing2->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be two-cell because Coordinates2 is set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL); +$drawing2->setCoordinates('C2'); +$drawing2->setCoordinates2('C2'); +$drawing2->setOffsetX2($drawing->getImageWidth()); +$drawing2->setOffsetY2($drawing->getImageHeight()); +$drawing2->setWorksheet($spreadsheet->getActiveSheet()); + +$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth($drawing->getImageWidth(), Dimension::UOM_PIXELS); +$spreadsheet->getActiveSheet()->getRowDimension(2)->setRowHeight($drawing->getImageHeight(), Dimension::UOM_PIXELS); + +// Add a drawing to the worksheet one cell anchor +$helper->log('Add a drawing to the worksheet one-cell anchor'); +$drawing3 = new Drawing(); +$drawing3->setName('PhpSpreadsheet'); +$drawing3->setDescription('PhpSpreadsheet'); +$drawing3->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be one-cell because Coordinates2 is not set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_ONECELL); +$drawing3->setCoordinates('D3'); +$drawing3->setWorksheet($spreadsheet->getActiveSheet()); + +// Add a drawing to the worksheet +$helper->log('Add a drawing to the worksheet two-cell anchor resized absolute'); +$drawing4 = new Drawing(); +$drawing4->setName('PhpSpreadsheet'); +$drawing4->setDescription('PhpSpreadsheet'); +$drawing4->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be two-cell because Coordinates2 is set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL); +$drawing4->setCoordinates('C6'); +$drawing4->setCoordinates2('C6'); +$drawing4->setOffsetX2($drawing->getImageWidth()); +$drawing4->setOffsetY2($drawing->getImageHeight()); +$drawing4->setWorksheet($spreadsheet->getActiveSheet()); +$drawing4->setEditAs(Drawing::EDIT_AS_ABSOLUTE); + +//$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth($drawing->getImageWidth(), Dimension::UOM_PIXELS); +$spreadsheet->getActiveSheet()->getRowDimension(6)->setRowHeight($drawing->getImageHeight(), Dimension::UOM_PIXELS); + +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/samples/Chart/33_Chart_create_area.php b/samples/Chart/33_Chart_create_area.php index 57db90fc..95d9149c 100644 --- a/samples/Chart/33_Chart_create_area.php +++ b/samples/Chart/33_Chart_create_area.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -71,7 +71,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title = new Title('Test %age-Stacked Area Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_bar_stacked.php b/samples/Chart/33_Chart_create_bar_stacked.php index 0c87224e..bb2d8f6d 100644 --- a/samples/Chart/33_Chart_create_bar_stacked.php +++ b/samples/Chart/33_Chart_create_bar_stacked.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -74,7 +74,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_BAR); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_column.php b/samples/Chart/33_Chart_create_column.php index 5af0908c..68aa983d 100644 --- a/samples/Chart/33_Chart_create_column.php +++ b/samples/Chart/33_Chart_create_column.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -74,7 +74,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_COL); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Column Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_column_2.php b/samples/Chart/33_Chart_create_column_2.php index a62b4906..96f5e316 100644 --- a/samples/Chart/33_Chart_create_column_2.php +++ b/samples/Chart/33_Chart_create_column_2.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -82,7 +82,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_COL); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_BOTTOM, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_BOTTOM, null, false); $title = new Title('Test Grouped Column Chart'); $xAxisLabel = new Title('Financial Period'); diff --git a/samples/Chart/33_Chart_create_composite.php b/samples/Chart/33_Chart_create_composite.php index ce42d2fc..b2952420 100644 --- a/samples/Chart/33_Chart_create_composite.php +++ b/samples/Chart/33_Chart_create_composite.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -128,7 +128,7 @@ $series3 = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series1, $series2, $series3]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Average Weather Chart for Crete'); diff --git a/samples/Chart/33_Chart_create_line.php b/samples/Chart/33_Chart_create_line.php index feae2f27..fee2a284 100644 --- a/samples/Chart/33_Chart_create_line.php +++ b/samples/Chart/33_Chart_create_line.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -72,7 +72,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title = new Title('Test Stacked Line Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_multiple_charts.php b/samples/Chart/33_Chart_create_multiple_charts.php index 608ffc53..3032bc28 100644 --- a/samples/Chart/33_Chart_create_multiple_charts.php +++ b/samples/Chart/33_Chart_create_multiple_charts.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -71,7 +71,7 @@ $series1 = new DataSeries( // Set the series in the plot area $plotArea1 = new PlotArea(null, [$series1]); // Set the chart legend -$legend1 = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend1 = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title1 = new Title('Test %age-Stacked Area Chart'); $yAxisLabel1 = new Title('Value ($k)'); @@ -146,7 +146,7 @@ $series2->setPlotDirection(DataSeries::DIRECTION_COL); // Set the series in the plot area $plotArea2 = new PlotArea(null, [$series2]); // Set the chart legend -$legend2 = new Legend(Legend::POSITION_RIGHT, null, false); +$legend2 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title2 = new Title('Test Column Chart'); $yAxisLabel2 = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_pie.php b/samples/Chart/33_Chart_create_pie.php index 5480a18a..4b35b24e 100644 --- a/samples/Chart/33_Chart_create_pie.php +++ b/samples/Chart/33_Chart_create_pie.php @@ -4,7 +4,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Layout; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -73,7 +73,7 @@ $layout1->setShowPercent(true); // Set the series in the plot area $plotArea1 = new PlotArea($layout1, [$series1]); // Set the chart legend -$legend1 = new Legend(Legend::POSITION_RIGHT, null, false); +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title1 = new Title('Test Pie Chart'); diff --git a/samples/Chart/33_Chart_create_pie_custom_colors.php b/samples/Chart/33_Chart_create_pie_custom_colors.php index ca5397a1..91f7e51b 100644 --- a/samples/Chart/33_Chart_create_pie_custom_colors.php +++ b/samples/Chart/33_Chart_create_pie_custom_colors.php @@ -4,7 +4,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Layout; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -79,7 +79,7 @@ $layout1->setShowPercent(true); // Set the series in the plot area $plotArea1 = new PlotArea($layout1, [$series1]); // Set the chart legend -$legend1 = new Legend(Legend::POSITION_RIGHT, null, false); +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title1 = new Title('Test Pie Chart'); diff --git a/samples/Chart/33_Chart_create_radar.php b/samples/Chart/33_Chart_create_radar.php index eba4dc39..59a8a5d9 100644 --- a/samples/Chart/33_Chart_create_radar.php +++ b/samples/Chart/33_Chart_create_radar.php @@ -4,7 +4,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Layout; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -85,7 +85,7 @@ $layout = new Layout(); // Set the series in the plot area $plotArea = new PlotArea($layout, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Radar Chart'); diff --git a/samples/Chart/33_Chart_create_scatter.php b/samples/Chart/33_Chart_create_scatter.php index c67e4e95..9a54c18b 100644 --- a/samples/Chart/33_Chart_create_scatter.php +++ b/samples/Chart/33_Chart_create_scatter.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -68,7 +68,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title = new Title('Test Scatter Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_stock.php b/samples/Chart/33_Chart_create_stock.php index 58686784..34fa3a6c 100644 --- a/samples/Chart/33_Chart_create_stock.php +++ b/samples/Chart/33_Chart_create_stock.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -79,7 +79,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Stock Chart'); $xAxisLabel = new Title('Counts'); diff --git a/samples/ConditionalFormatting/01_Basic_Comparisons.php b/samples/ConditionalFormatting/01_Basic_Comparisons.php index b201005b..5c5381ae 100644 --- a/samples/ConditionalFormatting/01_Basic_Comparisons.php +++ b/samples/ConditionalFormatting/01_Basic_Comparisons.php @@ -30,7 +30,8 @@ $spreadsheet->getActiveSheet() ->setCellValue('A1', 'Literal Value Comparison') ->setCellValue('A9', 'Value Comparison with Absolute Cell Reference $H$9') ->setCellValue('A17', 'Value Comparison with Relative Cell References') - ->setCellValue('A23', 'Value Comparison with Formula based on AVERAGE() ± STDEV()'); + ->setCellValue('A23', 'Value Comparison with Formula based on AVERAGE() ± STDEV()') + ->setCellValue('A30', 'Literal String Value Comparison'); $dataArray = [ [-2, -1, 0, 1, 2], @@ -45,11 +46,18 @@ $betweenDataArray = [ [4, 3, 8], ]; +$stringArray = [ + ['I'], + ['Love'], + ['PHP'], +]; + $spreadsheet->getActiveSheet() ->fromArray($dataArray, null, 'A2', true) ->fromArray($dataArray, null, 'A10', true) ->fromArray($betweenDataArray, null, 'A18', true) ->fromArray($dataArray, null, 'A24', true) + ->fromArray($stringArray, null, 'A31', true) ->setCellValue('H9', 1); // Set title row bold @@ -58,21 +66,31 @@ $spreadsheet->getActiveSheet()->getStyle('A1:E1')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A9:E9')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A17:E17')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A23:E23')->getFont()->setBold(true); +$spreadsheet->getActiveSheet()->getStyle('A30:E30')->getFont()->setBold(true); // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFill() ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFill() ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB(Color::COLOR_RED); +$redStyle->getFill() ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); @@ -166,6 +184,32 @@ $cellWizard->lessThan('AVERAGE(' . $formulaRange . ')-STDEV(' . $formulaRange . ->setStyle($redStyle); $conditionalStyles[] = $cellWizard->getConditional(); +$spreadsheet->getActiveSheet() + ->getStyle($cellWizard->getCellRange()) + ->setConditionalStyles($conditionalStyles); + +// Set rules for Value Comparison with String Literal +$cellRange = 'A31:A33'; +$formulaRange = implode( + ':', + array_map( + [Coordinate::class, 'absoluteCoordinate'], + Coordinate::splitRange($cellRange)[0] + ) +); +$conditionalStyles = []; +$wizardFactory = new Wizard($cellRange); +/** @var Wizard\CellValue $cellWizard */ +$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE); + +$cellWizard->equals('LOVE') + ->setStyle($redStyle); +$conditionalStyles[] = $cellWizard->getConditional(); + +$cellWizard->equals('PHP') + ->setStyle($greenStyle); +$conditionalStyles[] = $cellWizard->getConditional(); + $spreadsheet->getActiveSheet() ->getStyle($cellWizard->getCellRange()) ->setConditionalStyles($conditionalStyles); diff --git a/samples/ConditionalFormatting/02_Text_Comparisons.php b/samples/ConditionalFormatting/02_Text_Comparisons.php index 53aa84ad..10cd25b8 100644 --- a/samples/ConditionalFormatting/02_Text_Comparisons.php +++ b/samples/ConditionalFormatting/02_Text_Comparisons.php @@ -74,14 +74,17 @@ $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/03_Blank_Comparisons.php b/samples/ConditionalFormatting/03_Blank_Comparisons.php index c8584c3a..33b17ef6 100644 --- a/samples/ConditionalFormatting/03_Blank_Comparisons.php +++ b/samples/ConditionalFormatting/03_Blank_Comparisons.php @@ -46,10 +46,12 @@ $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/04_Error_Comparisons.php b/samples/ConditionalFormatting/04_Error_Comparisons.php index 957569cf..1c9c7b58 100644 --- a/samples/ConditionalFormatting/04_Error_Comparisons.php +++ b/samples/ConditionalFormatting/04_Error_Comparisons.php @@ -49,10 +49,12 @@ $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/05_Date_Comparisons.php b/samples/ConditionalFormatting/05_Date_Comparisons.php index ef9ad405..0834939d 100644 --- a/samples/ConditionalFormatting/05_Date_Comparisons.php +++ b/samples/ConditionalFormatting/05_Date_Comparisons.php @@ -108,12 +108,11 @@ $spreadsheet->getActiveSheet()->getStyle('B1:K1')->getAlignment()->setHorizontal // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); - $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); -$yellowStyle->getNumberFormat()->setFormatCode('ddd dd-mmm-yyyy'); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/06_Duplicate_Comparisons.php b/samples/ConditionalFormatting/06_Duplicate_Comparisons.php index 0b94bd47..cbed0eb2 100644 --- a/samples/ConditionalFormatting/06_Duplicate_Comparisons.php +++ b/samples/ConditionalFormatting/06_Duplicate_Comparisons.php @@ -51,14 +51,16 @@ $spreadsheet->getActiveSheet()->getStyle('A1:C1')->getFont()->setBold(true); // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); -$greenStyle = new Style(false, true); -$greenStyle->getFill() - ->setFillType(Fill::FILL_SOLID) - ->getEndColor()->setARGB(Color::COLOR_GREEN); $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); +$greenStyle = new Style(false, true); +$greenStyle->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/07_Expression_Comparisons.php b/samples/ConditionalFormatting/07_Expression_Comparisons.php index 6ea16bcc..0f2b1a84 100644 --- a/samples/ConditionalFormatting/07_Expression_Comparisons.php +++ b/samples/ConditionalFormatting/07_Expression_Comparisons.php @@ -28,6 +28,7 @@ $helper->log('Add data'); $spreadsheet->setActiveSheetIndex(0); $spreadsheet->getActiveSheet() ->setCellValue('A1', 'Odd/Even Expression Comparison') + ->setCellValue('A4', 'Note that these functions are not available for Xls files') ->setCellValue('A15', 'Sales Grid Expression Comparison') ->setCellValue('A25', 'Sales Grid Multiple Expression Comparison'); @@ -69,14 +70,16 @@ $spreadsheet->getActiveSheet()->getStyle('A25:D26')->getFont()->setBold(true); // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); -$greenStyle = new Style(false, true); -$greenStyle->getFill() - ->setFillType(Fill::FILL_SOLID) - ->getEndColor()->setARGB(Color::COLOR_GREEN); $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); +$greenStyle = new Style(false, true); +$greenStyle->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $greenStyleMoney = clone $greenStyle; $greenStyleMoney->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_ACCOUNTING_USD); diff --git a/samples/Reader/01_Simple_file_reader_using_IOFactory.php b/samples/Reader/01_Simple_file_reader_using_IOFactory.php index 584fd5be..622df231 100644 --- a/samples/Reader/01_Simple_file_reader_using_IOFactory.php +++ b/samples/Reader/01_Simple_file_reader_using_IOFactory.php @@ -5,7 +5,7 @@ use PhpOffice\PhpSpreadsheet\IOFactory; require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); $spreadsheet = IOFactory::load($inputFileName); $sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true); var_dump($sheetData); diff --git a/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php b/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php index 9a705123..9d4ea686 100644 --- a/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php +++ b/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php @@ -5,7 +5,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xls; require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using ' . Xls::class); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using ' . Xls::class); $reader = new Xls(); $spreadsheet = $reader->load($inputFileName); diff --git a/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php b/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php index 305651de..47b5264c 100644 --- a/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php +++ b/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php @@ -7,7 +7,7 @@ require __DIR__ . '/../Header.php'; $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $spreadsheet = $reader->load($inputFileName); diff --git a/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php b/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php index 98aabfc6..3a7dd065 100644 --- a/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php +++ b/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php @@ -7,9 +7,9 @@ require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; $inputFileType = IOFactory::identify($inputFileName); -$helper->log('File ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' has been identified as an ' . $inputFileType . ' file'); +$helper->log('File ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' has been identified as an ' . $inputFileType . ' file'); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with the identified reader type'); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with the identified reader type'); $reader = IOFactory::createReader($inputFileType); $spreadsheet = $reader->load($inputFileName); diff --git a/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php b/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php index d3ce9d82..912040a1 100644 --- a/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php +++ b/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php @@ -7,7 +7,7 @@ require __DIR__ . '/../Header.php'; $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Turning Formatting off for Load'); $reader->setReadDataOnly(true); diff --git a/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php b/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php index 5507c52b..247fef11 100644 --- a/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php +++ b/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php @@ -7,7 +7,7 @@ require __DIR__ . '/../Header.php'; $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading all WorkSheets'); $reader->setLoadAllSheets(); diff --git a/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php b/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php index 142a17f8..a78de203 100644 --- a/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php +++ b/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php @@ -8,7 +8,7 @@ $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; $sheetname = 'Data Sheet #2'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet "' . $sheetname . '" only'); $reader->setLoadSheetsOnly($sheetname); diff --git a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php index 66efc3e0..0cc58518 100644 --- a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php +++ b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php @@ -8,7 +8,7 @@ $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; $sheetnames = ['Data Sheet #1', 'Data Sheet #3']; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet' . ((count($sheetnames) == 1) ? '' : 's') . ' "' . implode('" and "', $sheetnames) . '" only'); $reader->setLoadSheetsOnly($sheetnames); diff --git a/samples/Reader/09_Simple_file_reader_using_a_read_filter.php b/samples/Reader/09_Simple_file_reader_using_a_read_filter.php index 04c47c64..4603164c 100644 --- a/samples/Reader/09_Simple_file_reader_using_a_read_filter.php +++ b/samples/Reader/09_Simple_file_reader_using_a_read_filter.php @@ -28,7 +28,7 @@ class MyReadFilter implements IReadFilter $filterSubset = new MyReadFilter(); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet "' . $sheetname . '" only'); $reader->setLoadSheetsOnly($sheetname); diff --git a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php index 6a600d43..82ca2234 100644 --- a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php +++ b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php @@ -40,7 +40,7 @@ class MyReadFilter implements IReadFilter $filterSubset = new MyReadFilter(9, 15, range('G', 'K')); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet "' . $sheetname . '" only'); $reader->setLoadSheetsOnly($sheetname); diff --git a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php index 6c908703..cf070054 100644 --- a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php +++ b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php @@ -40,7 +40,7 @@ class ChunkReadFilter implements IReadFilter } } -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); // Create a new Reader of the type defined in $inputFileType $reader = IOFactory::createReader($inputFileType); diff --git a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php index c594c798..13692e3a 100644 --- a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php +++ b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php @@ -40,7 +40,7 @@ class ChunkReadFilter implements IReadFilter } } -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); // Create a new Reader of the type defined in $inputFileType $reader = IOFactory::createReader($inputFileType); diff --git a/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php b/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php index d4817e30..9cba25a2 100644 --- a/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php +++ b/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php @@ -1,22 +1,21 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using Csv Reader'); $spreadsheet = $reader->load($inputFileName); -$spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); +$spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); foreach ($inputFileNames as $sheet => $inputFileName) { - $helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #' . ($sheet + 2) . ' using IOFactory with a defined reader type of ' . $inputFileType); + $helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #' . ($sheet + 2) . ' using Csv Reader'); $reader->setSheetIndex($sheet + 1); $reader->loadIntoExisting($inputFileName, $spreadsheet); - $spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); + $spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); } $helper->log($spreadsheet->getSheetCount() . ' worksheet' . (($spreadsheet->getSheetCount() == 1) ? '' : 's') . ' loaded'); diff --git a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php index 87fbb225..eab7ec01 100644 --- a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php +++ b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php @@ -2,13 +2,12 @@ namespace Samples\Sample14; -use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Reader\Csv; use PhpOffice\PhpSpreadsheet\Reader\IReadFilter; use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../Header.php'; -$inputFileType = 'Csv'; $inputFileName = __DIR__ . '/sampleData/example2.csv'; /** Define a Read Filter class implementing IReadFilter */ @@ -41,9 +40,9 @@ class ChunkReadFilter implements IReadFilter } } -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using Csv reader'); // Create a new Reader of the type defined in $inputFileType -$reader = IOFactory::createReader($inputFileType); +$reader = new Csv(); // Define how many rows we want to read for each "chunk" $chunkSize = 100; diff --git a/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php b/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php index 8213678a..848452c7 100644 --- a/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php +++ b/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php @@ -2,20 +2,19 @@ use PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder; use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Reader\Csv; require __DIR__ . '/../Header.php'; Cell::setValueBinder(new AdvancedValueBinder()); -$inputFileType = 'Csv'; $inputFileName = __DIR__ . '/sampleData/example1.tsv'; -$reader = IOFactory::createReader($inputFileType); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); +$reader = new Csv(); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using Csv reader'); $reader->setDelimiter("\t"); $spreadsheet = $reader->load($inputFileName); -$spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); +$spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); $helper->log($spreadsheet->getSheetCount() . ' worksheet' . (($spreadsheet->getSheetCount() == 1) ? '' : 's') . ' loaded'); $loadedSheetNames = $spreadsheet->getSheetNames(); diff --git a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php index 5b102967..603f6cb8 100644 --- a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php +++ b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php @@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/non-existing-file.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); try { $spreadsheet = IOFactory::load($inputFileName); diff --git a/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php b/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php index db30bff8..12c3c113 100644 --- a/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php +++ b/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php @@ -1,14 +1,13 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); -$reader = IOFactory::createReader($inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using Xls reader'); +$reader = new Xls(); // Read the list of Worksheet Names from the Workbook file $helper->log('Read the list of Worksheets in the WorkBook'); diff --git a/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php b/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php index bb58a2d5..6d2c3db9 100644 --- a/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php +++ b/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php @@ -1,15 +1,14 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' information using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' information using Xls reader'); -$reader = IOFactory::createReader($inputFileType); +$reader = new Xls(); $worksheetNames = $reader->listWorksheetNames($inputFileName); $helper->log('

Worksheet Names

'); diff --git a/samples/Reader/19_Reading_worksheet_information_without_loading_entire_file.php b/samples/Reader/19_Reading_worksheet_information_without_loading_entire_file.php index 5cdc4988..369fff9a 100644 --- a/samples/Reader/19_Reading_worksheet_information_without_loading_entire_file.php +++ b/samples/Reader/19_Reading_worksheet_information_without_loading_entire_file.php @@ -1,15 +1,15 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' information using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' information using Xls reader'); -$reader = IOFactory::createReader($inputFileType); +$reader = new Xls(); $worksheetData = $reader->listWorksheetInfo($inputFileName); $helper->log('

Worksheet Information

'); diff --git a/samples/Reader/20_Reader_worksheet_hyperlink_image.php b/samples/Reader/20_Reader_worksheet_hyperlink_image.php index 19d837a5..2b3f294a 100644 --- a/samples/Reader/20_Reader_worksheet_hyperlink_image.php +++ b/samples/Reader/20_Reader_worksheet_hyperlink_image.php @@ -13,6 +13,9 @@ $spreadsheet = new Spreadsheet(); $aSheet = $spreadsheet->getActiveSheet(); $gdImage = @imagecreatetruecolor(120, 20); +if ($gdImage === false) { + throw new \Exception('imagecreatetruecolor failed'); +} $textColor = imagecolorallocate($gdImage, 255, 255, 255); imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); diff --git a/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php b/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php index 2c80de3b..ec37b39a 100644 --- a/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php +++ b/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php @@ -12,10 +12,10 @@ $inputFileType = 'Csv'; $inputFileName = __DIR__ . '/sampleData/longIntegers.csv'; $reader = IOFactory::createReader($inputFileType); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); $spreadsheet = $reader->load($inputFileName); -$spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); +$spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); $helper->log($spreadsheet->getSheetCount() . ' worksheet' . (($spreadsheet->getSheetCount() == 1) ? '' : 's') . ' loaded'); $loadedSheetNames = $spreadsheet->getSheetNames(); diff --git a/samples/templates/chartSpreadsheet.php b/samples/templates/chartSpreadsheet.php index 2ad61d32..a5b1b882 100644 --- a/samples/templates/chartSpreadsheet.php +++ b/samples/templates/chartSpreadsheet.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -71,7 +71,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_BAR); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Bar Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 36adef01..b336920c 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1040,7 +1040,7 @@ class Calculation ], 'FILTER' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Filter::class, 'filter'], 'argumentCount' => '2-3', ], 'FILTERXML' => [ @@ -2282,12 +2282,12 @@ class Calculation ], 'SORT' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '1+', + 'functionCall' => [LookupRef\Sort::class, 'sort'], + 'argumentCount' => '1-4', ], 'SORTBY' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Sort::class, 'sortBy'], 'argumentCount' => '2+', ], 'SQRT' => [ @@ -2583,7 +2583,7 @@ class Calculation ], 'UNIQUE' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Unique::class, 'unique'], 'argumentCount' => '1+', ], 'UPPER' => [ diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dabe7d1c..cffad6a6 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); + return in_array($value, ExcelError::$errorCodes) || $value === ExcelError::CALC(); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 98242eb6..585dfdc8 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -22,6 +22,7 @@ class ExcelError 'num' => '#NUM!', 'na' => '#N/A', 'gettingdata' => '#GETTING_DATA', + 'spill' => '#SPILL!', ]; /** @@ -45,6 +46,10 @@ class ExcelError ++$i; } + if ($value === self::CALC()) { + return 14; + } + return self::NA(); } @@ -127,10 +132,20 @@ class ExcelError /** * DIV0. * - * @return string #Not Yet Implemented + * @return string #DIV/0! */ public static function DIV0() { return self::$errorCodes['divisionbyzero']; } + + /** + * CALC. + * + * @return string #Not Yet Implemented + */ + public static function CALC() + { + return '#CALC!'; + } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php new file mode 100644 index 00000000..74fa8321 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -0,0 +1,81 @@ +getMessage(); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index d0c13a5c..417a1f79 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -72,11 +72,17 @@ class Indirect [$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); if ( - (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) || - (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches))) + (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || + (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress2, $matches))) ) { return ExcelError::REF(); } @@ -95,4 +101,22 @@ class Indirect return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null) ->extractCellRange($cellAddress, $worksheet, false); } + + private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string + { + // Being lazy, we're only checking a single row/column to get the max + if (ctype_digit($start) && $start <= 1048576) { + // Max 16,384 columns for Excel2007 + $endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : 'XFD'; + + return "A{$start}:{$endColRef}{$end}"; + } elseif (ctype_alpha($start) && strlen($start) <= 3) { + // Max 1,048,576 rows for Excel2007 + $endRowRef = ($worksheet !== null) ? $worksheet->getHighestDataRow($start) : 1048576; + + return "{$start}1:{$end}{$endRowRef}"; + } + + return "{$start}:{$end}"; + } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 6a0933d6..8e451fe4 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -7,6 +7,16 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; abstract class LookupBase { + /** + * @param mixed $lookup_array + */ + protected static function validateLookupArray($lookup_array): void + { + if (!is_array($lookup_array)) { + throw new Exception(ExcelError::REF()); + } + } + protected static function validateIndexLookup(array $lookup_array, $index_number): int { // index_number must be a number greater than or equal to 1 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index 27e6f8eb..a447e203 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -10,6 +10,23 @@ class Matrix { use ArrayEnabled; + /** + * Helper function; NOT an implementation of any Excel Function. + */ + public static function isColumnVector(array $values): bool + { + return count($values, COUNT_RECURSIVE) === (count($values, COUNT_NORMAL) * 2); + } + + /** + * Helper function; NOT an implementation of any Excel Function. + */ + public static function isRowVector(array $values): bool + { + return count($values, COUNT_RECURSIVE) > 1 && + (count($values, COUNT_NORMAL) === 1 || count($values, COUNT_RECURSIVE) === count($values, COUNT_NORMAL)); + } + /** * TRANSPOSE. * diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php b/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php new file mode 100644 index 00000000..ff78fbea --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php @@ -0,0 +1,342 @@ +getMessage(); + } + + // We want a simple, enumrated array of arrays where we can reference column by its index number. + $sortArray = array_values(array_map('array_values', $sortArray)); + + return ($byColumn === true) + ? self::sortByColumn($sortArray, $sortIndex, $sortOrder) + : self::sortByRow($sortArray, $sortIndex, $sortOrder); + } + + /** + * SORTBY + * The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array. + * The returned array is the same shape as the provided array argument. + * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting. + * + * @param mixed $sortArray The range of cells being sorted + * @param mixed $args + * At least one additional argument must be provided, The vector or range to sort on + * After that, arguments are passed as pairs: + * sort order: ascending or descending + * Ascending = 1 (self::ORDER_ASCENDING) + * Descending = -1 (self::ORDER_DESCENDING) + * additional arrays or ranges for multi-level sorting + * + * @return mixed The sorted values from the sort range + */ + public static function sortBy($sortArray, ...$args) + { + if (!is_array($sortArray)) { + // Scalars are always returned "as is" + return $sortArray; + } + + $sortArray = self::enumerateArrayKeys($sortArray); + + $lookupArraySize = count($sortArray); + $argumentCount = count($args); + + try { + $sortBy = $sortOrder = []; + for ($i = 0; $i < $argumentCount; $i += 2) { + $sortBy[] = self::validateSortVector($args[$i], $lookupArraySize); + $sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::processSortBy($sortArray, $sortBy, $sortOrder); + } + + private static function enumerateArrayKeys(array $sortArray): array + { + array_walk( + $sortArray, + function (&$columns): void { + if (is_array($columns)) { + $columns = array_values($columns); + } + } + ); + + return array_values($sortArray); + } + + /** + * @param mixed $sortIndex + * @param mixed $sortOrder + */ + private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void + { + if (is_array($sortIndex) || is_array($sortOrder)) { + throw new Exception(ExcelError::VALUE()); + } + + $sortIndex = self::validatePositiveInt($sortIndex, false); + + if ($sortIndex > $sortArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + $sortOrder = self::validateSortOrder($sortOrder); + } + + /** + * @param mixed $sortVector + */ + private static function validateSortVector($sortVector, int $sortArraySize): array + { + if (!is_array($sortVector)) { + throw new Exception(ExcelError::VALUE()); + } + + // It doesn't matter if it's a row or a column vectors, it works either way + $sortVector = Functions::flattenArray($sortVector); + if (count($sortVector) !== $sortArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortVector; + } + + /** + * @param mixed $sortOrder + */ + private static function validateSortOrder($sortOrder): int + { + $sortOrder = self::validateInt($sortOrder); + if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortOrder; + } + + /** + * @param array $sortIndex + * @param mixed $sortOrder + */ + private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void + { + // It doesn't matter if they're row or column vectors, it works either way + $sortIndex = Functions::flattenArray($sortIndex); + $sortOrder = Functions::flattenArray($sortOrder); + + if ( + count($sortOrder) === 0 || count($sortOrder) > $sortArraySize || + (count($sortOrder) > count($sortIndex)) + ) { + throw new Exception(ExcelError::VALUE()); + } + + if (count($sortIndex) > count($sortOrder)) { + // If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated. + $sortOrder = array_merge( + $sortOrder, + array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder)) + ); + } + + foreach ($sortIndex as $key => &$value) { + self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize); + } + } + + private static function prepareSortVectorValues(array $sortVector): array + { + // Strings should be sorted case-insensitive; with booleans converted to locale-strings + return array_map( + function ($value) { + if (is_bool($value)) { + return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + } elseif (is_string($value)) { + return StringHelper::strToLower($value); + } + + return $value; + }, + $sortVector + ); + } + + /** + * @param array[] $sortIndex + * @param int[] $sortOrder + */ + private static function processSortBy(array $sortArray, array $sortIndex, $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortValues) { + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments); + + $sortVector = self::executeVectorSortQuery($sortData, $sortArguments); + + return self::sortLookupArrayFromVector($sortArray, $sortVector); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder); + + return self::sortLookupArrayFromVector($sortArray, $sortVector); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArray = Matrix::transpose($sortArray); + $result = self::sortByRow($sortArray, $sortIndex, $sortOrder); + + return Matrix::transpose($result); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortIndexValue) { + $sortValues = array_column($sortArray, $sortIndexValue - 1); + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments); + + $sortData = self::executeVectorSortQuery($sortData, $sortArguments); + + return $sortData; + } + + private static function executeVectorSortQuery(array $sortData, array $sortArguments): array + { + $sortData = Matrix::transpose($sortData); + + // We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys. + $sortDataIndexed = []; + foreach ($sortData as $key => $value) { + $sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value; + } + unset($sortData); + + $sortArguments[] = &$sortDataIndexed; + + array_multisort(...$sortArguments); + + // After the sort, we restore the numeric keys that will now be in the correct, sorted order + $sortedData = []; + foreach (array_keys($sortDataIndexed) as $key) { + $sortedData[] = Coordinate::columnIndexFromString($key) - 1; + } + + return $sortedData; + } + + private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array + { + // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays + $sortedArray = []; + foreach ($sortVector as $index) { + $sortedArray[] = $sortArray[$index]; + } + + return $sortedArray; + +// uksort( +// $lookupArray, +// function (int $a, int $b) use (array $sortVector) { +// return $sortVector[$a] <=> $sortVector[$b]; +// } +// ); +// +// return $lookupArray; + } + + /** + * Hack to handle PHP 7: + * From PHP 8.0.0, If two members compare as equal in a sort, they retain their original order; + * but prior to PHP 8.0.0, their relative order in the sorted array was undefined. + * MS Excel replicates the PHP 8.0.0 behaviour, retaining the original order of matching elements. + * To replicate that behaviour with PHP 7, we add an extra sort based on the row index. + */ + private static function applyPHP7Patch(array $sortArray, array $sortArguments): array + { + if (PHP_VERSION_ID < 80000) { + $sortArguments[] = range(1, count($sortArray)); + $sortArguments[] = SORT_ASC; + } + + return $sortArguments; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php b/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php new file mode 100644 index 00000000..2ba51281 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php @@ -0,0 +1,141 @@ + count($flattenedLookupVector, COUNT_RECURSIVE) + 1) { + // We're looking at a full column check (multiple rows) + $transpose = Matrix::transpose($lookupVector); + $result = self::uniqueByRow($transpose, $exactlyOnce); + + return (is_array($result)) ? Matrix::transpose($result) : $result; + } + + $result = self::countValuesCaseInsensitive($flattenedLookupVector); + + if ($exactlyOnce === true) { + $result = self::exactlyOnceFilter($result); + } + + if (count($result) === 0) { + return ExcelError::CALC(); + } + + $result = array_keys($result); + + return $result; + } + + private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array + { + $caseInsensitiveCounts = array_count_values( + array_map( + function (string $value) { + return StringHelper::strToUpper($value); + }, + $caseSensitiveLookupValues + ) + ); + + $caseSensitiveCounts = []; + foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) { + if (is_numeric($caseInsensitiveKey)) { + $caseSensitiveCounts[$caseInsensitiveKey] = $count; + } else { + foreach ($caseSensitiveLookupValues as $caseSensitiveValue) { + if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) { + $caseSensitiveCounts[$caseSensitiveValue] = $count; + + break; + } + } + } + } + + return $caseSensitiveCounts; + } + + private static function exactlyOnceFilter(array $values): array + { + return array_filter( + $values, + function ($value) { + return $value === 1; + } + ); + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 26f42eba..bc8624f3 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -33,6 +33,7 @@ class VLookup extends LookupBase $notExactMatch = (bool) ($notExactMatch ?? true); try { + self::validateLookupArray($lookupArray); $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); } catch (Exception $e) { return $e->getMessage(); diff --git a/src/PhpSpreadsheet/CellReferenceHelper.php b/src/PhpSpreadsheet/CellReferenceHelper.php new file mode 100644 index 00000000..0f079d69 --- /dev/null +++ b/src/PhpSpreadsheet/CellReferenceHelper.php @@ -0,0 +1,119 @@ +beforeCellAddress = str_replace('$', '', $beforeCellAddress); + $this->numberOfColumns = $numberOfColumns; + $this->numberOfRows = $numberOfRows; + + // Get coordinate of $beforeCellAddress + [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); + $this->beforeColumn = (int) Coordinate::columnIndexFromString($beforeColumn); + $this->beforeRow = (int) $beforeRow; + } + + public function beforeCellAddress(): string + { + return $this->beforeCellAddress; + } + + public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool + { + return $this->beforeCellAddress !== $beforeCellAddress || + $this->numberOfColumns !== $numberOfColumns || + $this->numberOfRows !== $numberOfRows; + } + + public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false): string + { + if (Coordinate::coordinateIsRange($cellReference)) { + throw new Exception('Only single cell references may be passed to this method.'); + } + + // Get coordinate of $cellReference + [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); + $newColumnIndex = (int) Coordinate::columnIndexFromString(str_replace('$', '', $newColumn)); + $newRowIndex = (int) str_replace('$', '', $newRow); + + $absoluteColumn = $newColumn[0] === '$' ? '$' : ''; + $absoluteRow = $newRow[0] === '$' ? '$' : ''; + // Verify which parts should be updated + if ($includeAbsoluteReferences === false) { + $updateColumn = (($absoluteColumn !== '$') && $newColumnIndex >= $this->beforeColumn); + $updateRow = (($absoluteRow !== '$') && $newRowIndex >= $this->beforeRow); + } else { + $updateColumn = ($newColumnIndex >= $this->beforeColumn); + $updateRow = ($newRowIndex >= $this->beforeRow); + } + + // Create new column reference + if ($updateColumn) { + $newColumn = ($includeAbsoluteReferences === false) + ? Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns) + : $absoluteColumn . Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns); + } + + // Create new row reference + if ($updateRow) { + $newRow = ($includeAbsoluteReferences === false) + ? $newRowIndex + $this->numberOfRows + : $absoluteRow . (string) ($newRowIndex + $this->numberOfRows); + } + + // Return new reference + return "{$newColumn}{$newRow}"; + } + + public function cellAddressInDeleteRange(string $cellAddress): bool + { + [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); + $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); + // Is cell within the range of rows/columns if we're deleting + if ( + $this->numberOfRows < 0 && + ($cellRow >= ($this->beforeRow + $this->numberOfRows)) && + ($cellRow < $this->beforeRow) + ) { + return true; + } elseif ( + $this->numberOfColumns < 0 && + ($cellColumnIndex >= ($this->beforeColumn + $this->numberOfColumns)) && + ($cellColumnIndex < $this->beforeColumn) + ) { + return true; + } + + return false; + } +} diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index 26526605..4737379a 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -619,6 +619,7 @@ class Html // Load the HTML file into the DOM object // Note the use of error suppression, because typically this will be an html fragment, so not fully valid markup $prefix = ''; + /** @scrutinizer ignore-unhandled */ @$dom->loadHTML($prefix . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); // Discard excess white space $dom->preserveWhiteSpace = false; @@ -808,7 +809,7 @@ class Html if (isset($callbacks[$callbackTag])) { $elementHandler = $callbacks[$callbackTag]; if (method_exists($this, $elementHandler)) { - // @phpstan-ignore-next-line + /** @phpstan-ignore-next-line */ call_user_func([$this, $elementHandler], $element); } } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 0fd05c87..402fea9f 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -7,7 +7,9 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\NamedRange; +use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; +use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\CodePage; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -21,6 +23,8 @@ use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Borders; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; @@ -142,6 +146,8 @@ class Xls extends BaseReader const XLS_TYPE_SHEETLAYOUT = 0x0862; const XLS_TYPE_XFEXT = 0x087d; const XLS_TYPE_PAGELAYOUTVIEW = 0x088b; + const XLS_TYPE_CFHEADER = 0x01b0; + const XLS_TYPE_CFRULE = 0x01b1; const XLS_TYPE_UNKNOWN = 0xffff; // Encryption type @@ -1031,6 +1037,14 @@ class Xls extends BaseReader case self::XLS_TYPE_DATAVALIDATION: $this->readDataValidation(); + break; + case self::XLS_TYPE_CFHEADER: + $cellRangeAddresses = $this->readCFHeader(); + + break; + case self::XLS_TYPE_CFRULE: + $this->readCFRule($cellRangeAddresses ?? []); + break; case self::XLS_TYPE_SHEETLAYOUT: $this->readSheetLayout(); @@ -4776,57 +4790,11 @@ class Xls extends BaseReader // bit: 0-3; mask: 0x0000000F; type $type = (0x0000000F & $options) >> 0; - switch ($type) { - case 0x00: - $type = DataValidation::TYPE_NONE; - - break; - case 0x01: - $type = DataValidation::TYPE_WHOLE; - - break; - case 0x02: - $type = DataValidation::TYPE_DECIMAL; - - break; - case 0x03: - $type = DataValidation::TYPE_LIST; - - break; - case 0x04: - $type = DataValidation::TYPE_DATE; - - break; - case 0x05: - $type = DataValidation::TYPE_TIME; - - break; - case 0x06: - $type = DataValidation::TYPE_TEXTLENGTH; - - break; - case 0x07: - $type = DataValidation::TYPE_CUSTOM; - - break; - } + $type = Xls\DataValidationHelper::type($type); // bit: 4-6; mask: 0x00000070; error type $errorStyle = (0x00000070 & $options) >> 4; - switch ($errorStyle) { - case 0x00: - $errorStyle = DataValidation::STYLE_STOP; - - break; - case 0x01: - $errorStyle = DataValidation::STYLE_WARNING; - - break; - case 0x02: - $errorStyle = DataValidation::STYLE_INFORMATION; - - break; - } + $errorStyle = Xls\DataValidationHelper::errorStyle($errorStyle); // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list) // I have only seen cases where this is 1 @@ -4846,39 +4814,10 @@ class Xls extends BaseReader // bit: 20-23; mask: 0x00F00000; condition operator $operator = (0x00F00000 & $options) >> 20; - switch ($operator) { - case 0x00: - $operator = DataValidation::OPERATOR_BETWEEN; + $operator = Xls\DataValidationHelper::operator($operator); - break; - case 0x01: - $operator = DataValidation::OPERATOR_NOTBETWEEN; - - break; - case 0x02: - $operator = DataValidation::OPERATOR_EQUAL; - - break; - case 0x03: - $operator = DataValidation::OPERATOR_NOTEQUAL; - - break; - case 0x04: - $operator = DataValidation::OPERATOR_GREATERTHAN; - - break; - case 0x05: - $operator = DataValidation::OPERATOR_LESSTHAN; - - break; - case 0x06: - $operator = DataValidation::OPERATOR_GREATERTHANOREQUAL; - - break; - case 0x07: - $operator = DataValidation::OPERATOR_LESSTHANOREQUAL; - - break; + if ($type === null || $errorStyle === null || $operator === null) { + return; } // offset: 4; size: var; title of the prompt box @@ -7921,4 +7860,230 @@ class Xls extends BaseReader { return $this->mapCellStyleXfIndex; } + + private function readCFHeader(): array + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer forward to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return []; + } + + // offset: 0; size: 2; Rule Count +// $ruleCount = self::getUInt2d($recordData, 0); + + // offset: var; size: var; cell range address list with + $cellRangeAddressList = ($this->version == self::XLS_BIFF8) + ? $this->readBIFF8CellRangeAddressList(substr($recordData, 12)) + : $this->readBIFF5CellRangeAddressList(substr($recordData, 12)); + $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; + + return $cellRangeAddresses; + } + + private function readCFRule(array $cellRangeAddresses): void + { + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer forward to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; Options + $cfRule = self::getUInt2d($recordData, 0); + + // bit: 8-15; mask: 0x00FF; type + $type = (0x00FF & $cfRule) >> 0; + $type = ConditionalFormatting::type($type); + + // bit: 0-7; mask: 0xFF00; type + $operator = (0xFF00 & $cfRule) >> 8; + $operator = ConditionalFormatting::operator($operator); + + if ($type === null || $operator === null) { + return; + } + + // offset: 2; size: 2; Size1 + $size1 = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; Size2 + $size2 = self::getUInt2d($recordData, 4); + + // offset: 6; size: 4; Options + $options = self::getInt4d($recordData, 6); + + $style = new Style(); + $this->getCFStyleOptions($options, $style); + + $hasFontRecord = (bool) ((0x04000000 & $options) >> 26); + $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27); + $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28); + $hasFillRecord = (bool) ((0x20000000 & $options) >> 29); + $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30); + + $offset = 12; + + if ($hasFontRecord === true) { + $fontStyle = substr($recordData, $offset, 118); + $this->getCFFontStyle($fontStyle, $style); + $offset += 118; + } + + if ($hasAlignmentRecord === true) { + $alignmentStyle = substr($recordData, $offset, 8); + $this->getCFAlignmentStyle($alignmentStyle, $style); + $offset += 8; + } + + if ($hasBorderRecord === true) { + $borderStyle = substr($recordData, $offset, 8); + $this->getCFBorderStyle($borderStyle, $style); + $offset += 8; + } + + if ($hasFillRecord === true) { + $fillStyle = substr($recordData, $offset, 4); + $this->getCFFillStyle($fillStyle, $style); + $offset += 4; + } + + if ($hasProtectionRecord === true) { + $protectionStyle = substr($recordData, $offset, 4); + $this->getCFProtectionStyle($protectionStyle, $style); + $offset += 2; + } + + $formula1 = $formula2 = null; + if ($size1 > 0) { + $formula1 = $this->readCFFormula($recordData, $offset, $size1); + if ($formula1 === null) { + return; + } + + $offset += $size1; + } + + if ($size2 > 0) { + $formula2 = $this->readCFFormula($recordData, $offset, $size2); + if ($formula2 === null) { + return; + } + + $offset += $size2; + } + + $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style); + } + + private function getCFStyleOptions(int $options, Style $style): void + { + } + + private function getCFFontStyle(string $options, Style $style): void + { + $fontSize = self::getInt4d($options, 64); + if ($fontSize !== -1) { + $style->getFont()->setSize($fontSize / 20); // Convert twips to points + } + + $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold + $style->getFont()->setBold($bold); + + $color = self::getInt4d($options, 80); + + if ($color !== -1) { + $style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']); + } + } + + private function getCFAlignmentStyle(string $options, Style $style): void + { + } + + private function getCFBorderStyle(string $options, Style $style): void + { + } + + private function getCFFillStyle(string $options, Style $style): void + { + $fillPattern = self::getUInt2d($options, 0); + // bit: 10-15; mask: 0xFC00; type + $fillPattern = (0xFC00 & $fillPattern) >> 10; + $fillPattern = FillPattern::lookup($fillPattern); + $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern; + + if ($fillPattern !== Fill::FILL_NONE) { + $style->getFill()->setFillType($fillPattern); + + $fillColors = self::getUInt2d($options, 2); + + // bit: 0-6; mask: 0x007F; type + $color1 = (0x007F & $fillColors) >> 0; + $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color1, $this->palette, $this->version)['rgb']); + + // bit: 7-13; mask: 0x3F80; type + $color2 = (0x3F80 & $fillColors) >> 7; + $style->getFill()->getEndColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']); + } + } + + private function getCFProtectionStyle(string $options, Style $style): void + { + } + + /** + * @return null|float|int|string + */ + private function readCFFormula(string $recordData, int $offset, int $size) + { + try { + $formula = substr($recordData, $offset, $size); + $formula = pack('v', $size) . $formula; // prepend the length + + $formula = $this->getFormulaFromStructure($formula); + if (is_numeric($formula)) { + return (strpos($formula, '.') !== false) ? (float) $formula : (int) $formula; + } + + return $formula; + } catch (PhpSpreadsheetException $e) { + } + + return null; + } + + /** + * @param null|float|int|string $formula1 + * @param null|float|int|string $formula2 + */ + private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2, Style $style): void + { + foreach ($cellRanges as $cellRange) { + $conditional = new Conditional(); + $conditional->setConditionType($type); + $conditional->setOperatorType($operator); + if ($formula1 !== null) { + $conditional->addCondition($formula1); + } + if ($formula2 !== null) { + $conditional->addCondition($formula2); + } + $conditional->setStyle($style); + + $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles(); + $conditionalStyles[] = $conditional; + + $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + } + } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php new file mode 100644 index 00000000..8400efb9 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php @@ -0,0 +1,49 @@ + + */ + private static $types = [ + 0x01 => Conditional::CONDITION_CELLIS, + 0x02 => Conditional::CONDITION_EXPRESSION, + ]; + + /** + * @var array + */ + private static $operators = [ + 0x00 => Conditional::OPERATOR_NONE, + 0x01 => Conditional::OPERATOR_BETWEEN, + 0x02 => Conditional::OPERATOR_NOTBETWEEN, + 0x03 => Conditional::OPERATOR_EQUAL, + 0x04 => Conditional::OPERATOR_NOTEQUAL, + 0x05 => Conditional::OPERATOR_GREATERTHAN, + 0x06 => Conditional::OPERATOR_LESSTHAN, + 0x07 => Conditional::OPERATOR_GREATERTHANOREQUAL, + 0x08 => Conditional::OPERATOR_LESSTHANOREQUAL, + ]; + + public static function type(int $type): ?string + { + if (isset(self::$types[$type])) { + return self::$types[$type]; + } + + return null; + } + + public static function operator(int $operator): ?string + { + if (isset(self::$operators[$operator])) { + return self::$operators[$operator]; + } + + return null; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php new file mode 100644 index 00000000..02f844e3 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php @@ -0,0 +1,72 @@ + + */ + private static $types = [ + 0x00 => DataValidation::TYPE_NONE, + 0x01 => DataValidation::TYPE_WHOLE, + 0x02 => DataValidation::TYPE_DECIMAL, + 0x03 => DataValidation::TYPE_LIST, + 0x04 => DataValidation::TYPE_DATE, + 0x05 => DataValidation::TYPE_TIME, + 0x06 => DataValidation::TYPE_TEXTLENGTH, + 0x07 => DataValidation::TYPE_CUSTOM, + ]; + + /** + * @var array + */ + private static $errorStyles = [ + 0x00 => DataValidation::STYLE_STOP, + 0x01 => DataValidation::STYLE_WARNING, + 0x02 => DataValidation::STYLE_INFORMATION, + ]; + + /** + * @var array + */ + private static $operators = [ + 0x00 => DataValidation::OPERATOR_BETWEEN, + 0x01 => DataValidation::OPERATOR_NOTBETWEEN, + 0x02 => DataValidation::OPERATOR_EQUAL, + 0x03 => DataValidation::OPERATOR_NOTEQUAL, + 0x04 => DataValidation::OPERATOR_GREATERTHAN, + 0x05 => DataValidation::OPERATOR_LESSTHAN, + 0x06 => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 0x07 => DataValidation::OPERATOR_LESSTHANOREQUAL, + ]; + + public static function type(int $type): ?string + { + if (isset(self::$types[$type])) { + return self::$types[$type]; + } + + return null; + } + + public static function errorStyle(int $errorStyle): ?string + { + if (isset(self::$errorStyles[$errorStyle])) { + return self::$errorStyles[$errorStyle]; + } + + return null; + } + + public static function operator(int $operator): ?string + { + if (isset(self::$operators[$operator])) { + return self::$operators[$operator]; + } + + return null; + } +} diff --git a/src/PhpSpreadsheet/Reader/Xls/MD5.php b/src/PhpSpreadsheet/Reader/Xls/MD5.php index a1108f92..14b6bc54 100644 --- a/src/PhpSpreadsheet/Reader/Xls/MD5.php +++ b/src/PhpSpreadsheet/Reader/Xls/MD5.php @@ -71,7 +71,7 @@ class MD5 */ public function add(string $data): void { - /** @phpstan-ignore-next-line */ + // @phpstan-ignore-next-line $words = array_values(unpack('V16', $data)); $A = $this->a; diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index c0b0c5ee..992002bb 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1315,6 +1315,11 @@ class Xlsx extends BaseReader $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(); + if (isset($editAs, $editAs['editAs'])) { + $objDrawing->setEditAs($editAs['editAs']); + } $objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); $embedImageKey = (string) self::getArrayItem( diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 98c4807a..665b2e18 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -5,6 +5,8 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class ReferenceHelper @@ -19,10 +21,15 @@ class ReferenceHelper /** * Instance of this class. * - * @var ReferenceHelper + * @var ?ReferenceHelper */ private static $instance; + /** + * @var CellReferenceHelper + */ + private $cellReferenceHelper; + /** * Get an instance of this class. * @@ -30,7 +37,7 @@ class ReferenceHelper */ public static function getInstance() { - if (!isset(self::$instance) || (self::$instance === null)) { + if (self::$instance === null) { self::$instance = new self(); } @@ -114,67 +121,32 @@ class ReferenceHelper return ($ar < $br) ? 1 : -1; } - /** - * Test whether a cell address falls within a defined range of cells. - * - * @param string $cellAddress Address of the cell we're testing - * @param int $beforeRow Number of the row we're inserting/deleting before - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before - * @param int $numberOfCols Number of columns to insert/delete (negative values indicate deletion) - * - * @return bool - */ - private static function cellAddressInDeleteRange($cellAddress, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfCols) - { - [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); - $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); - // Is cell within the range of rows/columns if we're deleting - if ( - $numberOfRows < 0 && - ($cellRow >= ($beforeRow + $numberOfRows)) && - ($cellRow < $beforeRow) - ) { - return true; - } elseif ( - $numberOfCols < 0 && - ($cellColumnIndex >= ($beforeColumnIndex + $numberOfCols)) && - ($cellColumnIndex < $beforeColumnIndex) - ) { - return true; - } - - return false; - } - /** * Update page breaks when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $beforeRow Number of the row we're inserting/deleting before * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustPageBreaks(Worksheet $worksheet, $beforeCellAddress, $beforeColumnIndex, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustPageBreaks(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aBreaks = $worksheet->getBreaks(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aBreaks, ['self', 'cellReverseSort']) : uksort($aBreaks, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aBreaks, [self::class, 'cellReverseSort']) + : uksort($aBreaks, [self::class, 'cellSort']); - foreach ($aBreaks as $key => $value) { - if (self::cellAddressInDeleteRange($key, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfColumns)) { + foreach ($aBreaks as $cellAddress => $value) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { // If we're deleting, then clear any defined breaks that are within the range // of rows/columns that we're deleting - $worksheet->setBreak($key, Worksheet::BREAK_NONE); + $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE); } else { // Otherwise update any affected breaks by inserting a new break at the appropriate point // and removing the old affected break - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->setBreak($newReference, $value) - ->setBreak($key, Worksheet::BREAK_NONE); + ->setBreak($cellAddress, Worksheet::BREAK_NONE); } } } @@ -184,22 +156,17 @@ class ReferenceHelper * Update cell comments when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $beforeRow Number of the row we're inserting/deleting before - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustComments($worksheet, $beforeCellAddress, $beforeColumnIndex, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustComments($worksheet): void { $aComments = $worksheet->getComments(); $aNewComments = []; // the new array of all comments - foreach ($aComments as $key => &$value) { + foreach ($aComments as $cellAddress => &$value) { // Any comments inside a deleted range will be ignored - if (!self::cellAddressInDeleteRange($key, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfColumns)) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) { // Otherwise build a new array of comments indexed by the adjusted cell reference - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($cellAddress); $aNewComments[$newReference] = $value; } } @@ -211,44 +178,85 @@ class ReferenceHelper * Update hyperlinks when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustHyperlinks($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows): void { $aHyperlinkCollection = $worksheet->getHyperlinkCollection(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aHyperlinkCollection, ['self', 'cellReverseSort']) : uksort($aHyperlinkCollection, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort']) + : uksort($aHyperlinkCollection, [self::class, 'cellSort']); - foreach ($aHyperlinkCollection as $key => $value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + foreach ($aHyperlinkCollection as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { + $worksheet->setHyperlink($cellAddress, null); + } elseif ($cellAddress !== $newReference) { $worksheet->setHyperlink($newReference, $value); - $worksheet->setHyperlink($key, null); + $worksheet->setHyperlink($cellAddress, null); } } } + /** + * Update conditional formatting styles when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows): void + { + $aStyles = $worksheet->getConditionalStylesCollection(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aStyles, [self::class, 'cellReverseSort']) + : uksort($aStyles, [self::class, 'cellSort']); + + foreach ($aStyles as $cellAddress => $cfRules) { + $worksheet->removeConditionalStyles($cellAddress); + $newReference = $this->updateCellReference($cellAddress); + + foreach ($cfRules as &$cfRule) { + /** @var Conditional $cfRule */ + $conditions = $cfRule->getConditions(); + foreach ($conditions as &$condition) { + if (is_string($condition)) { + $condition = $this->updateFormulaReferences( + $condition, + $this->cellReferenceHelper->beforeCellAddress(), + $numberOfColumns, + $numberOfRows, + $worksheet->getTitle(), + true + ); + } + } + $cfRule->setConditions($conditions); + } + $worksheet->setConditionalStyles($newReference, $cfRules); + } + } + /** * Update data validations when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $before Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustDataValidations(Worksheet $worksheet, $before, $numberOfColumns, $numberOfRows): void + protected function adjustDataValidations(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aDataValidationCollection = $worksheet->getDataValidationCollection(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aDataValidationCollection, ['self', 'cellReverseSort']) : uksort($aDataValidationCollection, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) + : uksort($aDataValidationCollection, [self::class, 'cellSort']); - foreach ($aDataValidationCollection as $key => $value) { - $newReference = $this->updateCellReference($key, $before, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + foreach ($aDataValidationCollection as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->setDataValidation($newReference, $value); - $worksheet->setDataValidation($key, null); + $worksheet->setDataValidation($cellAddress, null); } } } @@ -257,16 +265,13 @@ class ReferenceHelper * Update merged cells when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustMergeCells(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustMergeCells(Worksheet $worksheet): void { $aMergeCells = $worksheet->getMergeCells(); $aNewMergeCells = []; // the new array of all merge cells - foreach ($aMergeCells as $key => &$value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); + foreach ($aMergeCells as $cellAddress => &$value) { + $newReference = $this->updateCellReference($cellAddress); $aNewMergeCells[$newReference] = $newReference; } $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array @@ -276,20 +281,20 @@ class ReferenceHelper * Update protected cells when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustProtectedCells(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustProtectedCells(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aProtectedCells = $worksheet->getProtectedCells(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aProtectedCells, ['self', 'cellReverseSort']) : uksort($aProtectedCells, ['self', 'cellSort']); - foreach ($aProtectedCells as $key => $value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aProtectedCells, [self::class, 'cellReverseSort']) + : uksort($aProtectedCells, [self::class, 'cellSort']); + foreach ($aProtectedCells as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->protectCells($newReference, $value, true); - $worksheet->unprotectCells($key); + $worksheet->unprotectCells($cellAddress); } } } @@ -298,18 +303,15 @@ class ReferenceHelper * Update column dimensions when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustColumnDimensions(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustColumnDimensions(Worksheet $worksheet): void { $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true); if (!empty($aColumnDimensions)) { foreach ($aColumnDimensions as $objColumnDimension) { - $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1'); [$newReference] = Coordinate::coordinateFromString($newReference); - if ($objColumnDimension->getColumnIndex() != $newReference) { + if ($objColumnDimension->getColumnIndex() !== $newReference) { $objColumnDimension->setColumnIndex($newReference); } } @@ -321,20 +323,19 @@ class ReferenceHelper * Update row dimensions when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $beforeRow Number of the row we're inserting/deleting before * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustRowDimensions(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustRowDimensions(Worksheet $worksheet, $beforeRow, $numberOfRows): void { $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true); if (!empty($aRowDimensions)) { foreach ($aRowDimensions as $objRowDimension) { - $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex()); [, $newReference] = Coordinate::coordinateFromString($newReference); - if ($objRowDimension->getRowIndex() != $newReference) { - $objRowDimension->setRowIndex($newReference); + $newRoweference = (int) $newReference; + if ($objRowDimension->getRowIndex() !== $newRoweference) { + $objRowDimension->setRowIndex($newRoweference); } } $worksheet->refreshRowDimensions(); @@ -358,11 +359,22 @@ class ReferenceHelper * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) * @param Worksheet $worksheet The worksheet that we're editing */ - public function insertNewBefore($beforeCellAddress, $numberOfColumns, $numberOfRows, Worksheet $worksheet): void - { + public function insertNewBefore( + string $beforeCellAddress, + int $numberOfColumns, + int $numberOfRows, + Worksheet $worksheet + ): void { $remove = ($numberOfColumns < 0 || $numberOfRows < 0); $allCoordinates = $worksheet->getCoordinates(); + if ( + $this->cellReferenceHelper === null || + $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + // Get coordinate of $beforeCellAddress [$beforeColumn, $beforeRow] = Coordinate::indexesFromString($beforeCellAddress); @@ -372,30 +384,12 @@ class ReferenceHelper // 1. Clear column strips if we are removing columns if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) { - 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('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } - } - } + $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet); } // 2. Clear row strips if we are removing rows if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) { - for ($i = $beforeColumn - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$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('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } - } - } + $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet); } // Find missing coordinates. This is important when inserting column before the last column @@ -440,7 +434,7 @@ class ReferenceHelper $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); // Insert this cell at its new location - if ($cell->getDataType() == DataType::TYPE_FORMULA) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { // Formula should be adjusted $worksheet->getCell($newCoordinate) ->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle())); @@ -454,7 +448,7 @@ class ReferenceHelper } else { /* We don't need to update styles for rows/columns before our insertion position, but we do still need to adjust any formulae in those cells */ - if ($cell->getDataType() == DataType::TYPE_FORMULA) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { // Formula should be adjusted $cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle())); } @@ -466,148 +460,65 @@ class ReferenceHelper $highestRow = $worksheet->getHighestRow(); if ($numberOfColumns > 0 && $beforeColumn - 2 > 0) { - for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { - // Style - $coordinate = Coordinate::stringFromColumnIndex($beforeColumn - 1) . $i; - if ($worksheet->cellExists($coordinate)) { - $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { - $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned); - } - } - } - } + $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns); } if ($numberOfRows > 0 && $beforeRow - 1 > 0) { - for ($i = $beforeColumn; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { - // Style - $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); - if ($worksheet->cellExists($coordinate)) { - $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { - $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned); - } - } - } - } + $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows); } // Update worksheet: column dimensions - $this->adjustColumnDimensions($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustColumnDimensions($worksheet); // Update worksheet: row dimensions - $this->adjustRowDimensions($worksheet, $beforeCellAddress, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows); // Update worksheet: page breaks - $this->adjustPageBreaks($worksheet, $beforeCellAddress, $beforeColumn, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: comments - $this->adjustComments($worksheet, $beforeCellAddress, $beforeColumn, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustComments($worksheet); // Update worksheet: hyperlinks - $this->adjustHyperlinks($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows); + + // Update worksheet: conditional formatting styles + $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: data validations - $this->adjustDataValidations($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: merge cells - $this->adjustMergeCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustMergeCells($worksheet); // Update worksheet: protected cells - $this->adjustProtectedCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: autofilter - $autoFilter = $worksheet->getAutoFilter(); - $autoFilterRange = $autoFilter->getRange(); - if (!empty($autoFilterRange)) { - if ($numberOfColumns != 0) { - $autoFilterColumns = $autoFilter->getColumns(); - if (count($autoFilterColumns) > 0) { - $column = ''; - $row = 0; - sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); - $columnIndex = Coordinate::columnIndexFromString($column); - [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); - if ($columnIndex <= $rangeEnd[0]) { - if ($numberOfColumns < 0) { - // If we're actually deleting any columns that fall within the autofilter range, - // then we delete any rules for those columns - $deleteColumn = $columnIndex + $numberOfColumns - 1; - $deleteCount = abs($numberOfColumns); - for ($i = 1; $i <= $deleteCount; ++$i) { - if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { - $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); - } - ++$deleteColumn; - } - } - $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; - - // Shuffle columns in autofilter range - if ($numberOfColumns > 0) { - $startColRef = $startCol; - $endColRef = $rangeEnd[0]; - $toColRef = $rangeEnd[0] + $numberOfColumns; - - do { - $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); - --$endColRef; - --$toColRef; - } while ($startColRef <= $endColRef); - } else { - // For delete, we shuffle from beginning to end to avoid overwriting - $startColID = Coordinate::stringFromColumnIndex($startCol); - $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); - $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); - do { - $autoFilter->shiftColumn($startColID, $toColID); - ++$startColID; - ++$toColID; - } while ($startColID != $endColID); - } - } - } - } - $worksheet->setAutoFilter($this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows)); - } + $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns); // Update worksheet: freeze pane if ($worksheet->getFreezePane()) { $splitCell = $worksheet->getFreezePane() ?? ''; $topLeftCell = $worksheet->getTopLeftCell() ?? ''; - $splitCell = $this->updateCellReference($splitCell, $beforeCellAddress, $numberOfColumns, $numberOfRows); - $topLeftCell = $this->updateCellReference($topLeftCell, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $splitCell = $this->updateCellReference($splitCell); + $topLeftCell = $this->updateCellReference($topLeftCell); $worksheet->freezePane($splitCell, $topLeftCell); } // Page setup if ($worksheet->getPageSetup()->isPrintAreaSet()) { - $worksheet->getPageSetup()->setPrintArea($this->updateCellReference($worksheet->getPageSetup()->getPrintArea(), $beforeCellAddress, $numberOfColumns, $numberOfRows)); + $worksheet->getPageSetup()->setPrintArea( + $this->updateCellReference($worksheet->getPageSetup()->getPrintArea()) + ); } // Update worksheet: drawings $aDrawings = $worksheet->getDrawingCollection(); foreach ($aDrawings as $objDrawing) { - $newReference = $this->updateCellReference($objDrawing->getCoordinates(), $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($objDrawing->getCoordinates()); if ($objDrawing->getCoordinates() != $newReference) { $objDrawing->setCoordinates($newReference); } @@ -617,7 +528,7 @@ class ReferenceHelper 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(), $beforeCellAddress, $numberOfColumns, $numberOfRows)); + $definedName->setValue($this->updateCellReference($definedName->getValue())); } } } @@ -637,8 +548,21 @@ class ReferenceHelper * * @return string Updated formula */ - public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0, $worksheetName = '') - { + public function updateFormulaReferences( + $formula = '', + $beforeCellAddress = 'A1', + $numberOfColumns = 0, + $numberOfRows = 0, + $worksheetName = '', + bool $includeAbsoluteReferences = false + ) { + if ( + $this->cellReferenceHelper === null || + $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + // Update cell references in the formula $formulaBlocks = explode('"', $formula); $i = false; @@ -648,13 +572,13 @@ class ReferenceHelper $adjustCount = 0; $newCellTokens = $cellTokens = []; // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference('$A' . $match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows), 2); - $modified4 = substr($this->updateCellReference('$A' . $match[4], $beforeCellAddress, $numberOfColumns, $numberOfRows), 2); + $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2); + $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -673,13 +597,13 @@ class ReferenceHelper } } // Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference($match[3] . '$1', $beforeCellAddress, $numberOfColumns, $numberOfRows), 0, -2); - $modified4 = substr($this->updateCellReference($match[4] . '$1', $beforeCellAddress, $numberOfColumns, $numberOfRows), 0, -2); + $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2); + $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -698,13 +622,13 @@ class ReferenceHelper } } // Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = $this->updateCellReference($match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows); - $modified4 = $this->updateCellReference($match[4], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); + $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences); if ($match[3] . $match[4] !== $modified3 . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -724,14 +648,14 @@ class ReferenceHelper } } // Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3]; - $modified3 = $this->updateCellReference($match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); if ($match[3] !== $modified3) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { $toString = ($match[2] > '') ? $match[2] . '!' : ''; @@ -908,13 +832,10 @@ class ReferenceHelper * Update cell reference. * * @param string $cellReference Cell address or range of addresses - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment * * @return string Updated cell range */ - public function updateCellReference($cellReference = 'A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false) { // Is it in another worksheet? Will not have to update anything. if (strpos($cellReference, '!') !== false) { @@ -922,10 +843,10 @@ class ReferenceHelper // Is it a range or a single cell? } elseif (!Coordinate::coordinateIsRange($cellReference)) { // Single cell - return $this->updateSingleCellReference($cellReference, $beforeCellAddress, $numberOfColumns, $numberOfRows); + return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences); } elseif (Coordinate::coordinateIsRange($cellReference)) { // Range - return $this->updateCellRange($cellReference, $beforeCellAddress, $numberOfColumns, $numberOfRows); + return $this->updateCellRange($cellReference, $includeAbsoluteReferences); } // Return original @@ -948,7 +869,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 !== null) && ($cell->getDataType() === DataType::TYPE_FORMULA)) { $formula = $cell->getValue(); if (strpos($formula, $oldName) !== false) { $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); @@ -964,13 +885,10 @@ class ReferenceHelper * Update cell range. * * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment * * @return string Updated cell range */ - private function updateCellRange($cellRange = 'A1:A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false): string { if (!Coordinate::coordinateIsRange($cellRange)) { throw new Exception('Only cell ranges may be passed to this method.'); @@ -983,13 +901,15 @@ class ReferenceHelper $jc = count($range[$i]); for ($j = 0; $j < $jc; ++$j) { if (ctype_alpha($range[$i][$j])) { - $r = Coordinate::coordinateFromString($this->updateSingleCellReference($range[$i][$j] . '1', $beforeCellAddress, $numberOfColumns, $numberOfRows)); - $range[$i][$j] = $r[0]; + $range[$i][$j] = Coordinate::coordinateFromString( + $this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences) + )[0]; } elseif (ctype_digit($range[$i][$j])) { - $r = Coordinate::coordinateFromString($this->updateSingleCellReference('A' . $range[$i][$j], $beforeCellAddress, $numberOfColumns, $numberOfRows)); - $range[$i][$j] = $r[1]; + $range[$i][$j] = Coordinate::coordinateFromString( + $this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences) + )[1]; } else { - $range[$i][$j] = $this->updateSingleCellReference($range[$i][$j], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences); } } } @@ -998,44 +918,142 @@ class ReferenceHelper return Coordinate::buildRange($range); } - /** - * Update single cell reference. - * - * @param string $cellReference Single cell reference - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment - * - * @return string Updated cell reference - */ - private function updateSingleCellReference($cellReference = 'A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void { - if (Coordinate::coordinateIsRange($cellReference)) { - throw new Exception('Only single cell references may be passed to this method.'); + 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('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } } + } - // Get coordinate of $beforeCellAddress - [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); + private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void + { + $lastColumnIndex = Coordinate::columnIndexFromString($highestColumn) - 1; - // Get coordinate of $cellReference - [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); - - // Verify which parts should be updated - $updateColumn = (($newColumn[0] != '$') && ($beforeColumn[0] != '$') && (Coordinate::columnIndexFromString($newColumn) >= Coordinate::columnIndexFromString($beforeColumn))); - $updateRow = (($newRow[0] != '$') && ($beforeRow[0] != '$') && $newRow >= $beforeRow); - - // Create new column reference - if ($updateColumn) { - $newColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($newColumn) + $numberOfColumns); + 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('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } } + } - // Create new row reference - if ($updateRow) { - $newRow = (int) $newRow + $numberOfRows; + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void + { + $autoFilter = $worksheet->getAutoFilter(); + $autoFilterRange = $autoFilter->getRange(); + if (!empty($autoFilterRange)) { + if ($numberOfColumns !== 0) { + $autoFilterColumns = $autoFilter->getColumns(); + if (count($autoFilterColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString($column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter); + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in autofilter range + if ($numberOfColumns > 0) { + $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); + } else { + $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); + } + } + } + } + + $worksheet->setAutoFilter( + $this->updateCellReference($autoFilterRange) + ); } + } - // Return new reference - return $newColumn . $newRow; + private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void + { + // If we're actually deleting any columns that fall within the autofilter range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($autoFilterColumns[$columnName])) { + $autoFilter->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $autoFilter->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID !== $endColID); + } + + private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void + { + $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); + for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { + // Style + $coordinate = $beforeColumnName . $i; + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { + $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); + } + } + } + } + + private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void + { + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); + for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { + // Style + $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { + $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + } + } + } } /** diff --git a/src/PhpSpreadsheet/Shared/XMLWriter.php b/src/PhpSpreadsheet/Shared/XMLWriter.php index 84ad8a83..3dc0aad9 100644 --- a/src/PhpSpreadsheet/Shared/XMLWriter.php +++ b/src/PhpSpreadsheet/Shared/XMLWriter.php @@ -54,7 +54,9 @@ class XMLWriter extends \XMLWriter public function __destruct() { // Unlink temporary files + // There is nothing reasonable to do if unlink fails. if ($this->tempFileName != '') { + /** @scrutinizer ignore-unhandled */ @unlink($this->tempFileName); } } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 2b8c8360..52d7fb55 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -1602,4 +1602,14 @@ class Spreadsheet } } } + + /** + * Silliness to mollify Scrutinizer. + * + * @codeCoverageIgnore + */ + public function getSharedComponent(): Style + { + return new Style(); + } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php index 75d6856e..df9daab3 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php @@ -122,7 +122,7 @@ abstract class WizardAbstract return "{$worksheet}{$column}{$row}"; } - protected static function reverseAdjustCellRef(string $condition, string $cellRange): string + public static function reverseAdjustCellRef(string $condition, string $cellRange): string { $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellRange))); [$referenceCell] = $conditionalRange[0]; diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index bd90b2bc..815536b5 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -8,6 +8,22 @@ use PhpOffice\PhpSpreadsheet\IComparable; class BaseDrawing implements IComparable { + const EDIT_AS_ABSOLUTE = 'absolute'; + const EDIT_AS_ONECELL = 'onecell'; + const EDIT_AS_TWOCELL = 'twocell'; + private const VALID_EDIT_AS = [ + self::EDIT_AS_ABSOLUTE, + self::EDIT_AS_ONECELL, + self::EDIT_AS_TWOCELL, + ]; + + /** + * The editAs attribute, used only with two cell anchor. + * + * @var string + */ + protected $editAs = ''; + /** * Image counter. * @@ -27,14 +43,14 @@ class BaseDrawing implements IComparable * * @var string */ - protected $name; + protected $name = ''; /** * Description. * * @var string */ - protected $description; + protected $description = ''; /** * Worksheet. @@ -48,70 +64,84 @@ class BaseDrawing implements IComparable * * @var string */ - protected $coordinates; + protected $coordinates = 'A1'; /** * Offset X. * * @var int */ - protected $offsetX; + protected $offsetX = 0; /** * Offset Y. * * @var int */ - protected $offsetY; + protected $offsetY = 0; /** * Coordinates2. * - * @var null|string + * @var string */ - protected $coordinates2; + protected $coordinates2 = ''; /** * Offset X2. * * @var int */ - protected $offsetX2; + protected $offsetX2 = 0; /** * Offset Y2. * * @var int */ - protected $offsetY2; + protected $offsetY2 = 0; /** * Width. * * @var int */ - protected $width; + protected $width = 0; /** * Height. * * @var int */ - protected $height; + protected $height = 0; + + /** + * Pixel width of image. See $width for the size the Drawing will be in the sheet. + * + * @var int + */ + protected $imageWidth = 0; + + /** + * Pixel width of image. See $height for the size the Drawing will be in the sheet. + * + * @var int + */ + protected $imageHeight = 0; /** * Proportional resize. * * @var bool */ - protected $resizeProportional; + protected $resizeProportional = true; /** * Rotation. * * @var int */ - protected $rotation; + protected $rotation = 0; /** * Shadow. @@ -132,7 +162,7 @@ class BaseDrawing implements IComparable * * @var int */ - protected $type; + protected $type = IMAGETYPE_UNKNOWN; /** * Create a new BaseDrawing. @@ -140,91 +170,43 @@ class BaseDrawing implements IComparable public function __construct() { // Initialise values - $this->name = ''; - $this->description = ''; - $this->worksheet = null; - $this->coordinates = 'A1'; - $this->offsetX = 0; - $this->offsetY = 0; - $this->coordinates2 = null; - $this->offsetX2 = 0; - $this->offsetY2 = 0; - $this->width = 0; - $this->height = 0; - $this->resizeProportional = true; - $this->rotation = 0; - $this->shadow = new Drawing\Shadow(); - $this->type = IMAGETYPE_UNKNOWN; + $this->setShadow(); // Set image index ++self::$imageCounter; $this->imageIndex = self::$imageCounter; } - /** - * Get image index. - * - * @return int - */ - public function getImageIndex() + public function getImageIndex(): int { return $this->imageIndex; } - /** - * Get Name. - * - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * Set Name. - * - * @param string $name - * - * @return $this - */ - public function setName($name) + public function setName(string $name): self { $this->name = $name; return $this; } - /** - * Get Description. - * - * @return string - */ - public function getDescription() + public function getDescription(): string { return $this->description; } - /** - * Set Description. - * - * @param string $description - * - * @return $this - */ - public function setDescription($description) + public function setDescription(string $description): self { $this->description = $description; return $this; } - /** - * Get Worksheet. - * - * @return null|Worksheet - */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->worksheet; } @@ -233,16 +215,16 @@ class BaseDrawing implements IComparable * Set Worksheet. * * @param bool $overrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet? - * - * @return $this */ - public function setWorksheet(?Worksheet $worksheet = null, $overrideOld = false) + public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = false): self { if ($this->worksheet === null) { // Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet - $this->worksheet = $worksheet; - $this->worksheet->getCell($this->coordinates); - $this->worksheet->getDrawingCollection()->append($this); + if ($worksheet !== null) { + $this->worksheet = $worksheet; + $this->worksheet->getCell($this->coordinates); + $this->worksheet->getDrawingCollection()->append($this); + } } else { if ($overrideOld) { // Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet @@ -267,168 +249,84 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get Coordinates. - * - * @return string - */ - public function getCoordinates() + public function getCoordinates(): string { return $this->coordinates; } - /** - * Set Coordinates. - * - * @param string $coordinates eg: 'A1' - * - * @return $this - */ - public function setCoordinates($coordinates) + public function setCoordinates(string $coordinates): self { $this->coordinates = $coordinates; return $this; } - /** - * Get OffsetX. - * - * @return int - */ - public function getOffsetX() + public function getOffsetX(): int { return $this->offsetX; } - /** - * Set OffsetX. - * - * @param int $offsetX - * - * @return $this - */ - public function setOffsetX($offsetX) + public function setOffsetX(int $offsetX): self { $this->offsetX = $offsetX; return $this; } - /** - * Get OffsetY. - * - * @return int - */ - public function getOffsetY() + public function getOffsetY(): int { return $this->offsetY; } - /** - * Get Coordinates2. - * - * @return null|string - */ - public function getCoordinates2() - { - return $this->coordinates2; - } - - /** - * Set Coordinates2. - * - * @param null|string $coordinates2 eg: 'A1' - * - * @return $this - */ - public function setCoordinates2($coordinates2) - { - $this->coordinates2 = $coordinates2; - - return $this; - } - - /** - * Get OffsetX2. - * - * @return int - */ - public function getOffsetX2() - { - return $this->offsetX2; - } - - /** - * Set OffsetX2. - * - * @param int $offsetX2 - * - * @return $this - */ - public function setOffsetX2($offsetX2) - { - $this->offsetX2 = $offsetX2; - - return $this; - } - - /** - * Get OffsetY2. - * - * @return int - */ - public function getOffsetY2() - { - return $this->offsetY2; - } - - /** - * Set OffsetY2. - * - * @param int $offsetY2 - * - * @return $this - */ - public function setOffsetY2($offsetY2) - { - $this->offsetY2 = $offsetY2; - - return $this; - } - - /** - * Set OffsetY. - * - * @param int $offsetY - * - * @return $this - */ - public function setOffsetY($offsetY) + public function setOffsetY(int $offsetY): self { $this->offsetY = $offsetY; return $this; } - /** - * Get Width. - * - * @return int - */ - public function getWidth() + public function getCoordinates2(): string + { + return $this->coordinates2; + } + + public function setCoordinates2(string $coordinates2): self + { + $this->coordinates2 = $coordinates2; + + return $this; + } + + public function getOffsetX2(): int + { + return $this->offsetX2; + } + + public function setOffsetX2(int $offsetX2): self + { + $this->offsetX2 = $offsetX2; + + return $this; + } + + public function getOffsetY2(): int + { + return $this->offsetY2; + } + + public function setOffsetY2(int $offsetY2): self + { + $this->offsetY2 = $offsetY2; + + return $this; + } + + public function getWidth(): int { return $this->width; } - /** - * Set Width. - * - * @param int $width - * - * @return $this - */ - public function setWidth($width) + public function setWidth(int $width): self { // Resize proportional? if ($this->resizeProportional && $width != 0) { @@ -442,24 +340,12 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get Height. - * - * @return int - */ - public function getHeight() + public function getHeight(): int { return $this->height; } - /** - * Set Height. - * - * @param int $height - * - * @return $this - */ - public function setHeight($height) + public function setHeight(int $height): self { // Resize proportional? if ($this->resizeProportional && $height != 0) { @@ -482,14 +368,9 @@ class BaseDrawing implements IComparable * $objDrawing->setWidthAndHeight(160,120); * * - * @param int $width - * @param int $height - * - * @return $this - * * @author Vincent@luo MSN:kele_100@hotmail.com */ - public function setWidthAndHeight($width, $height) + public function setWidthAndHeight(int $width, int $height): self { $xratio = $width / ($this->width != 0 ? $this->width : 1); $yratio = $height / ($this->height != 0 ? $this->height : 1); @@ -509,72 +390,38 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get ResizeProportional. - * - * @return bool - */ - public function getResizeProportional() + public function getResizeProportional(): bool { return $this->resizeProportional; } - /** - * Set ResizeProportional. - * - * @param bool $resizeProportional - * - * @return $this - */ - public function setResizeProportional($resizeProportional) + public function setResizeProportional(bool $resizeProportional): self { $this->resizeProportional = $resizeProportional; return $this; } - /** - * Get Rotation. - * - * @return int - */ - public function getRotation() + public function getRotation(): int { return $this->rotation; } - /** - * Set Rotation. - * - * @param int $rotation - * - * @return $this - */ - public function setRotation($rotation) + public function setRotation(int $rotation): self { $this->rotation = $rotation; return $this; } - /** - * Get Shadow. - * - * @return Drawing\Shadow - */ - public function getShadow() + public function getShadow(): Drawing\Shadow { return $this->shadow; } - /** - * Set Shadow. - * - * @return $this - */ - public function setShadow(?Drawing\Shadow $shadow = null) + public function setShadow(?Drawing\Shadow $shadow = null): self { - $this->shadow = $shadow; + $this->shadow = $shadow ?? new Drawing\Shadow(); return $this; } @@ -589,7 +436,7 @@ class BaseDrawing implements IComparable return md5( $this->name . $this->description . - $this->worksheet->getHashCode() . + (($this->worksheet === null) ? '' : $this->worksheet->getHashCode()) . $this->coordinates . $this->offsetX . $this->offsetY . @@ -626,10 +473,7 @@ class BaseDrawing implements IComparable $this->hyperlink = $hyperlink; } - /** - * @return null|Hyperlink - */ - public function getHyperlink() + public function getHyperlink(): ?Hyperlink { return $this->hyperlink; } @@ -639,15 +483,19 @@ class BaseDrawing implements IComparable */ protected function setSizesAndType(string $path): void { - if ($this->width == 0 && $this->height == 0 && $this->type == IMAGETYPE_UNKNOWN) { + if ($this->imageWidth === 0 && $this->imageHeight === 0 && $this->type === IMAGETYPE_UNKNOWN) { $imageData = getimagesize($path); if (is_array($imageData)) { - $this->width = $imageData[0]; - $this->height = $imageData[1]; + $this->imageWidth = $imageData[0]; + $this->imageHeight = $imageData[1]; $this->type = $imageData[2]; } } + if ($this->width === 0 && $this->height === 0) { + $this->width = $this->imageWidth; + $this->height = $this->imageHeight; + } } /** @@ -657,4 +505,31 @@ class BaseDrawing implements IComparable { return $this->type; } + + public function getImageWidth(): int + { + return $this->imageWidth; + } + + public function getImageHeight(): int + { + return $this->imageHeight; + } + + public function getEditAs(): string + { + return $this->editAs; + } + + public function setEditAs(string $editAs): self + { + $this->editAs = $editAs; + + return $this; + } + + public function validEditAs(): bool + { + return in_array($this->editAs, self::VALID_EDIT_AS); + } } diff --git a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index 91acbb7b..e65541dc 100644 --- a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -47,6 +47,9 @@ class MemoryDrawing extends BaseDrawing */ private $uniqueName; + /** @var null|resource */ + private $alwaysNull; + /** * Create a new MemoryDrawing. */ @@ -56,6 +59,7 @@ class MemoryDrawing extends BaseDrawing $this->renderingFunction = self::RENDERING_DEFAULT; $this->mimeType = self::MIMETYPE_DEFAULT; $this->uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999)); + $this->alwaysNull = null; // Initialize parent parent::__construct(); @@ -64,8 +68,9 @@ class MemoryDrawing extends BaseDrawing public function __destruct() { if ($this->imageResource) { - imagedestroy($this->imageResource); - $this->imageResource = null; + $rslt = @imagedestroy($this->imageResource); + // "Fix" for Scrutinizer + $this->imageResource = $rslt ? null : $this->alwaysNull; } } diff --git a/src/PhpSpreadsheet/Worksheet/Row.php b/src/PhpSpreadsheet/Worksheet/Row.php index b5933356..a5f8f326 100644 --- a/src/PhpSpreadsheet/Worksheet/Row.php +++ b/src/PhpSpreadsheet/Worksheet/Row.php @@ -35,8 +35,7 @@ class Row */ public function __destruct() { - // @phpstan-ignore-next-line - $this->worksheet = null; + $this->worksheet = null; // @phpstan-ignore-line } /** diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d83f512a..08423356 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2279,16 +2279,18 @@ class Worksheet implements IComparable $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); $pColumnIndex = Coordinate::columnIndexFromString($column); - if ($pColumnIndex > $highestColumnIndex) { - return $this; - } - $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns); $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns); $objReferenceHelper = ReferenceHelper::getInstance(); $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this); + $this->columnDimensions = $holdColumnDimensions; + + if ($pColumnIndex > $highestColumnIndex) { + return $this; + } + $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1; for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) { @@ -2296,8 +2298,6 @@ class Worksheet implements IComparable $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1); } - $this->columnDimensions = $holdColumnDimensions; - $this->garbageCollect(); return $this; diff --git a/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php new file mode 100644 index 00000000..0f78b8c5 --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php @@ -0,0 +1,76 @@ +parser = $parser; + } + + /** + * @param mixed $condition + */ + public function processCondition($condition, string $cellRange): void + { + $this->condition = $condition; + $this->cellRange = $cellRange; + + if (is_int($condition) || is_float($condition)) { + $this->size = ($condition <= 65535 ? 3 : 0x0000); + $this->tokens = pack('Cv', 0x1E, $condition); + } else { + try { + $formula = Wizard\WizardAbstract::reverseAdjustCellRef((string) $condition, $cellRange); + $this->parser->parse($formula); + $this->tokens = $this->parser->toReversePolish(); + $this->size = strlen($this->tokens ?? ''); + } catch (PhpSpreadsheetException $e) { + // In the event of a parser error with a formula value, we set the expression to ptgInt + 0 + $this->tokens = pack('Cv', 0x1E, 0); + $this->size = 3; + } + } + } + + public function tokens(): ?string + { + return $this->tokens; + } + + public function size(): int + { + return $this->size; + } +} diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index fcf7789a..16059e28 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -539,37 +539,44 @@ class Worksheet extends BIFFwriter $this->writeSheetProtection(); $this->writeRangeProtection(); - $arrConditionalStyles = $phpSheet->getConditionalStylesCollection(); + // Write Conditional Formatting Rules and Styles + $this->writeConditionalFormatting(); + + $this->storeEof(); + } + + private function writeConditionalFormatting(): void + { + $conditionalFormulaHelper = new ConditionalHelper($this->parser); + + $arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection(); if (!empty($arrConditionalStyles)) { $arrConditional = []; - $cfHeaderWritten = false; // Write ConditionalFormattingTable records foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) { + $cfHeaderWritten = false; foreach ($conditionalStyles as $conditional) { /** @var Conditional $conditional */ if ( - $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION || - $conditional->getConditionType() == Conditional::CONDITION_CELLIS + $conditional->getConditionType() === Conditional::CONDITION_EXPRESSION || + $conditional->getConditionType() === Conditional::CONDITION_CELLIS ) { // Write CFHEADER record (only if there are Conditional Styles that we are able to write) if ($cfHeaderWritten === false) { - $this->writeCFHeader(); - $cfHeaderWritten = true; + $cfHeaderWritten = $this->writeCFHeader($cellCoordinate, $conditionalStyles); } - if (!isset($arrConditional[$conditional->getHashCode()])) { + if ($cfHeaderWritten === true && !isset($arrConditional[$conditional->getHashCode()])) { // This hash code has been handled $arrConditional[$conditional->getHashCode()] = true; // Write CFRULE record - $this->writeCFRule($conditional); + $this->writeCFRule($conditionalFormulaHelper, $conditional, $cellCoordinate); } } } } } - - $this->storeEof(); } /** @@ -2774,11 +2781,14 @@ class Worksheet extends BIFFwriter /** * Write CFRule Record. */ - private function writeCFRule(Conditional $conditional): void - { + private function writeCFRule( + ConditionalHelper $conditionalFormulaHelper, + Conditional $conditional, + string $cellRange + ): void { $record = 0x01B1; // Record identifier - $type = null; // Type of the CF - $operatorType = null; // Comparison operator + $type = null; // Type of the CF + $operatorType = null; // Comparison operator if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { $type = 0x02; @@ -2827,21 +2837,23 @@ class Worksheet extends BIFFwriter // $szValue2 : size of the formula data for second value or formula $arrConditions = $conditional->getConditions(); $numConditions = count($arrConditions); - if ($numConditions == 1) { - $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); - $szValue2 = 0x0000; - $operand1 = pack('Cv', 0x1E, $arrConditions[0]); - $operand2 = null; - } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) { - $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); - $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000); - $operand1 = pack('Cv', 0x1E, $arrConditions[0]); - $operand2 = pack('Cv', 0x1E, $arrConditions[1]); - } else { - $szValue1 = 0x0000; - $szValue2 = 0x0000; - $operand1 = null; - $operand2 = null; + + $szValue1 = 0x0000; + $szValue2 = 0x0000; + $operand1 = null; + $operand2 = null; + + if ($numConditions === 1) { + $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange); + $szValue1 = $conditionalFormulaHelper->size(); + $operand1 = $conditionalFormulaHelper->tokens(); + } elseif ($numConditions === 2 && ($conditional->getOperatorType() === Conditional::OPERATOR_BETWEEN)) { + $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange); + $szValue1 = $conditionalFormulaHelper->size(); + $operand1 = $conditionalFormulaHelper->tokens(); + $conditionalFormulaHelper->processCondition($arrConditions[1], $cellRange); + $szValue2 = $conditionalFormulaHelper->size(); + $operand2 = $conditionalFormulaHelper->tokens(); } // $flags : Option flags @@ -3127,8 +3139,10 @@ class Worksheet extends BIFFwriter /** * Write CFHeader record. + * + * @param Conditional[] $conditionalStyles */ - private function writeCFHeader(): void + private function writeCFHeader(string $cellCoordinate, array $conditionalStyles): bool { $record = 0x01B0; // Record identifier $length = 0x0016; // Bytes to follow @@ -3137,33 +3151,32 @@ class Worksheet extends BIFFwriter $numColumnMax = null; $numRowMin = null; $numRowMax = null; + $arrConditional = []; - foreach ($this->phpSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { - foreach ($conditionalStyles as $conditional) { - if ( - $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION || - $conditional->getConditionType() == Conditional::CONDITION_CELLIS - ) { - if (!in_array($conditional->getHashCode(), $arrConditional)) { - $arrConditional[] = $conditional->getHashCode(); - } - // Cells - $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate); - if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) { - $numColumnMin = $rangeCoordinates[0][0]; - } - if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) { - $numColumnMax = $rangeCoordinates[1][0]; - } - if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) { - $numRowMin = (int) $rangeCoordinates[0][1]; - } - if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) { - $numRowMax = (int) $rangeCoordinates[1][1]; - } - } + foreach ($conditionalStyles as $conditional) { + if (!in_array($conditional->getHashCode(), $arrConditional)) { + $arrConditional[] = $conditional->getHashCode(); + } + // Cells + $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate); + if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) { + $numColumnMin = $rangeCoordinates[0][0]; + } + if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) { + $numColumnMax = $rangeCoordinates[1][0]; + } + if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) { + $numRowMin = (int) $rangeCoordinates[0][1]; + } + if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) { + $numRowMax = (int) $rangeCoordinates[1][1]; } } + + if (count($arrConditional) === 0) { + return false; + } + $needRedraw = 1; $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1); @@ -3173,6 +3186,8 @@ class Worksheet extends BIFFwriter $data .= pack('v', 0x0001); $data .= $cellRange; $this->append($header . $data); + + return true; } private function getDataBlockProtection(Conditional $conditional): int diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 6868212a..816bb9d4 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; @@ -56,7 +57,10 @@ class Drawing extends WriterPart // Loop through charts and write the chart position if ($chartCount > 0) { for ($c = 0; $c < $chartCount; ++$c) { - $this->writeChart($objWriter, $worksheet->getChartByIndex($c), $c + $i); + $chart = $worksheet->getChartByIndex((string) $c); + if ($chart !== false) { + $this->writeChart($objWriter, $chart, $c + $i); + } } } } @@ -90,16 +94,16 @@ class Drawing extends WriterPart $objWriter->startElement('xdr:twoCellAnchor'); $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $tlColRow[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset'])); - $objWriter->writeElement('xdr:row', $tlColRow[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset'])); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); $objWriter->endElement(); $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', $brColRow[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset'])); - $objWriter->writeElement('xdr:row', $brColRow[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset'])); + $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); $objWriter->endElement(); $objWriter->startElement('xdr:graphicFrame'); @@ -107,7 +111,7 @@ class Drawing extends WriterPart $objWriter->startElement('xdr:nvGraphicFramePr'); $objWriter->startElement('xdr:cNvPr'); $objWriter->writeAttribute('name', 'Chart ' . $relationId); - $objWriter->writeAttribute('id', 1025 * $relationId); + $objWriter->writeAttribute('id', (string) (1025 * $relationId)); $objWriter->endElement(); $objWriter->startElement('xdr:cNvGraphicFramePr'); $objWriter->startElement('a:graphicFrameLocks'); @@ -153,28 +157,31 @@ class Drawing extends WriterPart public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relationId = -1, $hlinkClickId = null): void { if ($relationId >= 0) { - $isTwoCellAnchor = $drawing->getCoordinates2() !== null; + $isTwoCellAnchor = $drawing->getCoordinates2() !== ''; if ($isTwoCellAnchor) { // xdr:twoCellAnchor $objWriter->startElement('xdr:twoCellAnchor'); + if ($drawing->validEditAs()) { + $objWriter->writeAttribute('editAs', $drawing->getEditAs()); + } // Image location $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates()); $aCoordinates2 = Coordinate::indexesFromString($drawing->getCoordinates2()); // xdr:from $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX())); - $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY())); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY())); $objWriter->endElement(); // xdr:to $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', $aCoordinates2[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX2())); - $objWriter->writeElement('xdr:row', $aCoordinates2[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY2())); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates2[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX2())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates2[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY2())); $objWriter->endElement(); } else { // xdr:oneCellAnchor @@ -184,16 +191,16 @@ class Drawing extends WriterPart // xdr:from $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX())); - $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY())); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY())); $objWriter->endElement(); // xdr:ext $objWriter->startElement('xdr:ext'); - $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth())); - $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight())); + $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); + $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight())); $objWriter->endElement(); } @@ -205,7 +212,7 @@ class Drawing extends WriterPart // xdr:cNvPr $objWriter->startElement('xdr:cNvPr'); - $objWriter->writeAttribute('id', $relationId); + $objWriter->writeAttribute('id', (string) $relationId); $objWriter->writeAttribute('name', $drawing->getName()); $objWriter->writeAttribute('descr', $drawing->getDescription()); @@ -247,11 +254,11 @@ class Drawing extends WriterPart // a:xfrm $objWriter->startElement('a:xfrm'); - $objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getRotation())); + $objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation())); if ($isTwoCellAnchor) { $objWriter->startElement('a:ext'); - $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth())); - $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight())); + $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); + $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight())); $objWriter->endElement(); } $objWriter->endElement(); @@ -271,9 +278,9 @@ class Drawing extends WriterPart // a:outerShdw $objWriter->startElement('a:outerShdw'); - $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getBlurRadius())); - $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getDistance())); - $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getShadow()->getDirection())); + $objWriter->writeAttribute('blurRad', self::stringEmu($drawing->getShadow()->getBlurRadius())); + $objWriter->writeAttribute('dist', self::stringEmu($drawing->getShadow()->getDistance())); + $objWriter->writeAttribute('dir', (string) SharedDrawing::degreesToAngle($drawing->getShadow()->getDirection())); $objWriter->writeAttribute('algn', $drawing->getShadow()->getAlignment()); $objWriter->writeAttribute('rotWithShape', '0'); @@ -283,7 +290,7 @@ class Drawing extends WriterPart // a:alpha $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $drawing->getShadow()->getAlpha() * 1000); + $objWriter->writeAttribute('val', (string) ($drawing->getShadow()->getAlpha() * 1000)); $objWriter->endElement(); $objWriter->endElement(); @@ -528,4 +535,9 @@ class Drawing extends WriterPart $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId); $objWriter->endElement(); } + + private static function stringEmu(int $pixelValue): string + { + return (string) SharedDrawing::pixelsToEMU($pixelValue); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php index bb71ee6e..e95c3267 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php @@ -41,7 +41,7 @@ class CalculationFunctionListTest extends TestCase * @param array|string $functionCall * @param string $argumentCount */ - public function testGetFunctions($category, $functionCall, $argumentCount): void + public function testGetFunctions(/** @scrutinizer ignore-unused */ $category, $functionCall, /** @scrutinizer ignore-unused */ $argumentCount): void { self::assertIsCallable($functionCall); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php index 451908e0..827654ad 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php @@ -4,16 +4,10 @@ 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 ComplexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOMPLEX * @@ -21,7 +15,15 @@ class ComplexTest extends TestCase */ public function testCOMPLEX($expectedResult, ...$args): void { - $result = Engineering::COMPLEX(...$args); + if (count($args) === 0) { + $result = Engineering::COMPLEX(); + } elseif (count($args) === 1) { + $result = Engineering::COMPLEX($args[0]); + } elseif (count($args) === 2) { + $result = Engineering::COMPLEX($args[0], $args[1]); + } else { + $result = Engineering::COMPLEX($args[0], $args[1], $args[2]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php index cbb8e031..22d99eca 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php @@ -4,16 +4,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class DollarDeTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerDOLLARDE * @@ -21,7 +15,13 @@ class DollarDeTest extends TestCase */ public function testDOLLARDE($expectedResult, ...$args): void { - $result = Financial::DOLLARDE(...$args); + if (count($args) === 0) { + $result = Financial::DOLLARDE(); + } elseif (count($args) === 1) { + $result = Financial::DOLLARDE($args[0]); + } else { + $result = Financial::DOLLARDE($args[0], $args[1]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php index ce74595a..79e3d5ca 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class DollarFrTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerDOLLARFR * @@ -20,7 +14,13 @@ class DollarFrTest extends TestCase */ public function testDOLLARFR($expectedResult, ...$args): void { - $result = Financial::DOLLARFR(...$args); + if (count($args) === 0) { + $result = Financial::DOLLARFR(); + } elseif (count($args) === 1) { + $result = Financial::DOLLARFR($args[0]); + } else { + $result = Financial::DOLLARFR($args[0], $args[1]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php index 8d17d852..0e621766 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class FvTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerFV * @@ -20,7 +14,19 @@ class FvTest extends TestCase */ public function testFV($expectedResult, array $args): void { - $result = Financial::FV(...$args); + if (count($args) === 0) { + $result = Financial::FV(); + } elseif (count($args) === 1) { + $result = Financial::FV($args[0]); + } elseif (count($args) === 2) { + $result = Financial::FV($args[0], $args[1]); + } elseif (count($args) === 3) { + $result = Financial::FV($args[0], $args[1], $args[2]); + } elseif (count($args) === 4) { + $result = Financial::FV($args[0], $args[1], $args[2], $args[3]); + } else { + $result = Financial::FV($args[0], $args[1], $args[2], $args[3], $args[4]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php index 87230eca..2d7ded04 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class NPerTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerNPER * @@ -20,7 +14,19 @@ class NPerTest extends TestCase */ public function testNPER($expectedResult, array $args): void { - $result = Financial::NPER(...$args); + if (count($args) === 0) { + $result = Financial::NPER(); + } elseif (count($args) === 1) { + $result = Financial::NPER($args[0]); + } elseif (count($args) === 2) { + $result = Financial::NPER($args[0], $args[1]); + } elseif (count($args) === 3) { + $result = Financial::NPER($args[0], $args[1], $args[2]); + } elseif (count($args) === 4) { + $result = Financial::NPER($args[0], $args[1], $args[2], $args[3]); + } else { + $result = Financial::NPER($args[0], $args[1], $args[2], $args[3], $args[4]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php index a8d3095a..dbe1b09a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class PDurationTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPDURATION * @@ -20,7 +14,15 @@ class PDurationTest extends TestCase */ public function testPDURATION($expectedResult, array $args): void { - $result = Financial::PDURATION(...$args); + if (count($args) === 0) { + $result = Financial::PDURATION(); + } elseif (count($args) === 1) { + $result = Financial::PDURATION($args[0]); + } elseif (count($args) === 2) { + $result = Financial::PDURATION($args[0], $args[1]); + } else { + $result = Financial::PDURATION($args[0], $args[1], $args[2]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php index d996db10..5c1f6556 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class PmtTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPMT * @@ -23,7 +17,13 @@ class PmtTest extends TestCase $interestRate = array_shift($args); $numberOfPeriods = array_shift($args); $presentValue = array_shift($args); - $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue, ...$args); + if (count($args) === 0) { + $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue); + } elseif (count($args) === 1) { + $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue, $args[0]); + } else { + $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue, $args[0], $args[1]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php index c1b40231..4e4a8f80 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class PvTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPV * @@ -20,7 +14,19 @@ class PvTest extends TestCase */ public function testPV($expectedResult, array $args): void { - $result = Financial::PV(...$args); + if (count($args) === 0) { + $result = Financial::PV(); + } elseif (count($args) === 1) { + $result = Financial::PV($args[0]); + } elseif (count($args) === 2) { + $result = Financial::PV($args[0], $args[1]); + } elseif (count($args) === 3) { + $result = Financial::PV($args[0], $args[1], $args[2]); + } elseif (count($args) === 4) { + $result = Financial::PV($args[0], $args[1], $args[2], $args[3]); + } else { + $result = Financial::PV($args[0], $args[1], $args[2], $args[3], $args[4]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php index e641b590..9107ba96 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class RriTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerRRI * @@ -20,7 +14,15 @@ class RriTest extends TestCase */ public function testRRI($expectedResult, array $args): void { - $result = Financial::RRI(...$args); + if (count($args) === 0) { + $result = Financial::RRI(); + } elseif (count($args) === 1) { + $result = Financial::RRI($args[0]); + } elseif (count($args) === 2) { + $result = Financial::RRI($args[0], $args[1]); + } else { + $result = Financial::RRI($args[0], $args[1], $args[2]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php index 770b8ffa..db8fa112 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php @@ -12,9 +12,13 @@ class UsDollarTest extends TestCase * * @param mixed $expectedResult */ - public function testUSDOLLAR($expectedResult, ...$args): void + public function testUSDOLLAR($expectedResult, float $amount, ?int $precision = null): void { - $result = Dollar::format(...$args); + if ($precision === null) { + $result = Dollar::format($amount); + } else { + $result = Dollar::format($amount, $precision); + } self::assertSame($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php index b3e21986..58997161 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php @@ -2,17 +2,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Logical; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Logical; use PHPUnit\Framework\TestCase; class IfTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerIF * @@ -20,7 +14,15 @@ class IfTest extends TestCase */ public function testIF($expectedResult, ...$args): void { - $result = Logical::statementIf(...$args); + if (count($args) === 0) { + $result = Logical::statementIf(); + } elseif (count($args) === 1) { + $result = Logical::statementIf($args[0]); + } elseif (count($args) === 2) { + $result = Logical::statementIf($args[0], $args[1]); + } else { + $result = Logical::statementIf($args[0], $args[1], $args[2]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php index 03eb15ba..6d8b2ca9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Logical; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Logical; use PHPUnit\Framework\TestCase; class NotTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerNOT * @@ -21,7 +15,11 @@ class NotTest extends TestCase */ public function testNOT($expectedResult, ...$args): void { - $result = Logical::NOT(...$args); + if (count($args) === 0) { + $result = Logical::NOT(); + } else { + $result = Logical::NOT($args[0]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php new file mode 100644 index 00000000..5a584c46 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php @@ -0,0 +1,85 @@ +sampleDataForRow(), $criteria); + self::assertSame($expectedResult, $result); + } + + public function testFilterByColumn(): void + { + $criteria = [[false, false, true, false, true, false, false, false, true, true]]; + $expectedResult = [ + ['Betty', 'Charlotte', 'Oliver', 'Zoe'], + ['B', 'B', 'B', 'B'], + [1, 2, 4, 8], + ]; + $result = Filter::filter($this->sampleDataForColumn(), $criteria); + self::assertSame($expectedResult, $result); + } + + public function testFilterException(): void + { + $criteria = 'INVALID'; + $result = Filter::filter($this->sampleDataForColumn(), $criteria); + self::assertSame(ExcelError::VALUE(), $result); + } + + public function testFilterEmpty(): void + { + $criteria = [[false], [false], [false]]; + $expectedResult = ExcelError::CALC(); + $result = Filter::filter([[1], [2], [3]], $criteria); + self::assertSame($expectedResult, $result); + + $expectedResult = 'Invalid Data'; + $result = Filter::filter([[1], [2], [3]], $criteria, $expectedResult); + self::assertSame($expectedResult, $result); + } + + protected function sampleDataForRow(): array + { + return [ + ['East', 'Tom', 'Apple', 6830], + ['West', 'Fred', 'Grape', 5619], + ['North', 'Amy', 'Pear', 4565], + ['South', 'Sal', 'Banana', 5323], + ['East', 'Fritz', 'Apple', 4394], + ['West', 'Sravan', 'Grape', 7195], + ['North', 'Xi', 'Pear', 5231], + ['South', 'Hector', 'Banana', 2427], + ['East', 'Tom', 'Banana', 4213], + ['West', 'Fred', 'Pear', 3239], + ['North', 'Amy', 'Grape', 6420], + ['South', 'Sal', 'Apple', 1310], + ['East', 'Fritz', 'Banana', 6274], + ['West', 'Sravan', 'Pear', 4894], + ['North', 'Xi', 'Grape', 7580], + ['South', 'Hector', 'Apple', 8144], + ]; + } + + protected function sampleDataForColumn(): array + { + return [ + ['Aiden', 'Andrew', 'Betty', 'Caden', 'Charlotte', 'Emma', 'Isabella', 'Mason', 'Oliver', 'Zoe'], + ['A', 'C', 'B', 'A', 'B', 'C', 'A', 'A', 'B', 'B'], + [0, 4, 1, 2, 2, 0, 2, 4, 4, 8], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php new file mode 100644 index 00000000..e5b4cace --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php @@ -0,0 +1,77 @@ + ['A', 1], + 'Mismatched sortIndex count' => [[1, 2, 3, 4], 1], + 'Non-numeric sortOrder' => [[1, 2, 3], 'A'], + 'Invalid negative sortOrder' => [[1, 2, 3], -2], + 'Zero sortOrder' => [[1, 2, 3], 0], + 'Invalid positive sortOrder' => [[1, 2, 3], 2], + ]; + } + + /** + * @dataProvider providerSortByRow + */ + public function testSortByRow(array $expectedResult, array $matrix, ...$args): void + { + $result = Sort::sortBy($matrix, ...$args); + self::assertSame($expectedResult, $result); + } + + public function providerSortByRow(): array + { + return [ + 'Simple sort by age' => [ + [ + ['Fritz', 19], + ['Xi', 19], + ['Amy', 22], + ['Srivan', 39], + ['Tom', 52], + ['Fred', 65], + ['Hector', 66], + ['Sal', 73], + ], + $this->sampleDataForSimpleSort(), + array_column($this->sampleDataForSimpleSort(), 1), + ], + 'Simple sort by name' => [ + [ + ['Amy', 22], + ['Fred', 65], + ['Fritz', 19], + ['Hector', 66], + ['Sal', 73], + ['Srivan', 39], + ['Tom', 52], + ['Xi', 19], + ], + $this->sampleDataForSimpleSort(), + array_column($this->sampleDataForSimpleSort(), 0), + ], + 'Row vector' => [ + [ + ['Amy', 22], + ['Fred', 65], + ['Fritz', 19], + ['Hector', 66], + ['Sal', 73], + ['Srivan', 39], + ['Tom', 52], + ['Xi', 19], + ], + $this->sampleDataForSimpleSort(), + ['Tom', 'Fred', 'Amy', 'Sal', 'Fritz', 'Srivan', 'Xi', 'Hector'], + ], + 'Column vector' => [ + [ + ['Amy', 22], + ['Fred', 65], + ['Fritz', 19], + ['Hector', 66], + ['Sal', 73], + ['Srivan', 39], + ['Tom', 52], + ['Xi', 19], + ], + $this->sampleDataForSimpleSort(), + [['Tom'], ['Fred'], ['Amy'], ['Sal'], ['Fritz'], ['Srivan'], ['Xi'], ['Hector']], + ], + 'Sort by region asc, name asc' => [ + [ + ['East', 'Fritz', 19], + ['East', 'Tom', 52], + ['North', 'Amy', 22], + ['North', 'Xi', 19], + ['South', 'Hector', 66], + ['South', 'Sal', 73], + ['West', 'Fred', 65], + ['West', 'Srivan', 39], + ], + $this->sampleDataForMultiSort(), + array_column($this->sampleDataForMultiSort(), 0), + Sort::ORDER_ASCENDING, + array_column($this->sampleDataForMultiSort(), 1), + ], + 'Sort by region asc, age desc' => [ + [ + ['East', 'Tom', 52], + ['East', 'Fritz', 19], + ['North', 'Amy', 22], + ['North', 'Xi', 19], + ['South', 'Sal', 73], + ['South', 'Hector', 66], + ['West', 'Fred', 65], + ['West', 'Srivan', 39], + ], + $this->sampleDataForMultiSort(), + array_column($this->sampleDataForMultiSort(), 0), + Sort::ORDER_ASCENDING, + array_column($this->sampleDataForMultiSort(), 2), + Sort::ORDER_DESCENDING, + ], + ]; + } + + private function sampleDataForSimpleSort(): array + { + return [ + ['Tom', 52], + ['Fred', 65], + ['Amy', 22], + ['Sal', 73], + ['Fritz', 19], + ['Srivan', 39], + ['Xi', 19], + ['Hector', 66], + ]; + } + + private function sampleDataForMultiSort(): array + { + return [ + ['North', 'Amy', 22], + ['West', 'Fred', 65], + ['East', 'Fritz', 19], + ['South', 'Hector', 66], + ['South', 'Sal', 73], + ['West', 'Srivan', 39], + ['East', 'Tom', 52], + ['North', 'Xi', 19], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php new file mode 100644 index 00000000..bd120e30 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php @@ -0,0 +1,210 @@ + [-1, -1], + 'Non-numeric sortIndex' => ['A', -1], + 'Zero sortIndex' => [0, -1], + 'Too high sortIndex' => [3, -1], + 'Non-numeric sortOrder' => [1, 'A'], + 'Invalid negative sortOrder' => [1, -2], + 'Zero sortOrder' => [1, 0], + 'Invalid positive sortOrder' => [1, 2], + 'Too many sortOrders (scalar and array)' => [1, [-1, 1]], + 'Too many sortOrders (both array)' => [[1, 2], [1, 2, 3]], + 'Zero positive sortIndex in vector' => [[0, 1]], + 'Too high sortIndex in vector' => [[1, 3]], + 'Invalid sortOrder in vector' => [[1, 2], [1, -2]], + ]; + } + + /** + * @dataProvider providerSortByRow + */ + public function testSortByRow(array $expectedResult, array $matrix, int $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void + { + $result = Sort::sort($matrix, $sortIndex, $sortOrder); + self::assertSame($expectedResult, $result); + } + + public function providerSortByRow(): array + { + return [ + [ + [[142], [378], [404], [445], [483], [622], [650], [691], [783], [961]], + $this->sampleDataForRow(), + 1, + ], + [ + [[961], [783], [691], [650], [622], [483], [445], [404], [378], [142]], + $this->sampleDataForRow(), + 1, + Sort::ORDER_DESCENDING, + ], + [ + [['Peaches', 25], ['Cherries', 29], ['Grapes', 31], ['Lemons', 34], ['Oranges', 36], ['Apples', 38], ['Pears', 40]], + [['Apples', 38], ['Cherries', 29], ['Grapes', 31], ['Lemons', 34], ['Oranges', 36], ['Peaches', 25], ['Pears', 40]], + 2, + ], + ]; + } + + /** + * @dataProvider providerSortByRowMultiLevel + */ + public function testSortByRowMultiLevel(array $expectedResult, array $matrix, array $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void + { + $result = Sort::sort($matrix, $sortIndex, $sortOrder); + self::assertSame($expectedResult, $result); + } + + public function providerSortByRowMultiLevel(): array + { + return [ + [ + [ + ['East', 'Grapes', 31], + ['East', 'Lemons', 36], + ['North', 'Cherries', 29], + ['North', 'Grapes', 27], + ['North', 'Peaches', 25], + ['South', 'Apples', 38], + ['South', 'Cherries', 28], + ['South', 'Oranges', 36], + ['South', 'Pears', 40], + ['West', 'Apples', 30], + ['West', 'Lemons', 34], + ['West', 'Oranges', 25], + ], + $this->sampleDataForMultiRow(), + [1, 2], + ], + [ + [ + ['East', 'Grapes', 31], + ['East', 'Lemons', 36], + ['North', 'Peaches', 25], + ['North', 'Grapes', 27], + ['North', 'Cherries', 29], + ['South', 'Cherries', 28], + ['South', 'Oranges', 36], + ['South', 'Apples', 38], + ['South', 'Pears', 40], + ['West', 'Oranges', 25], + ['West', 'Apples', 30], + ['West', 'Lemons', 34], + ], + $this->sampleDataForMultiRow(), + [1, 3], + ], + [ + [ + ['West', 'Apples', 30], + ['South', 'Apples', 38], + ['South', 'Cherries', 28], + ['North', 'Cherries', 29], + ['North', 'Grapes', 27], + ['East', 'Grapes', 31], + ['West', 'Lemons', 34], + ['East', 'Lemons', 36], + ['West', 'Oranges', 25], + ['South', 'Oranges', 36], + ['North', 'Peaches', 25], + ['South', 'Pears', 40], + ], + $this->sampleDataForMultiRow(), + [2, 3], + ], + ]; + } + + /** + * @dataProvider providerSortByColumn + */ + public function testSortByColumn(array $expectedResult, array $matrix, int $sortIndex, int $sortOrder): void + { + $result = Sort::sort($matrix, $sortIndex, $sortOrder, true); + self::assertSame($expectedResult, $result); + } + + public function providerSortByColumn(): array + { + return [ + [ + [[142, 378, 404, 445, 483, 622, 650, 691, 783, 961]], + $this->sampleDataForColumn(), + 1, + Sort::ORDER_ASCENDING, + ], + [ + [[961, 783, 691, 650, 622, 483, 445, 404, 378, 142]], + $this->sampleDataForColumn(), + 1, + Sort::ORDER_DESCENDING, + ], + ]; + } + + public function sampleDataForRow(): array + { + return [ + [622], [961], [691], [445], [378], [483], [650], [783], [142], [404], + ]; + } + + public function sampleDataForMultiRow(): array + { + return [ + ['South', 'Pears', 40], + ['South', 'Apples', 38], + ['South', 'Oranges', 36], + ['East', 'Lemons', 36], + ['West', 'Lemons', 34], + ['East', 'Grapes', 31], + ['West', 'Apples', 30], + ['North', 'Cherries', 29], + ['South', 'Cherries', 28], + ['North', 'Grapes', 27], + ['North', 'Peaches', 25], + ['West', 'Oranges', 25], + ]; + } + + public function sampleDataForColumn(): array + { + return [ + [622, 961, 691, 445, 378, 483, 650, 783, 142, 404], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php new file mode 100644 index 00000000..6232505d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php @@ -0,0 +1,157 @@ +createMock(StreamInterface::class); $body->expects(self::atMost(1))->method('getContents')->willReturn($responseData[1]); diff --git a/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php index c12bf060..9ea62400 100644 --- a/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php @@ -76,12 +76,10 @@ class FunctionsTest extends TestCase /** * @dataProvider providerIfCondition - * - * @param mixed $expectedResult */ - public function testIfCondition($expectedResult, ...$args): void + public function testIfCondition(string $expectedResult, string $args): void { - $result = Functions::ifCondition(...$args); + $result = Functions::ifCondition($args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php b/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php index 58c696ef..f82a4ead 100644 --- a/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php @@ -32,15 +32,17 @@ class AddressHelperTest extends TestCase ?int $row = null, ?int $column = null ): void { - $args = []; - if ($row !== null) { - $args[] = $row; + if ($row === null) { + if ($column === null) { + $actualValue = AddressHelper::convertToA1($address); + } else { + $actualValue = AddressHelper::convertToA1($address, $column); + } + } elseif ($column === null) { + $actualValue = AddressHelper::convertToA1($address, $row); + } else { + $actualValue = AddressHelper::convertToA1($address, $row, $column); } - if ($column !== null) { - $args[] = $column; - } - - $actualValue = AddressHelper::convertToA1($address, ...$args); self::assertSame($expectedValue, $actualValue); } diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 9a0a2c21..8ed23427 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -265,7 +265,7 @@ class CoordinateTest extends TestCase $cellRange = null; // @phpstan-ignore-next-line - Coordinate::buildRange($cellRange); + Coordinate::buildRange(/** @scrutinizer ignore-type */ $cellRange); } public function testBuildRangeInvalid2(): void diff --git a/tests/PhpSpreadsheetTests/CellReferenceHelperTest.php b/tests/PhpSpreadsheetTests/CellReferenceHelperTest.php new file mode 100644 index 00000000..35352afd --- /dev/null +++ b/tests/PhpSpreadsheetTests/CellReferenceHelperTest.php @@ -0,0 +1,193 @@ +updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertColumnsProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['G5', 'E5'], + ['$E5', '$E5'], + ['G$5', 'E$5'], + ['I5', 'G5'], + ['$G$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteColumnsProvider + */ + public function testCellReferenceHelperDeleteColumns(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', -2, 0); + $result = $cellReferenceHelper->updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteColumnsProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['C5', 'E5'], + ['$E5', '$E5'], + ['C$5', 'E$5'], + ['E5', 'G5'], + ['$G$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperInsertRowsProvider + */ + public function testCellReferenceHelperInsertRows(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, 2); + $result = $cellReferenceHelper->updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertRowsProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E7', 'E5'], + ['E$5', 'E$5'], + ['$E7', '$E5'], + ['E11', 'E9'], + ['$E$9', '$E$9'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteRowsProvider + */ + public function testCellReferenceHelperDeleteRows(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, -2); + $result = $cellReferenceHelper->updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteRowsProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E3', 'E5'], + ['E$5', 'E$5'], + ['$E3', '$E5'], + ['E7', 'E9'], + ['$E$9', '$E$9'], + ]; + } + + /** + * @dataProvider cellReferenceHelperInsertColumnsAbsoluteProvider + */ + public function testCellReferenceHelperInsertColumnsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 2, 0); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertColumnsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['G5', 'E5'], + ['$G5', '$E5'], + ['G$5', 'E$5'], + ['I5', 'G5'], + ['$I$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteColumnsAbsoluteProvider + */ + public function testCellReferenceHelperDeleteColumnsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', -2, 0); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteColumnsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['C5', 'E5'], + ['$C5', '$E5'], + ['C$5', 'E$5'], + ['E5', 'G5'], + ['$E$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperInsertRowsAbsoluteProvider + */ + public function testCellReferenceHelperInsertRowsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, 2); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertRowsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E7', 'E5'], + ['E$7', 'E$5'], + ['$E7', '$E5'], + ['E11', 'E9'], + ['$E$11', '$E$9'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteRowsAbsoluteProvider + */ + public function testCellReferenceHelperDeleteRowsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, -2); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteRowsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E3', 'E5'], + ['E$3', 'E$5'], + ['$E3', '$E5'], + ['E7', 'E9'], + ['$E$7', '$E$9'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php index c539da09..e6c95cd4 100644 --- a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -155,11 +155,17 @@ class PropertiesTest extends TestCase * * @param mixed $expectedType * @param mixed $expectedValue - * @param mixed $propertyName + * @param string $propertyName + * @param mixed $propertyValue + * @param ?string $propertyType */ - public function testSetCustomProperties($expectedType, $expectedValue, $propertyName, ...$args): void + public function testSetCustomProperties($expectedType, $expectedValue, $propertyName, $propertyValue, $propertyType = null): void { - $this->properties->setCustomProperty($propertyName, ...$args); + if ($propertyType === null) { + $this->properties->setCustomProperty($propertyName, $propertyValue); + } else { + $this->properties->setCustomProperty($propertyName, $propertyValue, $propertyType); + } self::assertTrue($this->properties->isCustomPropertySet($propertyName)); self::assertSame($expectedType, $this->properties->getCustomPropertyType($propertyName)); $result = $this->properties->getCustomPropertyValue($propertyName); diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index 958c6eb0..b516a9f4 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -81,39 +81,13 @@ class IOFactoryTest extends TestCase /** * @dataProvider providerIdentify - * - * @param string $file - * @param string $expectedName - * @param string $expectedClass */ - public function testIdentify($file, $expectedName, $expectedClass): void + public function testIdentifyCreateLoad(string $file, string $expectedName, string $expectedClass): void { $actual = IOFactory::identify($file); self::assertSame($expectedName, $actual); - } - - /** - * @dataProvider providerIdentify - * - * @param string $file - * @param string $expectedName - * @param string $expectedClass - */ - public function testCreateReaderForFile($file, $expectedName, $expectedClass): void - { $actual = IOFactory::createReaderForFile($file); self::assertSame($expectedClass, get_class($actual)); - } - - /** - * @dataProvider providerIdentify - * - * @param string $file - * @param string $expectedName - * @param string $expectedClass - */ - public function testLoad($file, $expectedName, $expectedClass): void - { $actual = IOFactory::load($file); self::assertInstanceOf(Spreadsheet::class, $actual); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php new file mode 100644 index 00000000..ce224864 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php @@ -0,0 +1,158 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + /** + * @dataProvider conditionalFormattingProvider + */ + public function testReadConditionalFormatting(string $expectedRange, array $expectedRules): void + { + $hasConditionalStyles = $this->sheet->conditionalStylesExists($expectedRange); + self::assertTrue($hasConditionalStyles); + + $conditionalStyles = $this->sheet->getConditionalStyles($expectedRange); + + foreach ($conditionalStyles as $index => $conditionalStyle) { + self::assertSame($expectedRules[$index]['type'], $conditionalStyle->getConditionType()); + self::assertSame($expectedRules[$index]['operator'], $conditionalStyle->getOperatorType()); + self::assertSame($expectedRules[$index]['conditions'], $conditionalStyle->getConditions()); + } + } + + public function conditionalFormattingProvider(): array + { + return [ + [ + 'A2:E5', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + 0, + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_GREATERTHAN, + 'conditions' => [ + 0, + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_LESSTHAN, + 'conditions' => [ + 0, + ], + ], + ], + ], + [ + 'A10:E13', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + '$H$9', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_GREATERTHAN, + 'conditions' => [ + '$H$9', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_LESSTHAN, + 'conditions' => [ + '$H$9', + ], + ], + ], + ], + [ + 'A18:A20', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_BETWEEN, + 'conditions' => [ + '$B1', + '$C1', + ], + ], + ], + ], + [ + 'A24:E27', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_BETWEEN, + 'conditions' => [ + 'AVERAGE($A$24:$E$27)-STDEV($A$24:$E$27)', + 'AVERAGE($A$24:$E$27)+STDEV($A$24:$E$27)', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_GREATERTHAN, + 'conditions' => [ + 'AVERAGE($A$24:$E$27)+STDEV($A$24:$E$27)', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_LESSTHAN, + 'conditions' => [ + 'AVERAGE($A$24:$E$27)-STDEV($A$24:$E$27)', + ], + ], + ], + ], + [ + 'A31:A33', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + '"LOVE"', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + '"PHP"', + ], + ], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php new file mode 100644 index 00000000..6cc566c1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php @@ -0,0 +1,71 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + /** + * @dataProvider conditionalFormattingProvider + */ + public function testReadConditionalFormatting(string $expectedRange, array $expectedRule): void + { + $hasConditionalStyles = $this->sheet->conditionalStylesExists($expectedRange); + self::assertTrue($hasConditionalStyles); + + $conditionalStyles = $this->sheet->getConditionalStyles($expectedRange); + + foreach ($conditionalStyles as $index => $conditionalStyle) { + self::assertSame($expectedRule[$index]['type'], $conditionalStyle->getConditionType()); + self::assertSame($expectedRule[$index]['operator'], $conditionalStyle->getOperatorType()); + self::assertSame($expectedRule[$index]['conditions'], $conditionalStyle->getConditions()); + } + } + + public function conditionalFormattingProvider(): array + { + return [ + [ + 'A3:D8', + [ + [ + 'type' => Conditional::CONDITION_EXPRESSION, + 'operator' => Conditional::OPERATOR_NONE, + 'conditions' => [ + '$C1="USA"', + ], + ], + ], + ], + [ + 'A13:D18', + [ + [ + 'type' => Conditional::CONDITION_EXPRESSION, + 'operator' => Conditional::OPERATOR_NONE, + 'conditions' => [ + 'AND($C1="USA",$D1="Q4")', + ], + ], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php new file mode 100644 index 00000000..bdefa17e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php @@ -0,0 +1,60 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + /** + * @dataProvider dataValidationProvider + */ + public function testDataValidation(string $expectedRange, array $expectedRule): void + { + $hasDataValidation = $this->sheet->dataValidationExists($expectedRange); + self::assertTrue($hasDataValidation); + + $dataValidation = $this->sheet->getDataValidation($expectedRange); + self::assertSame($expectedRule['type'], $dataValidation->getType()); + self::assertSame($expectedRule['operator'], $dataValidation->getOperator()); + self::assertSame($expectedRule['formula'], $dataValidation->getFormula1()); + } + + public function dataValidationProvider(): array + { + return [ + [ + 'B2', + [ + 'type' => DataValidation::TYPE_WHOLE, + 'operator' => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 'formula' => '18', + ], + ], + [ + 'B3', + [ + 'type' => DataValidation::TYPE_LIST, + 'operator' => DataValidation::OPERATOR_BETWEEN, + 'formula' => '"Blocked,Pending,Approved"', + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index 2c016b0b..2640d80c 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -3,8 +3,12 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; +use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class ReferenceHelperTest extends TestCase @@ -177,4 +181,357 @@ class ReferenceHelperTest extends TestCase self::assertNull($cells[1][1]); self::assertArrayNotHasKey(2, $cells[1]); } + + public function testInsertRowsWithPageBreaks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->setBreak('A2', Worksheet::BREAK_ROW); + $sheet->setBreak('A5', Worksheet::BREAK_ROW); + + $sheet->insertNewRowBefore(2, 2); + + $breaks = $sheet->getBreaks(); + ksort($breaks); + self::assertSame(['A4' => Worksheet::BREAK_ROW, 'A7' => Worksheet::BREAK_ROW], $breaks); + } + + public function testDeleteRowsWithPageBreaks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->setBreak('A2', Worksheet::BREAK_ROW); + $sheet->setBreak('A5', Worksheet::BREAK_ROW); + + $sheet->removeRow(2, 2); + + $breaks = $sheet->getBreaks(); + self::assertSame(['A3' => Worksheet::BREAK_ROW], $breaks); + } + + public function testInsertRowsWithComments(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getComment('A2')->getText()->createText('First Comment'); + $sheet->getComment('A5')->getText()->createText('Second Comment'); + + $sheet->insertNewRowBefore(2, 2); + + $comments = array_map( + function (Comment $value) { + return $value->getText()->getPlainText(); + }, + $sheet->getComments() + ); + + self::assertSame(['A4' => 'First Comment', 'A7' => 'Second Comment'], $comments); + } + + public function testDeleteRowsWithComments(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getComment('A2')->getText()->createText('First Comment'); + $sheet->getComment('A5')->getText()->createText('Second Comment'); + + $sheet->removeRow(2, 2); + + $comments = array_map( + function (Comment $value) { + return $value->getText()->getPlainText(); + }, + $sheet->getComments() + ); + + self::assertSame(['A3' => 'Second Comment'], $comments); + } + + public function testInsertRowsWithHyperlinks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getCell('A2')->getHyperlink()->setUrl('https://github.com/PHPOffice/PhpSpreadsheet'); + $sheet->getCell('A5')->getHyperlink()->setUrl('https://phpspreadsheet.readthedocs.io/en/latest/'); + + $sheet->insertNewRowBefore(2, 2); + + $hyperlinks = array_map( + function (Hyperlink $value) { + return $value->getUrl(); + }, + $sheet->getHyperlinkCollection() + ); + ksort($hyperlinks); + + self::assertSame( + [ + 'A4' => 'https://github.com/PHPOffice/PhpSpreadsheet', + 'A7' => 'https://phpspreadsheet.readthedocs.io/en/latest/', + ], + $hyperlinks + ); + } + + public function testDeleteRowsWithHyperlinks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getCell('A2')->getHyperlink()->setUrl('https://github.com/PHPOffice/PhpSpreadsheet'); + $sheet->getCell('A5')->getHyperlink()->setUrl('https://phpspreadsheet.readthedocs.io/en/latest/'); + + $sheet->removeRow(2, 2); + + $hyperlinks = array_map( + function (Hyperlink $value) { + return $value->getUrl(); + }, + $sheet->getHyperlinkCollection() + ); + + self::assertSame(['A3' => 'https://phpspreadsheet.readthedocs.io/en/latest/'], $hyperlinks); + } + + public function testInsertRowsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->insertNewRowBefore(2, 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('E7')->hasDataValidation()); + } + + public function testDeleteRowsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->removeRow(2, 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('E3')->hasDataValidation()); + } + + public function testDeleteColumnsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->removeColumn('B', 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('C5')->hasDataValidation()); + } + + public function testInsertColumnsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->insertNewColumnBefore('C', 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('G5')->hasDataValidation()); + } + + private function setDataValidation(Worksheet $sheet, string $cellAddress): void + { + $validation = $sheet->getCell($cellAddress) + ->getDataValidation(); + $validation->setType(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::TYPE_LIST); + $validation->setErrorStyle(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::STYLE_INFORMATION); + $validation->setAllowBlank(false); + $validation->setShowInputMessage(true); + $validation->setShowErrorMessage(true); + $validation->setShowDropDown(true); + $validation->setErrorTitle('Input error'); + $validation->setError('Value is not in list.'); + $validation->setPromptTitle('Pick from list'); + $validation->setPrompt('Please pick a value from the drop-down list.'); + $validation->setFormula1('$A5:$A8'); + } + + public function testInsertRowsWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->insertNewRowBefore(4, 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:F9', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$H$7', $conditions->getConditions()[0]); + } + } + } + + public function testInsertColumnssWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->insertNewColumnBefore('C', 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('E3:H7', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$J$5', $conditions->getConditions()[0]); + } + } + } + + public function testDeleteRowsWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->removeRow(4, 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:F5', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$H$5', $conditions->getConditions()[0]); + } + } + } + + public function testDeleteColumnsWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->removeColumn('D', 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:D7', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$F$5', $conditions->getConditions()[0]); + } + } + } + + private function setConditionalFormatting(Worksheet $sheet, string $cellRange): void + { + $conditionalStyles = []; + $wizardFactory = new Wizard($cellRange); + /** @var Wizard\CellValue $cellWizard */ + $cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE); + + $cellWizard->equals('$H$5', Wizard::VALUE_TYPE_CELL); + $conditionalStyles[] = $cellWizard->getConditional(); + + $cellWizard->greaterThan('$H$5', Wizard::VALUE_TYPE_CELL); + $conditionalStyles[] = $cellWizard->getConditional(); + + $cellWizard->lessThan('$H$5', Wizard::VALUE_TYPE_CELL); + $conditionalStyles[] = $cellWizard->getConditional(); + + $sheet->getStyle($cellWizard->getCellRange()) + ->setConditionalStyles($conditionalStyles); + } + + public function testInsertRowsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->insertNewRowBefore(2, 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:J12', $printArea); + } + + public function testInsertColumnsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->insertNewColumnBefore('B', 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:L10', $printArea); + } + + public function testDeleteRowsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->removeRow(2, 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:J8', $printArea); + } + + public function testDeleteColumnsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->removeColumn('B', 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:H10', $printArea); + } } diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 9fb4d919..90ed5cc2 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -176,9 +176,9 @@ class DateTest extends TestCase * * @param mixed $expectedResult */ - public function testIsDateTimeFormatCode($expectedResult, ...$args): void + public function testIsDateTimeFormatCode($expectedResult, string $format): void { - $result = Date::isDateTimeFormatCode(...$args); + $result = Date::isDateTimeFormatCode($format); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Style/ColorTest.php b/tests/PhpSpreadsheetTests/Style/ColorTest.php index 6575c850..9767c537 100644 --- a/tests/PhpSpreadsheetTests/Style/ColorTest.php +++ b/tests/PhpSpreadsheetTests/Style/ColorTest.php @@ -78,11 +78,14 @@ class ColorTest extends TestCase * @dataProvider providerColorGetRed * * @param mixed $expectedResult - * @param mixed $color */ - public function testGetRed($expectedResult, $color, ...$args): void + public function testGetRed($expectedResult, string $color, ?bool $bool = null): void { - $result = Color::getRed($color, ...$args); + if ($bool === null) { + $result = Color::getRed($color); + } else { + $result = Color::getRed($color, $bool); + } self::assertEquals($expectedResult, $result); } @@ -95,11 +98,14 @@ class ColorTest extends TestCase * @dataProvider providerColorGetGreen * * @param mixed $expectedResult - * @param mixed $color */ - public function testGetGreen($expectedResult, $color, ...$args): void + public function testGetGreen($expectedResult, string $color, ?bool $bool = null): void { - $result = Color::getGreen($color, ...$args); + if ($bool === null) { + $result = Color::getGreen($color); + } else { + $result = Color::getGreen($color, $bool); + } self::assertEquals($expectedResult, $result); } @@ -112,11 +118,14 @@ class ColorTest extends TestCase * @dataProvider providerColorGetBlue * * @param mixed $expectedResult - * @param mixed $color */ - public function testGetBlue($expectedResult, $color, ...$args): void + public function testGetBlue($expectedResult, string $color, ?bool $bool = null): void { - $result = Color::getBlue($color, ...$args); + if ($bool === null) { + $result = Color::getBlue($color); + } else { + $result = Color::getBlue($color, $bool); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index 28bbbb87..a80d3d63 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -134,6 +134,64 @@ class AutoFilterTest extends SetupTeardown } } + public function testRemoveColumns(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + $autoFilter->getColumn('L')->addRule((new Column\Rule())->setValue(5)); + + $sheet->removeColumn('K', 2); + $result = $autoFilter->getRange(); + self::assertEquals('H2:M256', $result); + + // Check that the rule that was set for column L is no longer set + self::assertEmpty($autoFilter->getColumn('L')->getRule(0)->getValue()); + } + + public function testRemoveRows(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + + $sheet->removeRow(42, 128); + $result = $autoFilter->getRange(); + self::assertEquals('H2:O128', $result); + } + + public function testInsertColumns(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + $autoFilter->getColumn('N')->addRule((new Column\Rule())->setValue(5)); + + $sheet->insertNewColumnBefore('N', 3); + $result = $autoFilter->getRange(); + self::assertEquals('H2:R256', $result); + + // Check that column N no longer has a rule set + self::assertEmpty($autoFilter->getColumn('N')->getRule(0)->getValue()); + // Check that the rule originally set in column N has been moved to column Q + self::assertSame(5, $autoFilter->getColumn('Q')->getRule(0)->getValue()); + } + + public function testInsertRows(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + + $sheet->insertNewRowBefore(3, 4); + $result = $autoFilter->getRange(); + self::assertEquals('H2:O260', $result); + } + public function testGetInvalidColumnOffset(): void { $this->expectException(PhpSpreadsheetException::class); diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php index a81058af..b9e18e43 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php @@ -44,7 +44,7 @@ class ColumnCellIteratorTest extends TestCase self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); } - $transposed = array_map(null, ...self::CELL_VALUES); + $transposed = array_map(/** @scrutinizer ignore-type */ null, ...self::CELL_VALUES); self::assertSame($transposed[0], $values); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php b/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php index d18ff002..17e17e19 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php @@ -2,7 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Worksheet; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; +use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PHPUnit\Framework\TestCase; @@ -41,4 +44,76 @@ class DrawingTest extends TestCase $spreadsheet->disconnectWorksheets(); } } + + public function testChangeWorksheet(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + + $drawing = new Drawing(); + $drawing->setName('Green Square'); + $drawing->setPath('tests/data/Writer/XLSX/green_square.gif'); + self::assertEquals($drawing->getWidth(), 150); + self::assertEquals($drawing->getHeight(), 150); + $drawing->setCoordinates('A1'); + $drawing->setOffsetX(30); + $drawing->setOffsetY(10); + $drawing->setWorksheet($sheet1); + + try { + $drawing->setWorksheet($sheet2); + self::fail('Should throw exception when attempting set worksheet without specifying override'); + } catch (PhpSpreadsheetException $e) { + self::assertStringContainsString('A Worksheet has already been assigned.', $e->getMessage()); + } + self::assertSame($sheet1, $drawing->getWorksheet()); + self::assertCount(1, $sheet1->getDrawingCollection()); + self::assertCount(0, $sheet2->getDrawingCollection()); + $drawing->setWorksheet($sheet2, true); + self::assertSame($sheet2, $drawing->getWorksheet()); + self::assertCount(0, $sheet1->getDrawingCollection()); + self::assertCount(1, $sheet2->getDrawingCollection()); + } + + public function testHeaderFooter(): void + { + $drawing1 = new HeaderFooterDrawing(); + $drawing1->setName('Blue Square'); + $drawing1->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertEquals($drawing1->getWidth(), 100); + self::assertEquals($drawing1->getHeight(), 100); + $drawing2 = new HeaderFooterDrawing(); + $drawing2->setName('Blue Square'); + $drawing2->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertSame($drawing1->getHashCode(), $drawing2->getHashCode()); + $drawing2->setOffsetX(100); + self::assertNotEquals($drawing1->getHashCode(), $drawing2->getHashCode()); + } + + public function testSetWidthAndHeight(): void + { + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertSame(100, $drawing->getWidth()); + self::assertSame(100, $drawing->getHeight()); + self::assertTrue($drawing->getResizeProportional()); + $drawing->setResizeProportional(false); + $drawing->setWidthAndHeight(150, 200); + self::assertSame(150, $drawing->getWidth()); + self::assertSame(200, $drawing->getHeight()); + $drawing->setResizeProportional(true); + $drawing->setWidthAndHeight(300, 250); + // width increase% more than height, so scale width + self::assertSame(188, $drawing->getWidth()); + self::assertSame(250, $drawing->getHeight()); + $drawing->setResizeProportional(false); + $drawing->setWidthAndHeight(150, 200); + $drawing->setResizeProportional(true); + // height increase% more than width, so scale height + $drawing->setWidthAndHeight(175, 350); + self::assertSame(175, $drawing->getWidth()); + self::assertSame(234, $drawing->getHeight()); + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index f9df2b1c..ccbced32 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -8,7 +8,9 @@ use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; +use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class DrawingsTest extends AbstractFunctional @@ -28,6 +30,8 @@ class DrawingsTest extends AbstractFunctional // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -59,6 +63,8 @@ class DrawingsTest extends AbstractFunctional // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -96,6 +102,8 @@ class DrawingsTest extends AbstractFunctional unlink($tempFileName); self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -173,7 +181,8 @@ class DrawingsTest extends AbstractFunctional self::assertEquals($comment->getBackgroundImage()->getType(), IMAGETYPE_PNG); unlink($tempFileName); - self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -287,6 +296,7 @@ class DrawingsTest extends AbstractFunctional $drawing->setPath('tests/data/Writer/XLSX/orange_square_24_bit.bmp'); self::assertEquals($drawing->getWidth(), 70); self::assertEquals($drawing->getHeight(), 70); + self::assertSame(IMAGETYPE_PNG, $drawing->getImageTypeForSave()); $comment = $sheet->getComment('A6'); $comment->setBackgroundImage($drawing); $comment->setSizeAsBackgroundImage(); @@ -304,6 +314,8 @@ class DrawingsTest extends AbstractFunctional $drawing = new Drawing(); $drawing->setName('Purple Square'); $drawing->setPath('tests/data/Writer/XLSX/purple_square.tiff'); + self::assertStringContainsString('purple_square.tiff', $drawing->getFilename()); + self::assertFalse($drawing->getIsUrl()); $comment = $sheet->getComment('A7'); self::assertTrue($comment instanceof Comment); self::assertFalse($comment->hasBackgroundImage()); @@ -326,6 +338,14 @@ class DrawingsTest extends AbstractFunctional self::assertEquals($e->getMessage(), 'Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); } + try { + $drawing->getMediaFilename(); + self::fail('Should throw exception when attempting to get media file name for tiff'); + } catch (PhpSpreadsheetException $e) { + self::assertTrue($e instanceof PhpSpreadsheetException); + self::assertEquals($e->getMessage(), 'Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + try { $drawing->getImageFileExtensionForSave(); self::fail('Should throw exception when attempting to get image file extention for tiff'); @@ -428,7 +448,8 @@ class DrawingsTest extends AbstractFunctional unlink($tempFileName); - self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -436,7 +457,6 @@ class DrawingsTest extends AbstractFunctional */ public function testTwoCellAnchorDrawing(): void { - $reader = new Xlsx(); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); @@ -455,31 +475,133 @@ class DrawingsTest extends AbstractFunctional $drawing->setWorksheet($sheet); // Write file - $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); - $tempFileName = File::sysGetTempDir() . '/drawings_image_that_two_cell_anchor.xlsx'; - $writer->save($tempFileName); - - // Read new file - $reloadedSpreadsheet = $reader->load($tempFileName); - $sheet = $reloadedSpreadsheet->getActiveSheet(); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); // Check image coordinates. - $drawingCollection = $sheet->getDrawingCollection(); + $drawingCollection = $rsheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection); $drawing = $drawingCollection[0]; self::assertNotNull($drawing); + self::assertSame(150, $drawing->getWidth()); + self::assertSame(150, $drawing->getHeight()); + self::assertSame('A1', $drawing->getCoordinates()); + self::assertSame(30, $drawing->getOffsetX()); + self::assertSame(10, $drawing->getOffsetY()); + self::assertSame('E8', $drawing->getCoordinates2()); + self::assertSame(-50, $drawing->getOffsetX2()); + self::assertSame(-20, $drawing->getOffsetY2()); + self::assertSame($rsheet, $drawing->getWorksheet()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + /** + * Test editAs attribute for two-cell anchors. + * + * @dataProvider providerEditAs + */ + public function testTwoCellEditAs(string $editAs, ?string $expectedResult = null): void + { + if ($expectedResult === null) { + $expectedResult = $editAs; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // Add gif image that coordinates is two cell anchor. + $drawing = new Drawing(); + $drawing->setName('Green Square'); + $drawing->setPath('tests/data/Writer/XLSX/green_square.gif'); self::assertEquals($drawing->getWidth(), 150); self::assertEquals($drawing->getHeight(), 150); - self::assertEquals($drawing->getCoordinates(), 'A1'); - self::assertEquals($drawing->getOffsetX(), 30); - self::assertEquals($drawing->getOffsetY(), 10); - self::assertEquals($drawing->getCoordinates2(), 'E8'); - self::assertEquals($drawing->getOffsetX2(), -50); - self::assertEquals($drawing->getOffsetY2(), -20); - self::assertEquals($drawing->getWorksheet(), $sheet); + $drawing->setCoordinates('A1'); + $drawing->setOffsetX(30); + $drawing->setOffsetY(10); + $drawing->setCoordinates2('E8'); + $drawing->setOffsetX2(-50); + $drawing->setOffsetY2(-20); + if ($editAs !== '') { + $drawing->setEditAs($editAs); + } + $drawing->setWorksheet($sheet); - unlink($tempFileName); + // Write file + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); - self::assertNotNull($reloadedSpreadsheet); + // Check image coordinates. + $drawingCollection = $rsheet->getDrawingCollection(); + $drawing = $drawingCollection[0]; + self::assertNotNull($drawing); + + self::assertSame(150, $drawing->getWidth()); + self::assertSame(150, $drawing->getHeight()); + self::assertSame('A1', $drawing->getCoordinates()); + self::assertSame(30, $drawing->getOffsetX()); + self::assertSame(10, $drawing->getOffsetY()); + self::assertSame('E8', $drawing->getCoordinates2()); + self::assertSame(-50, $drawing->getOffsetX2()); + self::assertSame(-20, $drawing->getOffsetY2()); + self::assertSame($rsheet, $drawing->getWorksheet()); + self::assertSame($expectedResult, $drawing->getEditAs()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function providerEditAs(): array + { + return [ + 'absolute' => ['absolute'], + 'onecell' => ['onecell'], + 'twocell' => ['twocell'], + 'unset (will be treated as twocell)' => [''], + 'unknown (will be treated as twocell)' => ['unknown', ''], + ]; + } + + public function testMemoryDrawingDuplicateResource(): void + { + $gdImage = imagecreatetruecolor(120, 20); + $textColor = ($gdImage === false) ? false : imagecolorallocate($gdImage, 255, 255, 255); + if ($gdImage === false || $textColor === false) { + self::fail('imagecreatetruecolor or imagecolorallocate failed'); + } else { + $spreadsheet = new Spreadsheet(); + $aSheet = $spreadsheet->getActiveSheet(); + imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); + $listOfModes = [ + BaseDrawing::EDIT_AS_TWOCELL, + BaseDrawing::EDIT_AS_ABSOLUTE, + BaseDrawing::EDIT_AS_ONECELL, + ]; + + foreach ($listOfModes as $i => $mode) { + $drawing = new MemoryDrawing(); + $drawing->setName('In-Memory image ' . $i); + $drawing->setDescription('In-Memory image ' . $i); + + $drawing->setCoordinates('A' . ((4 * $i) + 1)); + $drawing->setCoordinates2('D' . ((4 * $i) + 4)); + $drawing->setEditAs($mode); + + $drawing->setImageResource($gdImage); + $drawing->setRenderingFunction( + MemoryDrawing::RENDERING_JPEG + ); + + $drawing->setMimeType(MemoryDrawing::MIMETYPE_DEFAULT); + + $drawing->setWorksheet($aSheet); + } + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $index => $pDrawing) { + self::assertEquals($listOfModes[$index], $pDrawing->getEditAs(), 'functional test drawing twoCellAnchor'); + } + $reloadedSpreadsheet->disconnectWorksheets(); + } } } diff --git a/tests/data/Calculation/Information/ERROR_TYPE.php b/tests/data/Calculation/Information/ERROR_TYPE.php index b08d69a6..45f27970 100644 --- a/tests/data/Calculation/Information/ERROR_TYPE.php +++ b/tests/data/Calculation/Information/ERROR_TYPE.php @@ -53,4 +53,12 @@ return [ 7, '#N/A', ], + [ + 9, + '#SPILL!', + ], + [ + 14, + '#CALC!', + ], ]; diff --git a/tests/data/Calculation/Information/IS_ERROR.php b/tests/data/Calculation/Information/IS_ERROR.php index 0755fd81..24e864fe 100644 --- a/tests/data/Calculation/Information/IS_ERROR.php +++ b/tests/data/Calculation/Information/IS_ERROR.php @@ -49,6 +49,14 @@ return [ true, '#N/A', ], + [ + true, + '#SPILL!', + ], + [ + true, + '#CALC!', + ], [ false, 'TRUE', diff --git a/tests/data/Calculation/LookupRef/INDIRECT.php b/tests/data/Calculation/LookupRef/INDIRECT.php index b8519657..7dc2515a 100644 --- a/tests/data/Calculation/LookupRef/INDIRECT.php +++ b/tests/data/Calculation/LookupRef/INDIRECT.php @@ -34,4 +34,8 @@ return [ 'supply a1 argument as int' => [900, 'A2:A4', 1], 'supply a1 argument as float' => [900, 'A2:A4', 7.3], 'supply a1 argument as string not permitted' => ['#VALUE!', 'A2:A4', '1'], + 'row range' => [600, '1:3'], + 'column range' => [1500, 'A:C'], + 'row range on different sheet' => [66, 'OtherSheet!1:3'], + 'column range on different sheet' => [165, 'OtherSheet!A:C'], ]; diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index ac60bda3..2162d49a 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -24,6 +24,13 @@ return [ 2, false, ], + [ + '#REF!', + 1, + 'HELLO WORLD', + 2, + false, + ], [ 100, 1, diff --git a/tests/data/Reader/XLS/CF_Basic_Comparisons.xls b/tests/data/Reader/XLS/CF_Basic_Comparisons.xls new file mode 100644 index 00000000..10c91be6 Binary files /dev/null and b/tests/data/Reader/XLS/CF_Basic_Comparisons.xls differ diff --git a/tests/data/Reader/XLS/CF_Expression_Comparisons.xls b/tests/data/Reader/XLS/CF_Expression_Comparisons.xls new file mode 100644 index 00000000..58273cae Binary files /dev/null and b/tests/data/Reader/XLS/CF_Expression_Comparisons.xls differ diff --git a/tests/data/Reader/XLS/DataValidation.xls b/tests/data/Reader/XLS/DataValidation.xls new file mode 100644 index 00000000..44e9d1d3 Binary files /dev/null and b/tests/data/Reader/XLS/DataValidation.xls differ diff --git a/tests/data/ReferenceHelperFormulaUpdates.php b/tests/data/ReferenceHelperFormulaUpdates.php index d0835603..ef563f31 100644 --- a/tests/data/ReferenceHelperFormulaUpdates.php +++ b/tests/data/ReferenceHelperFormulaUpdates.php @@ -22,6 +22,34 @@ return [ '2020', '=SUM(A1:C3)', ], + 'column range' => [ + '=SUM(B:C)', + 2, + 0, + '2020', + '=SUM(D:E)', + ], + 'column range with absolute' => [ + '=SUM($B:C)', + 2, + 0, + '2020', + '=SUM($B:E)', + ], + 'row range' => [ + '=SUM(2:3)', + 0, + 2, + '2020', + '=SUM(4:5)', + ], + 'row range with absolute' => [ + '=SUM($2:3)', + 0, + 2, + '2020', + '=SUM($2:5)', + ], [ '=SUM(2020!C3:E5,2019!C3:E5)', -2,