From abc1d3db70c51d59ad60ae493bd58e7985392598 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 07:46:32 -0700 Subject: [PATCH 01/69] Big Memory Leak in One Test (#2958) * Big Memory Leak in One Test Many tests leak a little by not issuing disconnectWorksheets at the end. CellMatcherTest leaks a lot by opening a spreadsheet many times. Phpunit reported a high watermark of 390MB; fixing CellMatcherTest brings that figure down to 242MB. I have also changed it to use a formal assertion in many cases where markTestSkipped was (IMO inappropriately) used. * Another Leak Issue2301Test lacks a disconnect. Adding one reduces HWM from 242MB to 224. --- .../Reader/Xlsx/Issue2301Test.php | 2 + .../ConditionalFormatting/CellMatcherTest.php | 105 +++++++----------- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php index 4484b9e0..7a5829f7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php @@ -24,5 +24,7 @@ class Issue2301Test extends \PHPUnit\Framework\TestCase self::assertSame('Arial CE', $font->getName()); self::assertSame(9.0, $font->getSize()); self::assertSame('protected', $sheet->getCell('BT10')->getStyle()->getProtection()->getHidden()); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); } } diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 3c96403b..2c6d0da8 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -10,15 +10,24 @@ use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase { /** - * @var Spreadsheet + * @var ?Spreadsheet */ protected $spreadsheet; - protected function setUp(): void + protected function loadSpreadsheet(): Spreadsheet { $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); - $this->spreadsheet = $reader->load($filename); + + return $reader->load($filename); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } /** @@ -26,16 +35,13 @@ class CellMatcherTest extends TestCase */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "{$cellAddress} is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -82,16 +88,13 @@ class CellMatcherTest extends TestCase */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -128,16 +131,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -167,16 +167,13 @@ class CellMatcherTest extends TestCase */ public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -216,16 +213,13 @@ class CellMatcherTest extends TestCase */ public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -329,16 +323,13 @@ class CellMatcherTest extends TestCase */ public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -365,16 +356,13 @@ class CellMatcherTest extends TestCase */ public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -400,16 +388,13 @@ class CellMatcherTest extends TestCase */ public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -447,16 +432,13 @@ class CellMatcherTest extends TestCase */ public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::AssertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -486,16 +468,13 @@ class CellMatcherTest extends TestCase */ public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { + $this->spreadsheet = $this->loadSpreadsheet(); $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + self::assertNotNull($worksheet, "$sheetname not found in test workbook"); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); From b8456105dd5ea44210b5f33dcac3ab732af4444d Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 23 Jul 2022 08:06:13 -0700 Subject: [PATCH 02/69] Charts - Gradients, Transparency, Hidden Axes (#2950) Fix #2257. Fix #2929. Fix #2935 (probably in a way that will not satisfy the requester). 2257 and 2929 requested changes that ultimately affect the same section of code, so it's appropriate to deal with them together. 2257 requests the ability to make the chart background transparent (so that the Excel gridlines are visible beneath the chart), and the ability to hide an Axis. 2929 requests the ability to set a gradient background on the chart. --- samples/Chart/33_Chart_create_scatter3.php | 192 ++++++++++++++++++ samples/Chart/33_Chart_create_scatter4.php | 130 ++++++++++++ .../templates/32readwriteScatterChart10.xlsx | Bin 0 -> 12363 bytes .../templates/32readwriteScatterChart9.xlsx | Bin 0 -> 12554 bytes src/PhpSpreadsheet/Chart/Axis.php | 5 +- src/PhpSpreadsheet/Chart/Chart.php | 15 ++ src/PhpSpreadsheet/Chart/ChartColor.php | 32 ++- src/PhpSpreadsheet/Chart/DataSeries.php | 2 +- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 2 +- src/PhpSpreadsheet/Chart/PlotArea.php | 62 ++++++ src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 51 +++++ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 56 ++++- .../Chart/Charts32ScatterTest.php | 85 ++++++++ 13 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 samples/Chart/33_Chart_create_scatter3.php create mode 100644 samples/Chart/33_Chart_create_scatter4.php create mode 100644 samples/templates/32readwriteScatterChart10.xlsx create mode 100644 samples/templates/32readwriteScatterChart9.xlsx diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php new file mode 100644 index 00000000..b4fc97b1 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -0,0 +1,192 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + + // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); +$xAxis->setAxisOption('hidden', '1'); + +$yAxis = new Axis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$yAxis->setAxisOption('hidden', '1'); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setNoFill(true); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend Chart'); +//$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null, //$yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); +$chart->setNoFill(true); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('P20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_scatter4.php b/samples/Chart/33_Chart_create_scatter4.php new file mode 100644 index 00000000..d42933b6 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter4.php @@ -0,0 +1,130 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 +]; +// Set the X-Axis Labels +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), +]; + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); + +$pos1 = 0; // pos = 0% (extreme low side or lower left corner) +$brightness1 = 0; // 0% +$gsColor1 = new ChartColor(); +$gsColor1->setColorProperties('FF0000', 75, 'srgbClr', $brightness1); // red +$gradientStop1 = [$pos1, $gsColor1]; + +$pos2 = 0.5; // pos = 50% (middle) +$brightness2 = 0.5; // 50% +$gsColor2 = new ChartColor(); +$gsColor2->setColorProperties('FFFF00', 50, 'srgbClr', $brightness2); // yellow +$gradientStop2 = [$pos2, $gsColor2]; + +$pos3 = 1.0; // pos = 100% (extreme high side or upper right corner) +$brightness3 = 0.5; // 50% +$gsColor3 = new ChartColor(); +$gsColor3->setColorProperties('00B050', 50, 'srgbClr', $brightness3); // green +$gradientStop3 = [$pos3, $gsColor3]; + +$gradientFillStops = [ + $gradientStop1, + $gradientStop2, + $gradientStop3, +]; +$gradientFillAngle = 315.0; // 45deg above horiz + +$plotArea->setGradientFillProperties($gradientFillStops, $gradientFillAngle); + +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/templates/32readwriteScatterChart10.xlsx b/samples/templates/32readwriteScatterChart10.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7d5ac0d280f328c2685e6de590a78a2e30a9cb00 GIT binary patch literal 12363 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 PL;~ysi6~3|^RNF0pR-I- literal 0 HcmV?d00001 diff --git a/samples/templates/32readwriteScatterChart9.xlsx b/samples/templates/32readwriteScatterChart9.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..3a0b51b2f85429405f5d07484620113e44ffb455 GIT binary patch literal 12554 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

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

\n"; + $html .= "
" . PHP_EOL; } else { - $html .= "
\n"; + $html .= "
" . PHP_EOL; } $this->generateTableTag($worksheet, $id, $html, $sheetIndex); @@ -1457,6 +1458,10 @@ class Html extends BaseWriter // Sheet index $sheetIndex = $worksheet->getParent()->getIndex($worksheet); $html = $this->generateRowStart($worksheet, $sheetIndex, $row); + $generateDiv = $this->isMPdf && $worksheet->getRowDimension($row + 1)->getVisible() === false; + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } // Write cells $colNum = 0; @@ -1504,6 +1509,9 @@ class Html extends BaseWriter } // Write row end + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } $html .= ' ' . PHP_EOL; // Return @@ -1834,26 +1842,26 @@ class Html extends BaseWriter } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { $htmlPage .= 'size: portrait; '; } - $htmlPage .= "}\n"; + $htmlPage .= '}' . PHP_EOL; ++$sheetId; } - $htmlPage .= <<div {margin-top: 5px;} - body>div:first-child {margin-top: 0;} - .scrpgbrk {margin-top: 1px;} -} -@media print { - .gridlinesp td {border: 1px solid black;} - .gridlinesp th {border: 1px solid black;} - .navigation {display: none;} -} - -EOF; + $htmlPage .= implode(PHP_EOL, [ + '.navigation {page-break-after: always;}', + '.scrpgbrk, div + div {page-break-before: always;}', + '@media screen {', + ' .gridlines td {border: 1px solid black;}', + ' .gridlines th {border: 1px solid black;}', + ' body>div {margin-top: 5px;}', + ' body>div:first-child {margin-top: 0;}', + ' .scrpgbrk {margin-top: 1px;}', + '}', + '@media print {', + ' .gridlinesp td {border: 1px solid black;}', + ' .gridlinesp th {border: 1px solid black;}', + ' .navigation {display: none;}', + '}', + '', + ]); $htmlPage .= $generateSurroundingHTML ? ('' . PHP_EOL) : ''; return $htmlPage; diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index bf9e28cb..cd17cccf 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -7,6 +7,13 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Dompdf extends Pdf { + /** + * embed images, or link to images. + * + * @var bool + */ + protected $embedImages = true; + /** * Gets the implementation of external PDF library that should be used. * From bb072d1ca7529b70d98f1015fe23df4b70ab5419 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 14 Aug 2022 10:57:34 -0700 Subject: [PATCH 26/69] Upgrade Dev TCPDF to 6.5 (#3006) * Upgrade Dev TCPDF to 6.5 Implementation of https://github.com/tecnickcom/TCPDF/pull/467, which is available in just-released Tcpdf 6.5, will improve look of Tcpdf rendering for PhpSpreadsheet. Fix #1164. One test had been suppressed for Tcpdf, ostensibly because it was not compatible with Php8. As it turns out, the PhpSpreadsheet code which invokes Tcpdf was (harmlessly) incorrect, so the Php8 issue was actually with PhpSpreadsheet, not Tcpdf. That code is corrected, and the test is no longer suppressed. * Update Change Log Pick up some earlier changes as well as this one, and deprecations which had been omitted from the 1.24 change log. --- CHANGELOG.md | 16 +++++++++++++++- composer.json | 4 ++-- composer.lock | 16 ++++++++++------ src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 2 +- .../Functional/StreamTest.php | 8 +------- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00167722..4e19cb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Implementation of the `ARRAYTOTEXT()` Excel Function - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines. ### Changed @@ -20,7 +21,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Deprecated -- Nothing +- Axis getLineProperty deprecated in favor of getLineColorProperty. +- Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated. +- Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated. +- ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed). +- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT. +- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead. ### Removed @@ -30,6 +36,14 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) +- Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) +- Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960) +- Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971) +- Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950) +- Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976) +- Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) +- Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) +- Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) ## 1.24.1 - 2022-07-18 diff --git a/composer.json b/composer.json index 4ef1c1b4..cda2dc96 100644 --- a/composer.json +++ b/composer.json @@ -88,13 +88,13 @@ "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.4" + "tecnickcom/tcpdf": "6.5" }, "suggest": { "ext-intl": "PHP Internationalization Functions", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "dompdf/dompdf": "Option for rendering PDF with PDF Writer", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet fully support PHP8)", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer", "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" }, "autoload": { diff --git a/composer.lock b/composer.lock index bbbd0c75..f54b9c58 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fc6928651785d4bb82d727d22f50227d", + "content-hash": "05bd955232ea7ceab5b849e990f593bd", "packages": [ { "name": "ezyang/htmlpurifier", @@ -5084,16 +5084,16 @@ }, { "name": "tecnickcom/tcpdf", - "version": "6.4.4", + "version": "6.5.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "42cd0f9786af7e5db4fcedaa66f717b0d0032320" + "reference": "cc54c1503685e618b23922f53635f46e87653662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/42cd0f9786af7e5db4fcedaa66f717b0d0032320", - "reference": "42cd0f9786af7e5db4fcedaa66f717b0d0032320", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/cc54c1503685e618b23922f53635f46e87653662", + "reference": "cc54c1503685e618b23922f53635f46e87653662", "shasum": "" }, "require": { @@ -5142,13 +5142,17 @@ "pdf417", "qrcode" ], + "support": { + "issues": "https://github.com/tecnickcom/TCPDF/issues", + "source": "https://github.com/tecnickcom/TCPDF/tree/6.5.0" + }, "funding": [ { "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", "type": "custom" } ], - "time": "2021-12-31T08:39:24+00:00" + "time": "2022-08-12T07:50:54+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index d29d4764..aefc6b56 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -77,7 +77,7 @@ class Tcpdf extends Pdf $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); // Write to file - fwrite($fileHandle, $pdf->output($filename, 'S')); + fwrite($fileHandle, $pdf->output('', 'S')); parent::restoreStateAfterSave(); } diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php index a84a2490..05b87ab9 100644 --- a/tests/PhpSpreadsheetTests/Functional/StreamTest.php +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -18,15 +18,9 @@ class StreamTest extends TestCase ['Html'], ['Mpdf'], ['Dompdf'], + ['Tcpdf'], ]; - if (\PHP_VERSION_ID < 80000) { - $providerFormats = array_merge( - $providerFormats, - [['Tcpdf']] - ); - } - return $providerFormats; } From cd6629890162f447edd2c343c5b45fe0dfa8f8fe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 14 Aug 2022 23:43:52 +0200 Subject: [PATCH 27/69] Adjust `extractAllCellReferencesInRange()` method to allow a worksheet in the reference --- src/PhpSpreadsheet/Cell/Coordinate.php | 21 +++++++++++- src/PhpSpreadsheet/Worksheet/Worksheet.php | 23 +++++++++---- testing/cellReferenceTest.php | 28 ++++++++++++++++ .../CellExtractAllCellReferencesInRange.php | 33 +++++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 testing/cellReferenceTest.php diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 50678397..2fca6212 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -349,6 +350,19 @@ abstract class Coordinate */ public static function extractAllCellReferencesInRange($cellRange): array { + if (substr_count($cellRange, '!') > 1) { + throw new Exception('3-D Range References are not supported'); + } + + [$worksheet, $cellRange] = Worksheet::extractSheetTitle($cellRange, true); + $quoted = ''; + if ($worksheet > '') { + $quoted = Worksheet::nameRequiresQuotes($worksheet) ? "'" : ''; + if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") { + $worksheet = substr($worksheet, 1, -1); + } + $worksheet = str_replace("'", "''", $worksheet); + } [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange); $cells = []; @@ -364,7 +378,12 @@ abstract class Coordinate $cellList = array_merge(...$cells); - return self::sortCellReferenceArray($cellList); + return array_map( + function ($cellAddress) use ($worksheet, $quoted) { + return ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress; + }, + self::sortCellReferenceArray($cellList) + ); } private static function processRangeSetOperators(array $operators, array $cells): array diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index e89043c8..3464a321 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -32,14 +32,16 @@ use PhpOffice\PhpSpreadsheet\Style\Style; class Worksheet implements IComparable { // Break types - const BREAK_NONE = 0; - const BREAK_ROW = 1; - const BREAK_COLUMN = 2; + public const BREAK_NONE = 0; + public const BREAK_ROW = 1; + public const BREAK_COLUMN = 2; // Sheet state - const SHEETSTATE_VISIBLE = 'visible'; - const SHEETSTATE_HIDDEN = 'hidden'; - const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const SHEETSTATE_VISIBLE = 'visible'; + public const SHEETSTATE_HIDDEN = 'hidden'; + public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** * Maximum 31 characters allowed for sheet title. @@ -3051,7 +3053,11 @@ class Worksheet implements IComparable * Extract worksheet title from range. * * Example: extractSheetTitle("testSheet!A1") ==> 'A1' + * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3' * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1']; + * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3']; + * Example: extractSheetTitle("A1", true) ==> ['', 'A1']; + * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3'] * * @param string $range Range to extract title from * @param bool $returnRange Return range? (see example) @@ -3450,4 +3456,9 @@ class Worksheet implements IComparable { return $this->codeName !== null; } + + public static function nameRequiresQuotes(string $sheetName): bool + { + return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1; + } } diff --git a/testing/cellReferenceTest.php b/testing/cellReferenceTest.php new file mode 100644 index 00000000..c8d1659c --- /dev/null +++ b/testing/cellReferenceTest.php @@ -0,0 +1,28 @@ + Date: Mon, 15 Aug 2022 06:33:31 +0200 Subject: [PATCH 28/69] Fix phpstan baseline --- phpstan-baseline.neon | 5 ----- 1 file changed, 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 596e1e10..c6d769d5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1060,11 +1060,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/TextData/Text.php - - - message: "#^Elseif branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\:\\:getFormulaAttributes\\(\\) has no return type specified\\.$#" count: 1 From d7da20610323508de897e3640445e8f8b842c396 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 15 Aug 2022 07:47:27 +0200 Subject: [PATCH 29/69] Remove file accidentally committed --- testing/cellReferenceTest.php | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 testing/cellReferenceTest.php diff --git a/testing/cellReferenceTest.php b/testing/cellReferenceTest.php deleted file mode 100644 index c8d1659c..00000000 --- a/testing/cellReferenceTest.php +++ /dev/null @@ -1,28 +0,0 @@ - Date: Fri, 19 Aug 2022 11:28:51 +0200 Subject: [PATCH 30/69] Bugfix for Issue #3013, Named ranges not usable as anchors in OFFSET function --- src/PhpSpreadsheet/Calculation/Functions.php | 2 +- .../Calculation/LookupRef/Offset.php | 11 ++++++ .../Functions/LookupRef/HLookupTest.php | 39 +++++++++++++++++++ .../Functions/LookupRef/OffsetTest.php | 15 +++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index b3d6998c..172f2022 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -687,7 +687,7 @@ class Functions $worksheet2 = $defined->getWorkSheet(); if (!$defined->isFormula() && $worksheet2 !== null) { $coordinate = "'" . $worksheet2->getTitle() . "'!" . - (string) preg_replace('/^=/', '', $defined->getValue()); + (string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue())); } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index 9f3377f6..02a25581 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -99,6 +99,8 @@ class Offset private static function extractWorksheet($cellAddress, Cell $cell): array { + $cellAddress = self::assessCellAddress($cellAddress, $cell); + $sheetName = ''; if (strpos($cellAddress, '!') !== false) { [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); @@ -112,6 +114,15 @@ class Offset return [$cellAddress, $worksheet]; } + private static function assessCellAddress(string $cellAddress, Cell $cell): string + { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $cellAddress) !== false) { + $cellAddress = Functions::expandDefinedName($cellAddress, $cell); + } + + return $cellAddress; + } + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns) { $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php index 0ce3513b..084e3d9f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -91,6 +92,44 @@ class HLookupTest extends TestCase self::assertSame($expectedResult, $result); } + /** + * @dataProvider providerHLookupNamedRange + */ + public function testHLookupNamedRange(string $expectedResult, string $cellAddress): void + { + $lookupData = [ + ['Rating', 1, 2, 3, 4], + ['Level', 'Poor', 'Average', 'Good', 'Excellent'], + ]; + $formData = [ + ['Category', 'Rating', 'Level'], + ['Service', 2, '=HLOOKUP(C5,Lookup_Table,2,FALSE)'], + ['Quality', 3, '=HLOOKUP(C6,Lookup_Table,2,FALSE)'], + ['Value', 4, '=HLOOKUP(C7,Lookup_Table,2,FALSE)'], + ['Cleanliness', 3, '=HLOOKUP(C8,Lookup_Table,2,FALSE)'], + ]; + + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($lookupData, null, 'F4'); + $worksheet->fromArray($formData, null, 'B4'); + + $spreadsheet->addNamedRange(new NamedRange('Lookup_Table', $worksheet, '=$G$4:$J$5')); + + $result = $worksheet->getCell($cellAddress)->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerHLookupNamedRange(): array + { + return [ + ['Average', 'D5'], + ['Good', 'D6'], + ['Excellent', 'D7'], + ['Good', 'D8'], + ]; + } + /** * @dataProvider providerHLookupArray */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php index e0786505..22787e25 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\NamedRange; class OffsetTest extends AllSetupTeardown { @@ -46,4 +47,18 @@ class OffsetTest extends AllSetupTeardown $sheet->getCell('A5')->setValue('=OFFSET(C1, 0, 0)'); self::assertSame(5, $sheet->getCell('A5')->getCalculatedValue()); } + + public function testOffsetNamedRange(): void + { + $workSheet = $this->getSheet(); + $workSheet->setCellValue('A1', 1); + $workSheet->setCellValue('A2', 2); + + $this->getSpreadsheet()->addNamedRange(new NamedRange('demo', $workSheet, '=$A$1')); + + $workSheet->setCellValue('B1', '=demo'); + $workSheet->setCellValue('B2', '=OFFSET(demo, 1, 0)'); + + self::assertSame(2, $workSheet->getCell('B2')->getCalculatedValue()); + } } From 4e82b55f3718ced5bf8fc8b12e789482c741529a Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 19 Aug 2022 14:26:39 +0200 Subject: [PATCH 31/69] Update Change Log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e19cb2d..62fdaf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) - Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) From e67de6f300b687721b3359cdb6b0e421b26f5bdc Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 20 Aug 2022 21:27:05 +0200 Subject: [PATCH 32/69] Support for SimpleCache Interface versions 1.0, 2.0 and 3.0; to stop people moaning; even though it requires a second implementation of the Memory cache for Cells --- CHANGELOG.md | 1 + composer.json | 2 +- phpstan-baseline.neon | 5 - src/PhpSpreadsheet/Collection/Cells.php | 4 +- .../{Memory.php => Memory/SimpleCache1.php} | 7 +- .../Collection/Memory/SimpleCache3.php | 109 ++++++++++++++++++ .../Reader/Security/XmlScanner.php | 2 +- src/PhpSpreadsheet/Settings.php | 10 +- .../Collection/CellsTest.php | 10 +- 9 files changed, 137 insertions(+), 13 deletions(-) rename src/PhpSpreadsheet/Collection/{Memory.php => Memory/SimpleCache1.php} (93%) create mode 100644 src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 62fdaf91..da29bc8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0 - Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) ### Changed diff --git a/composer.json b/composer.json index cda2dc96..46ee56b9 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0 || ^2.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c6d769d5..7c0070d7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1100,11 +1100,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Cell/Coordinate.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Memory\\:\\:\\$cache has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Collection/Memory.php - - message: "#^Parameter \\#1 \\$namedRange of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:addNamedRange\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\NamedRange, \\$this\\(PhpOffice\\\\PhpSpreadsheet\\\\DefinedName\\) given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 03ad1cd4..82c9ae1a 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -268,7 +268,9 @@ class Cells */ private function getUniqueID() { - return Settings::getCache() instanceof Memory + $cacheType = Settings::getCache(); + + return ($cacheType instanceof Memory\SimpleCache1 || $cacheType instanceof Memory\SimpleCache3) ? random_bytes(7) . ':' : uniqid('phpspreadsheet.', true) . '.'; } diff --git a/src/PhpSpreadsheet/Collection/Memory.php b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php similarity index 93% rename from src/PhpSpreadsheet/Collection/Memory.php rename to src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php index 2690ab7d..a0eb6ec2 100644 --- a/src/PhpSpreadsheet/Collection/Memory.php +++ b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php @@ -1,6 +1,6 @@ cache = []; + + return true; + } + + /** + * @param string $key + */ + public function delete($key): bool + { + unset($this->cache[$key]); + + return true; + } + + /** + * @param iterable $keys + */ + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + /** + * @param string $key + * @param mixed $default + */ + public function get($key, $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + /** + * @param iterable $keys + * @param mixed $default + */ + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + /** + * @param string $key + */ + public function has($key): bool + { + return array_key_exists($key, $this->cache); + } + + /** + * @param string $key + * @param mixed $value + * @param null|DateInterval|int $ttl + */ + public function set($key, $value, $ttl = null): bool + { + $this->cache[$key] = $value; + + return true; + } + + /** + * @param iterable $values + * @param null|DateInterval|int $ttl + */ + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index 8155b838..40008d01 100644 --- a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -52,7 +52,7 @@ class XmlScanner public static function threadSafeLibxmlDisableEntityLoaderAvailability() { - if (PHP_MAJOR_VERSION == 7) { + if (PHP_MAJOR_VERSION === 7) { switch (PHP_MINOR_VERSION) { case 2: return PHP_RELEASE_VERSION >= 1; diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 5fbbadb6..3282a596 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Memory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\CacheInterface; +use ReflectionClass; class Settings { @@ -161,12 +162,19 @@ class Settings public static function getCache(): CacheInterface { if (!self::$cache) { - self::$cache = new Memory(); + self::$cache = self::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(); } return self::$cache; } + public static function useSimpleCacheVersion3(): bool + { + return + PHP_MAJOR_VERSION === 8 && + (new ReflectionClass(CacheInterface::class))->getMethod('get')->getReturnType() !== null; + } + /** * Set the HTTP client implementation to be used for network request. */ diff --git a/tests/PhpSpreadsheetTests/Collection/CellsTest.php b/tests/PhpSpreadsheetTests/Collection/CellsTest.php index 5731d581..9e662b92 100644 --- a/tests/PhpSpreadsheetTests/Collection/CellsTest.php +++ b/tests/PhpSpreadsheetTests/Collection/CellsTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Collection; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Collection\Memory; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -107,7 +108,10 @@ class CellsTest extends TestCase $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); $collection = $this->getMockBuilder(Cells::class) - ->setConstructorArgs([new Worksheet(), new Memory()]) + ->setConstructorArgs([ + new Worksheet(), + Settings::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(), + ]) ->onlyMethods(['has']) ->getMock(); @@ -121,7 +125,9 @@ class CellsTest extends TestCase { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $cache = $this->createMock(Memory::class); + $cache = $this->createMock( + Settings::useSimpleCacheVersion3() ? Memory\SimpleCache3::class : Memory\SimpleCache1::class + ); $cell = $this->createMock(Cell::class); $cache->method('set') ->willReturn(false); From d55978cf931ced2bc9f0d982bb6b8269fbace1f9 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 20 Aug 2022 19:58:43 -0700 Subject: [PATCH 33/69] Correct Namespaces in 11 Tests (#3020) The setup for unit testing in Github in the "Install dependencies" log reports 11 members as "does not comply with with psr-4 autoloading standard." In each case, it is because the test namespace does not match the directory; in most cases, it was caused by the member being moved from one directory to another without changing the namespace declaration. No harm results from these problems, but there's also no reason to not correct them. --- tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php | 2 +- tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php | 2 +- tests/PhpSpreadsheetTests/Chart/PieFillTest.php | 2 +- tests/PhpSpreadsheetTests/Chart/RenderTest.php | 2 +- .../PhpSpreadsheetTests/Reader/Xlsx/NamespaceIssue2109bTest.php | 2 +- tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php | 2 +- .../PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php | 2 +- tests/PhpSpreadsheetTests/Reader/Xlsx/NamespacePurlTest.php | 2 +- tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php | 2 +- .../Style/ConditionalFormatting/Wizard/DateValueWizardTest.php | 2 +- tests/PhpSpreadsheetTests/Writer/Mpdf/ImageCopyPdfTest.php | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php index f8f02f0e..3edd22c8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php @@ -1,6 +1,6 @@ Date: Wed, 24 Aug 2022 08:33:57 +0200 Subject: [PATCH 34/69] Minor changes, mainly cosmetic --- composer.lock | 1014 ++++++++--------- samples/Calculations/LookupRef/VLOOKUP.php | 27 +- samples/Chart/33_Chart_create_scatter2.php | 2 +- samples/Chart/33_Chart_create_scatter3.php | 2 +- .../33_Chart_create_scatter5_trendlines.php | 2 +- .../Calculation/Calculation.php | 8 +- .../Calculation/Engine/Logger.php | 6 +- .../Calculation/MathTrig/Random.php | 2 +- .../Calculation/Statistical/Averages.php | 2 +- .../Statistical/Distributions/ChiSquared.php | 2 +- .../Chart/Renderer/JpGraphRendererBase.php | 18 +- src/PhpSpreadsheet/Reader/Xls.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 7 +- .../Shared/JAMA/EigenvalueDecomposition.php | 6 +- src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 14 +- .../JAMA/SingularValueDecomposition.php | 6 +- src/PhpSpreadsheet/Writer/Xls/Parser.php | 2 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 2 +- 18 files changed, 560 insertions(+), 564 deletions(-) diff --git a/composer.lock b/composer.lock index f54b9c58..37e2dfa8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "05bd955232ea7ceab5b849e990f593bd", + "content-hash": "dd19bb54ddc39f5b24f564818cb46c7e", "packages": [ { "name": "ezyang/htmlpurifier", @@ -51,33 +51,39 @@ "keywords": [ "html" ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + }, "time": "2021-12-25T01:21:49+00:00" }, { "name": "maennchen/zipstream-php", - "version": "2.1.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", - "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/211e9ba1530ea5260b45d90c9ea252f56ec52729", + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729", "shasum": "" }, "require": { "myclabs/php-enum": "^1.5", - "php": ">= 7.1", + "php": "^7.4 || ^8.0", "psr/http-message": "^1.0", "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { "ext-zip": "*", - "guzzlehttp/guzzle": ">= 6.3", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": ">= 7.5" + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^4.1" }, "type": "library", "autoload": { @@ -112,13 +118,17 @@ "stream", "zip" ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.1" + }, "funding": [ { "url": "https://opencollective.com/zipstream", "type": "open_collective" } ], - "time": "2020-05-30T13:11:16+00:00" + "time": "2022-05-18T15:52:06+00:00" }, { "name": "markbaker/complex", @@ -165,6 +175,10 @@ "complex", "mathematics" ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.1" + }, "time": "2021-06-29T15:32:53+00:00" }, { @@ -217,20 +231,24 @@ "matrix", "vector" ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" + }, "time": "2021-07-01T19:01:15+00:00" }, { "name": "myclabs/php-enum", - "version": "1.8.3", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/myclabs/php-enum.git", - "reference": "b942d263c641ddb5190929ff840c68f78713e937" + "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", - "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", + "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", "shasum": "" }, "require": { @@ -246,7 +264,10 @@ "autoload": { "psr-4": { "MyCLabs\\Enum\\": "src/" - } + }, + "classmap": [ + "stubs/Stringable.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -263,6 +284,10 @@ "keywords": [ "enum" ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.4" + }, "funding": [ { "url": "https://github.com/mnapoli", @@ -273,7 +298,7 @@ "type": "tidelift" } ], - "time": "2021-07-05T08:18:36+00:00" + "time": "2022-08-04T09:53:51+00:00" }, { "name": "psr/http-client", @@ -322,6 +347,9 @@ "psr", "psr-18" ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, "time": "2020-06-29T06:28:15+00:00" }, { @@ -374,6 +402,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, "time": "2019-04-30T12:38:16+00:00" }, { @@ -424,6 +455,9 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, "time": "2016-08-06T14:39:51+00:00" }, { @@ -472,32 +506,38 @@ "psr-16", "simple-cache" ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/master" + }, "time": "2017-10-23T01:57:42+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -535,6 +575,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -549,36 +592,36 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [ { "name": "composer/pcre", - "version": "1.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2" + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2", + "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1", + "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -604,6 +647,10 @@ "regex", "regular expression" ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.0.0" + }, "funding": [ { "url": "https://packagist.com", @@ -618,27 +665,27 @@ "type": "tidelift" } ], - "time": "2021-12-06T15:17:27+00:00" + "time": "2022-02-25T20:21:48+00:00" }, { "name": "composer/semver", - "version": "3.2.6", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "83e511e247de329283478496f7a1e114c9517506" + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/83e511e247de329283478496f7a1e114c9517506", - "reference": "83e511e247de329283478496f7a1e114c9517506", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.54", + "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", @@ -680,6 +727,11 @@ "validation", "versioning" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.3.2" + }, "funding": [ { "url": "https://packagist.com", @@ -694,31 +746,31 @@ "type": "tidelift" } ], - "time": "2021-10-25T11:34:17+00:00" + "time": "2022-04-01T19:23:25+00:00" }, { "name": "composer/xdebug-handler", - "version": "2.0.3", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e" + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6555461e76962fd0379c444c46fd558a0fcfb65e", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "composer/pcre": "^1", - "php": "^5.3.2 || ^7.0 || ^8.0", + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "symfony/phpunit-bridge": "^6.0" }, "type": "library", "autoload": { @@ -741,6 +793,11 @@ "Xdebug", "performance" ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + }, "funding": [ { "url": "https://packagist.com", @@ -755,7 +812,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T13:07:32+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -784,6 +841,7 @@ "phpcompatibility/php-compatibility": "^9.0", "yoast/phpunit-polyfills": "^1.0" }, + "default-branch": true, "type": "composer-plugin", "extra": { "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" @@ -829,20 +887,24 @@ "stylecheck", "tests" ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, "time": "2022-07-26T12:51:47+00:00" }, { "name": "doctrine/annotations", - "version": "1.13.2", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + "reference": "648b0343343565c4a056bfc8392201385e8d89f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0", "shasum": "" }, "require": { @@ -854,9 +916,10 @@ "require-dev": { "doctrine/cache": "^1.11 || ^2.0", "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" }, "type": "library", "autoload": { @@ -897,7 +960,11 @@ "docblock", "parser" ], - "time": "2021-08-05T19:00:23+00:00" + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.13.3" + }, + "time": "2022-07-02T10:48:51+00:00" }, { "name": "doctrine/instantiator", @@ -949,6 +1016,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -967,32 +1038,28 @@ }, { "name": "doctrine/lexer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" @@ -1025,6 +1092,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.3" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1039,7 +1110,7 @@ "type": "tidelift" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2022-02-28T11:07:21+00:00" }, { "name": "dompdf/dompdf", @@ -1105,56 +1176,60 @@ ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v2.0.0" + }, "time": "2022-06-21T21:14:57+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.4.0", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad" + "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", + "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", "shasum": "" }, "require": { "composer/semver": "^3.2", - "composer/xdebug-handler": "^2.0", - "doctrine/annotations": "^1.12", + "composer/xdebug-handler": "^3.0.3", + "doctrine/annotations": "^1.13", "ext-json": "*", "ext-tokenizer": "*", - "php": "^7.2.5 || ^8.0", - "php-cs-fixer/diff": "^2.0", - "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", - "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", - "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", - "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.23", - "symfony/process": "^4.4.20 || ^5.0 || ^6.0", - "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php81": "^1.25", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" }, "require-dev": { "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^1.5", - "mikey179/vfsstream": "^1.6.8", + "mikey179/vfsstream": "^1.6.10", "php-coveralls/php-coveralls": "^2.5.2", "php-cs-fixer/accessible-object": "^1.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^8.5.21 || ^9.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "phpunitgoodpractices/polyfill": "^1.5", "phpunitgoodpractices/traits": "^1.9.1", - "symfony/phpunit-bridge": "^5.2.4 || ^6.0", - "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" + "symfony/phpunit-bridge": "^6.0", + "symfony/yaml": "^5.4 || ^6.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1184,26 +1259,30 @@ } ], "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.10.0" + }, "funding": [ { "url": "https://github.com/keradus", "type": "github" } ], - "time": "2021-12-11T16:25:08+00:00" + "time": "2022-08-17T22:13:10+00:00" }, { "name": "masterminds/html5", - "version": "2.7.5", + "version": "2.7.6", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab" + "reference": "897eb517a343a2281f11bc5556d6548db7d93947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f640ac1bdddff06ea333a920c95bbad8872429ab", - "reference": "f640ac1bdddff06ea333a920c95bbad8872429ab", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", + "reference": "897eb517a343a2281f11bc5556d6548db7d93947", "shasum": "" }, "require": { @@ -1255,7 +1334,11 @@ "serializer", "xml" ], - "time": "2021-07-01T14:25:37+00:00" + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + }, + "time": "2022-08-18T16:18:26+00:00" }, { "name": "mitoteam/jpgraph", @@ -1364,6 +1447,11 @@ "php", "utf-8" ], + "support": { + "docs": "http://mpdf.github.io", + "issues": "https://github.com/mpdf/mpdf/issues", + "source": "https://github.com/mpdf/mpdf" + }, "funding": [ { "url": "https://www.paypal.me/mpdf", @@ -1419,6 +1507,10 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", @@ -1477,6 +1569,10 @@ "parser", "php" ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + }, "time": "2022-05-31T20:59:12+00:00" }, { @@ -1522,6 +1618,11 @@ "pseudorandom", "random" ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, "time": "2020-10-15T08:29:30+00:00" }, { @@ -1578,6 +1679,10 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.3" + }, "time": "2021-07-20T11:28:43+00:00" }, { @@ -1625,6 +1730,10 @@ } ], "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, "time": "2022-02-21T01:04:05+00:00" }, { @@ -1665,6 +1774,10 @@ ], "description": "A library to read, parse, export and make subsets of different types of font files.", "homepage": "https://github.com/PhenX/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/0.5.4" + }, "time": "2021-12-17T19:44:54+00:00" }, { @@ -1707,56 +1820,12 @@ ], "description": "A library to read, parse and export to PDF SVG files.", "homepage": "https://github.com/PhenX/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" + }, "time": "2022-03-07T12:52:04+00:00" }, - { - "name": "php-cs-fixer/diff", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "sebastian/diff v3 backport support for PHP 5.6+", - "homepage": "https://github.com/PHP-CS-Fixer", - "keywords": [ - "diff" - ], - "time": "2020-10-14T08:32:19+00:00" - }, { "name": "php-http/message-factory", "version": "v1.0.2", @@ -1805,6 +1874,10 @@ "stream", "uri" ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/master" + }, "time": "2015-12-19T14:08:53+00:00" }, { @@ -1863,219 +1936,12 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, "time": "2019-12-27T09:44:58+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2022-03-15T21:29:03+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpstan/phpstan", "version": "1.8.2", @@ -2111,6 +1977,10 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/1.8.2" + }, "funding": [ { "url": "https://github.com/ondrejmirtes", @@ -2177,27 +2047,31 @@ "MIT" ], "description": "PHPUnit extensions and rules for PHPStan", + "support": { + "issues": "https://github.com/phpstan/phpstan-phpunit/issues", + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.1.1" + }, "time": "2022-04-20T15:24:25+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073", + "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.14", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -2244,13 +2118,17 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2022-08-20T05:26:47+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2300,6 +2178,10 @@ "filesystem", "iterator" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2359,6 +2241,10 @@ "keywords": [ "process" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2414,6 +2300,10 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2469,6 +2359,10 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2479,16 +2373,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.21", + "version": "9.5.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1" + "reference": "888556852e7e9bbeeedb9656afe46118765ade34" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e32b76be457de00e83213528f6bb37e2a38fcb1", - "reference": "0e32b76be457de00e83213528f6bb37e2a38fcb1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34", + "reference": "888556852e7e9bbeeedb9656afe46118765ade34", "shasum": "" }, "require": { @@ -2503,7 +2397,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -2521,9 +2414,6 @@ "sebastian/type": "^3.0", "sebastian/version": "^3.0.2" }, - "require-dev": { - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { "ext-soap": "*", "ext-xdebug": "*" @@ -2563,6 +2453,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23" + }, "funding": [ { "url": "https://phpunit.de/sponsors.html", @@ -2573,7 +2467,7 @@ "type": "github" } ], - "time": "2022-06-19T12:14:25+00:00" + "time": "2022-08-22T14:01:36+00:00" }, { "name": "psr/cache", @@ -2619,24 +2513,27 @@ "psr", "psr-6" ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, "time": "2016-08-06T20:24:11+00:00" }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -2663,7 +2560,11 @@ "container-interop", "psr" ], - "time": "2021-03-05T17:36:06+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/event-dispatcher", @@ -2709,6 +2610,10 @@ "psr", "psr-14" ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, "time": "2019-01-08T18:20:26+00:00" }, { @@ -2756,6 +2661,9 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "time": "2021-05-03T11:20:27+00:00" }, { @@ -2805,6 +2713,10 @@ "parser", "stylesheet" ], + "support": { + "issues": "https://github.com/sabberworm/PHP-CSS-Parser/issues", + "source": "https://github.com/sabberworm/PHP-CSS-Parser/tree/8.4.0" + }, "time": "2021-12-11T13:40:54+00:00" }, { @@ -2851,6 +2763,10 @@ ], "description": "Library for parsing CLI options", "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2903,6 +2819,10 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -2954,6 +2874,10 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3024,6 +2948,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3077,6 +3005,10 @@ ], "description": "Library for calculating the complexity of PHP code units", "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3139,6 +3071,10 @@ "unidiff", "unified diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3198,6 +3134,10 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3271,6 +3211,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3331,6 +3275,10 @@ "keywords": [ "global state" ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3384,6 +3332,10 @@ ], "description": "Library for counting the lines of code in PHP source code", "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3437,6 +3389,10 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3488,6 +3444,10 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3547,6 +3507,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3598,6 +3562,10 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3650,6 +3618,10 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3699,6 +3671,10 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -3767,6 +3743,10 @@ "fpdi", "pdf" ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.3.6" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", @@ -3824,20 +3804,25 @@ "phpcs", "standards" ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, "time": "2022-06-18T07:21:10+00:00" }, { "name": "symfony/console", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" + "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", - "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", + "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", + "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", "shasum": "" }, "require": { @@ -3906,6 +3891,9 @@ "console", "terminal" ], + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3920,20 +3908,20 @@ "type": "tidelift" } ], - "time": "2021-12-20T16:11:12+00:00" + "time": "2022-07-22T10:42:43+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -3970,6 +3958,9 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -3984,20 +3975,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.0", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "27d39ae126352b9fa3be5e196ccf4617897be3eb" + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/27d39ae126352b9fa3be5e196ccf4617897be3eb", - "reference": "27d39ae126352b9fa3be5e196ccf4617897be3eb", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", "shasum": "" }, "require": { @@ -4052,6 +4043,9 @@ ], "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4066,20 +4060,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-05-05T16:45:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a" + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", "shasum": "" }, "require": { @@ -4128,6 +4122,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4142,20 +4139,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.0", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "731f917dc31edcffec2c6a777f3698c33bea8f01" + "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/731f917dc31edcffec2c6a777f3698c33bea8f01", - "reference": "731f917dc31edcffec2c6a777f3698c33bea8f01", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6699fb0228d1bc35b12aed6dd5e7455457609ddd", + "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd", "shasum": "" }, "require": { @@ -4189,6 +4186,9 @@ ], "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4203,20 +4203,20 @@ "type": "tidelift" } ], - "time": "2021-10-28T13:39:27+00:00" + "time": "2022-07-20T13:00:38+00:00" }, { "name": "symfony/finder", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e77046c252be48c48a40816187ed527703c8f76c" + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e77046c252be48c48a40816187ed527703c8f76c", - "reference": "e77046c252be48c48a40816187ed527703c8f76c", + "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", "shasum": "" }, "require": { @@ -4249,6 +4249,9 @@ ], "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4263,20 +4266,20 @@ "type": "tidelift" } ], - "time": "2021-12-15T11:06:13+00:00" + "time": "2022-07-29T07:37:50+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.4.0", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b0fb78576487af19c500aaddb269fd36701d4847" + "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b0fb78576487af19c500aaddb269fd36701d4847", - "reference": "b0fb78576487af19c500aaddb269fd36701d4847", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/54f14e36aa73cb8f7261d7686691fd4d75ea2690", + "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690", "shasum": "" }, "require": { @@ -4315,6 +4318,9 @@ "configuration", "options" ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4329,7 +4335,7 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-07-20T13:00:38+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4394,6 +4400,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4412,16 +4421,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535" + "reference": "433d05519ce6990bf3530fba6957499d327395c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", "shasum": "" }, "require": { @@ -4433,7 +4442,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4472,6 +4481,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4486,20 +4498,20 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -4511,7 +4523,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4553,6 +4565,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4567,20 +4582,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -4589,7 +4604,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4629,6 +4644,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4643,20 +4661,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -4665,7 +4683,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4709,6 +4727,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4723,20 +4744,20 @@ "type": "tidelift" } ], - "time": "2021-07-28T13:41:28+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "e66119f3de95efc359483f810c4c3e6436279436" + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436", - "reference": "e66119f3de95efc359483f810c4c3e6436279436", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", "shasum": "" }, "require": { @@ -4745,7 +4766,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4785,6 +4806,9 @@ "portable", "shim" ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4799,20 +4823,20 @@ "type": "tidelift" } ], - "time": "2021-05-21T13:25:03+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/process", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4" + "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", - "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", + "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", + "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", "shasum": "" }, "require": { @@ -4844,6 +4868,9 @@ ], "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4858,26 +4885,26 @@ "type": "tidelift" } ], - "time": "2021-12-27T21:01:00+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -4924,6 +4951,9 @@ "interoperability", "standards" ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4938,20 +4968,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-05-30T19:17:29+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.4.0", + "version": "v5.4.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "208ef96122bfed82a8f3a61458a07113a08bdcfe" + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/208ef96122bfed82a8f3a61458a07113a08bdcfe", - "reference": "208ef96122bfed82a8f3a61458a07113a08bdcfe", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", "shasum": "" }, "require": { @@ -4983,6 +5013,9 @@ ], "description": "Provides a way to profile code", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v5.4.5" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -4997,20 +5030,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-02-18T16:06:09+00:00" }, { "name": "symfony/string", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d" + "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", - "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", + "url": "https://api.github.com/repos/symfony/string/zipball/5eb661e49ad389e4ae2b6e4df8d783a8a6548322", + "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322", "shasum": "" }, "require": { @@ -5066,6 +5099,9 @@ "utf-8", "utf8" ], + "support": { + "source": "https://github.com/symfony/string/tree/v5.4.11" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -5080,7 +5116,7 @@ "type": "tidelift" } ], - "time": "2021-12-16T21:52:00+00:00" + "time": "2022-07-24T16:15:25+00:00" }, { "name": "tecnickcom/tcpdf", @@ -5192,6 +5228,10 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" + }, "funding": [ { "url": "https://github.com/theseer", @@ -5199,60 +5239,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", - "shasum": "" - }, - "require": { - "ext-ctype": "*", - "php": "^7.2 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], @@ -5279,5 +5265,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.2.0" + "plugin-api-version": "2.3.0" } diff --git a/samples/Calculations/LookupRef/VLOOKUP.php b/samples/Calculations/LookupRef/VLOOKUP.php index 3e7eaa71..16a911e8 100644 --- a/samples/Calculations/LookupRef/VLOOKUP.php +++ b/samples/Calculations/LookupRef/VLOOKUP.php @@ -14,8 +14,8 @@ $worksheet = $spreadsheet->getActiveSheet(); $data = [ ['ID', 'First Name', 'Last Name', 'Salary'], - [72, 'Emily', 'Smith', 64901, null, 'ID', 53, 66, 56], - [66, 'James', 'Anderson', 70855, null, 'Salary'], + [72, 'Emily', 'Smith', 64901], + [66, 'James', 'Anderson', 70855], [14, 'Mia', 'Clark', 188657], [30, 'John', 'Lewis', 97566], [53, 'Jessica', 'Walker', 58339], @@ -25,11 +25,24 @@ $data = [ $worksheet->fromArray($data, null, 'B2'); -$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); -$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); -$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); +$lookupFields = [ + ['ID', 53, 66, 56], + ['Name'], + ['Salary'], +]; + +$worksheet->fromArray($lookupFields, null, 'G3'); + +$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 2, FALSE) & " " & VLOOKUP(H3, B3:E9, 3, FALSE)'); +$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 2, FALSE) & " " & VLOOKUP(I3, B3:E9, 3, FALSE)'); +$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 2, FALSE) & " " & VLOOKUP(J3, B3:E9, 3, FALSE)'); +$worksheet->getCell('H5')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); +$worksheet->getCell('I5')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); +$worksheet->getCell('J5')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); for ($column = 'H'; $column !== 'K'; ++$column) { - $cell = $worksheet->getCell("{$column}4"); - $helper->log("{$column}4: {$cell->getValue()} => {$cell->getCalculatedValue()}"); + for ($row = 4; $row <= 5; ++$row) { + $cell = $worksheet->getCell("{$column}{$row}"); + $helper->log("{$column}{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); + } } diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index eed87119..59310620 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -120,7 +120,7 @@ $dataSeriesValues[2] // triangle border ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected - // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php index b4fc97b1..f6f9a6f4 100644 --- a/samples/Chart/33_Chart_create_scatter3.php +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -120,7 +120,7 @@ $dataSeriesValues[2] // triangle border ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected - // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index a87f6ee1..da831fa9 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -121,7 +121,7 @@ $dataSeriesValues[2] // triangle border ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected - // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers $xAxis = new Axis(); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index cf93a694..d9485ad9 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4756,9 +4756,8 @@ class Calculation break; } - - // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on } elseif (($token === '~') || ($token === '%')) { + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on if (($arg = $stack->pop()) === null) { return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } @@ -4865,9 +4864,8 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $cellValue; } - - // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on if ($pCellParent) { $cell->attach($pCellParent); } @@ -4977,8 +4975,8 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $token; } - // if the token is a named range or formula, evaluate it and push the result onto the stack } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) { + // if the token is a named range or formula, evaluate it and push the result onto the stack $definedName = $matches[6]; if ($cell === null || $pCellWorksheet === null) { return $this->raiseFormulaError("undefined name '$token'"); diff --git a/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/src/PhpSpreadsheet/Calculation/Engine/Logger.php index c6ee5969..256c3eff 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/Logger.php +++ b/src/PhpSpreadsheet/Calculation/Engine/Logger.php @@ -98,9 +98,9 @@ class Logger $cellReference = implode(' -> ', $this->cellStack->showStack()); if ($this->echoDebugLog) { echo $cellReference, - ($this->cellStack->count() > 0 ? ' => ' : ''), - $message, - PHP_EOL; + ($this->cellStack->count() > 0 ? ' => ' : ''), + $message, + PHP_EOL; } $this->debugLog[] = $cellReference . ($this->cellStack->count() > 0 ? ' => ' : '') . diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php index b9fcfc73..22cad2cf 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -17,7 +17,7 @@ class Random */ public static function rand() { - return (mt_rand(0, 10000000)) / 10000000; + return mt_rand(0, 10000000) / 10000000; } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php index 41b011a5..85195c88 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php @@ -203,7 +203,7 @@ class Averages extends AggregateBase $args, function ($value) { // Is it a numeric value? - return (is_numeric($value)) && (!is_string($value)); + return is_numeric($value) && (!is_string($value)); } ); } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 8574d58d..c8743364 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -101,7 +101,7 @@ class ChiSquared return 1 - self::distributionRightTail($value, $degrees); } - return (($value ** (($degrees / 2) - 1) * exp(-$value / 2))) / + return ($value ** (($degrees / 2) - 1) * exp(-$value / 2)) / ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2)); } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index f0ce1f65..d31a55b3 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -729,21 +729,21 @@ abstract class JpGraphRendererBase implements IRenderer switch ($chartType) { case 'area3DChart': $dimensions = '3d'; - // no break + // no break case 'areaChart': $this->renderPlotLine($i, true, true, $dimensions); break; case 'bar3DChart': $dimensions = '3d'; - // no break + // no break case 'barChart': $this->renderPlotBar($i, $dimensions); break; case 'line3DChart': $dimensions = '3d'; - // no break + // no break case 'lineChart': $this->renderPlotLine($i, false, true, $dimensions); @@ -799,35 +799,35 @@ abstract class JpGraphRendererBase implements IRenderer switch ($chartType) { case 'area3DChart': $dimensions = '3d'; - // no break + // no break case 'areaChart': $this->renderAreaChart($groupCount, $dimensions); break; case 'bar3DChart': $dimensions = '3d'; - // no break + // no break case 'barChart': $this->renderBarChart($groupCount, $dimensions); break; case 'line3DChart': $dimensions = '3d'; - // no break + // no break case 'lineChart': $this->renderLineChart($groupCount, $dimensions); break; case 'pie3DChart': $dimensions = '3d'; - // no break + // no break case 'pieChart': $this->renderPieChart($groupCount, $dimensions, false, false); break; case 'doughnut3DChart': $dimensions = '3d'; - // no break + // no break case 'doughnutChart': $this->renderPieChart($groupCount, $dimensions, true, true); @@ -846,7 +846,7 @@ abstract class JpGraphRendererBase implements IRenderer break; case 'surface3DChart': $dimensions = '3d'; - // no break + // no break case 'surfaceChart': $this->renderContourChart($groupCount, $dimensions); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 9f8a3ace..71496ece 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -6999,7 +6999,7 @@ class Xls extends BaseReader } break; - // Unknown cases // don't know how to deal with + // Unknown cases // don't know how to deal with default: throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula'); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 52df94e4..630fd1dd 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -479,7 +479,7 @@ class Xlsx extends BaseReader $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); break; - //Ribbon + // Ribbon case Namespaces::EXTENSIBILITY: $customUI = $relTarget; if ($customUI) { @@ -530,7 +530,7 @@ class Xlsx extends BaseReader } break; - // a vbaProject ? (: some macros) + // a vbaProject ? (: some macros) case Namespaces::VBA: $macros = $ele['Target']; @@ -1695,8 +1695,8 @@ class Xlsx extends BaseReader break; - // unparsed case 'application/vnd.ms-excel.controlproperties+xml': + // unparsed $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; break; @@ -1722,7 +1722,6 @@ class Xlsx extends BaseReader $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { if (is_object($is->r)) { - /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php index 66111b6c..1ec7f6ab 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php @@ -495,7 +495,7 @@ class EigenvalueDecomposition $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; } - // Complex pair + // Complex pair } else { $this->d[$n - 1] = $x + $p; $this->d[$n] = $x + $p; @@ -671,7 +671,7 @@ class EigenvalueDecomposition } else { $this->H[$i][$n] = -$r / ($eps * $norm); } - // Solve real equations + // Solve real equations } else { $x = $this->H[$i][$i + 1]; $y = $this->H[$i + 1][$i]; @@ -693,7 +693,7 @@ class EigenvalueDecomposition } } } - // Complex vector + // Complex vector } elseif ($q < 0) { $l = $n - 1; // Last vector component imaginary so matrix is triangular diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index 0bbd94a7..5e35d491 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -67,21 +67,21 @@ class Matrix $this->A = $args[0]; break; - //Square matrix - n x n + //Square matrix - n x n case 'integer': $this->m = $args[0]; $this->n = $args[0]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n + //Rectangular matrix - m x n case 'integer,integer': $this->m = $args[0]; $this->n = $args[1]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n initialized from packed array + //Rectangular matrix - m x n initialized from packed array case 'array,integer': $this->m = $args[1]; if ($this->m != 0) { @@ -191,7 +191,7 @@ class Matrix return $R; break; - //A($i0...$iF; $j0...$jF) + //A($i0...$iF; $j0...$jF) case 'integer,integer,integer,integer': [$i0, $iF, $j0, $jF] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -214,7 +214,7 @@ class Matrix return $R; break; - //$R = array of row indices; $C = array of column indices + //$R = array of row indices; $C = array of column indices case 'array,array': [$RL, $CL] = $args; if (count($RL) > 0) { @@ -237,7 +237,7 @@ class Matrix return $R; break; - //A($i0...$iF); $CL = array of column indices + //A($i0...$iF); $CL = array of column indices case 'integer,integer,array': [$i0, $iF, $CL] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -260,7 +260,7 @@ class Matrix return $R; break; - //$RL = array of row indices + //$RL = array of row indices case 'array,integer,integer': [$RL, $j0, $jF] = $args; if (count($RL) > 0) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php index 6c8999d0..b809bfa1 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -315,7 +315,7 @@ class SingularValueDecomposition } break; - // Split at negligible s(k). + // Split at negligible s(k). case 2: $f = $e[$k - 1]; $e[$k - 1] = 0.0; @@ -336,7 +336,7 @@ class SingularValueDecomposition } break; - // Perform one qr step. + // Perform one qr step. case 3: // Calculate the shift. $scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k])); @@ -396,7 +396,7 @@ class SingularValueDecomposition $iter = $iter + 1; break; - // Convergence. + // Convergence. case 4: // Make the singular values positive. if ($this->s[$k] <= 0.0) { diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 2f75f908..ca9b67b5 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -531,7 +531,7 @@ class Parser { return($this->convertFunction($token, $this->_func_args)); }*/ - // if it's an argument, ignore the token (the argument remains) + // if it's an argument, ignore the token (the argument remains) } elseif ($token == 'arg') { return ''; } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 37865518..8f09af69 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2836,7 +2836,7 @@ class Worksheet extends BIFFwriter $operatorType = 0x01; break; - // not OPERATOR_NOTBETWEEN 0x02 + // not OPERATOR_NOTBETWEEN 0x02 } } From d0781c3fd236b594d94335b53691b339b4fe8818 Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre Date: Thu, 25 Aug 2022 02:47:22 +0200 Subject: [PATCH 35/69] Explain that UoM = Unit of Measure (#3014) --- docs/topics/recipes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index e4313382..afa3dcc8 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1161,7 +1161,7 @@ A column's width can be set using the following code: $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(12); ``` -If you want to set a column width using a different unit of measure, +If you want to set a column width using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the width value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1258,7 +1258,7 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. -If you want to set a row height using a different unit of measure, +If you want to set a row height using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the height value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1670,7 +1670,7 @@ $spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(12); Excel measures column width in its own proprietary units, based on the number of characters that will be displayed in the default font. -If you want to set the default column width using a different unit of measure, +If you want to set the default column width using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the width value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1693,7 +1693,7 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. -If you want to set a row height using a different unit of measure, +If you want to set a row height using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the height value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), From e97428ba6708615fd12b411c9ef2a079bcb3d252 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 24 Aug 2022 18:00:37 -0700 Subject: [PATCH 36/69] Html Writer - Do Not Generate background-color When Fill is None (#3016) * Html Writer - Do Not Generate background-color When Fill is None For PR #3002, I noted that there was a problem with Dompdf truncating images. I raised an issue with them (https://github.com/dompdf/dompdf/issues/2980), and they agree that there is a bug; however, they also suggested a workaround, namely omitting background-color from any cells which the image overlays. That did not at first appear to be a solution which could be generalized for PhpSpreasheet. However, investigating further, I saw that Html Writer is generating background-color for all cells, even though most of them use the default Fill type None (which suggests that background-color should not be specified after all). So this PR changes HTML Writer to generate background-color only when the user has actually set Fill type to something other than None. This is not a complete workaround for the Dompdf problem - we will still see truncation if the image overlays a cell which does specify a Fill type - however, it is almost certainly good enough for most use cases. In addition to that change, I made the generated Html a little smaller and the code a little more efficient by combining the TD and TH styles for each cell into a single declaration and calling createCssStyle only once. * Revamp One Test Look for both td.style and th.style instead of just td.style in test. --- src/PhpSpreadsheet/Writer/Html.php | 11 ++++++----- .../Writer/Html/VisibilityTest.php | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 362eae00..68e200f5 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -940,8 +940,8 @@ class Html extends BaseWriter // Calculate cell style hashes foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { - $css['td.style' . $index] = $this->createCSSStyle($style); - $css['th.style' . $index] = $this->createCSSStyle($style); + $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style); + //$css['th.style' . $index] = $this->createCSSStyle($style); } // Fetch sheets @@ -1094,9 +1094,10 @@ class Html extends BaseWriter $css = []; // Create CSS - $value = $fill->getFillType() == Fill::FILL_NONE ? - 'white' : '#' . $fill->getStartColor()->getRGB(); - $css['background-color'] = $value; + if ($fill->getFillType() !== Fill::FILL_NONE) { + $value = '#' . $fill->getStartColor()->getRGB(); + $css['background-color'] = $value; + } return $css; } diff --git a/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php index c5d4da68..1c1ffb06 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php @@ -99,11 +99,11 @@ class VisibilityTest extends Functional\AbstractFunctional self::assertEquals(1, $rowsrch); $rowsrch = preg_match('/^\\s*table[.]sheet0 tr[.]row1 [{] height:25pt [}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style1 [{].*text-decoration:line-through;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style1, th[.]style1 [{].*text-decoration:line-through;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style2 [{].*text-decoration:underline line-through;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style2, th[.]style2 [{].*text-decoration:underline line-through;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style3 [{].*text-decoration:underline;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style3, th[.]style3 [{].*text-decoration:underline;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); $this->writeAndReload($spreadsheet, 'Html'); From 3861f7e37e9453a9babc30d09ed08b361bb219bb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 24 Aug 2022 19:31:55 -0700 Subject: [PATCH 37/69] Charts - Add Support for Date Axis (#3018) * Charts - Add Support for Date Axis Fix #2967. Fix #2969 (which had already been fixed prior to opening the issue, but had added urgency for Date Axes). Add ability to set axis type to date axis, in addition to original possiblities of value axis and category axis. * Update 33_Chart_create_line_dateaxis.php No idea why php-cs-fixer is complaining. It didn't do so when I first uploaded. I can't duplicate problem on my own system. Not enough detail in error message for me to act. Grasping at straws, I have moved the function definition (which is the only use of braces in the entire script) from the end of the script to the beginning. * Update 33_Chart_create_line_dateaxis.php Some comments were mis-aligned. This may be related to the reasons behind PR #3025, which didn't take care of this because this script had not yet been merged. --- .../Chart/33_Chart_create_line_dateaxis.php | 375 ++++++++++++++++++ .../33_Chart_create_scatter5_trendlines.php | 1 + .../32readwriteLineDateAxisChart1.xlsx | Bin 0 -> 12114 bytes src/PhpSpreadsheet/Chart/Axis.php | 26 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 22 +- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 42 +- .../Chart/AxisPropertiesTest.php | 10 +- .../Chart/Charts32CatAxValAxTest.php | 8 +- .../Chart/Charts32XmlTest.php | 41 ++ 9 files changed, 500 insertions(+), 25 deletions(-) create mode 100644 samples/Chart/33_Chart_create_line_dateaxis.php create mode 100644 samples/templates/32readwriteLineDateAxisChart1.xlsx diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php new file mode 100644 index 00000000..1a47e5aa --- /dev/null +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -0,0 +1,375 @@ +getActiveSheet(); +$dataSheet->setTitle('Data'); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +// Dates changed not to fall on exact quarter start +$dataSheet->fromArray( + [ + ['', 'date', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE(B2)', '2021-01-10', 12.1, 15.1, 21.1], + ['=DATEVALUE(B3)', '2021-04-21', 56.2, 73.2, 86.2], + ['=DATEVALUE(B4)', '2021-07-31', 52.2, 61.2, 69.2], + ['=DATEVALUE(B5)', '2021-10-11', 30.2, 22.2, 0.2], + ['=DATEVALUE(B6)', '2022-01-21', 40.1, 38.1, 65.1], + ['=DATEVALUE(B7)', '2022-04-11', 45.2, 44.2, 96.2], + ['=DATEVALUE(B8)', '2022-07-01', 52.2, 51.2, 55.2], + ['=DATEVALUE(B9)', '2022-10-31', 41.2, 72.2, 56.2], + ] +); + +$dataSheet->getStyle('A2:A9')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$dataSheet->getColumnDimension('A')->setAutoSize(true); +$dataSheet->getColumnDimension('B')->setAutoSize(true); +$dataSheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$C$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$E$1', null, 1), +]; +// Set the X-Axis Labels +// NUMBER, not STRING +// added x-axis values for each of the 3 metrics +// added FORMATE_CODE_NUMBER +// Number of datapoints in series +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +// Data Marker Color fill/[fill,Border] +// Data Marker size +// Color(s) added +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$C$2:$C$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'diamond', + null, + 5 + ), + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$D$2:$D$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'square', + '*accent1', + 6 + ), + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$E$2:$E$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + null, + null, + 7 + ), // let Excel choose marker shape +]; +// series 1 - metric1 +// marker details +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + +// line details - dashed, smooth line (Bezier) with arrows, 40% transparent +$dataSeriesValues[0] + ->setSmoothLine(true) + ->setScatterLines(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - metric2, straight line - no special effects, connected +$dataSeriesValues[1] // square marker border color + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square marker fill color + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - metric3, markers, no line +$dataSeriesValues[2] // triangle? fill + //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); +$yAxis = new Axis(); +$yAxis->setMajorGridlines(new GridLines()); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('P12'); + +// create a 'Chart' worksheet, add $chart to it +$spreadsheet->createSheet(); +$chartSheet = $spreadsheet->getSheet(1); +$chartSheet->setTitle('Scatter+Line Chart'); + +$chartSheet = $spreadsheet->getSheetByName('Scatter+Line Chart'); +// Add the chart to the worksheet +$chartSheet->addChart($chart); + +// ------- Demonstrate Date Xaxis in Line Chart, not possible using Scatter Chart ------------ + +// Set the Labels (Column header) for each data series we want to plot +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$E$1', null, 1), +]; + +// Set the X-Axis Labels - dates, N.B. 01/10/2021 === Jan 10, NOT Oct 1 !! +// x-axis values are the Excel numeric representation of the date - so set +// formatCode=General for the xAxis VALUES, but we want the labels to be +// DISPLAYED as 'yyyy-mm-dd' That is, read a number, display a date. +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; + +// X axis (date) settings +$xAxisLabel = new Title('Date'); +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // yyyy-mm-dd + +// Set the Data values for each data series we want to plot +$dataSeriesValues = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$E$2:$E$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'triangle', + null, + 7 + ), +]; + +// series - metric3, markers, no line +$dataSeriesValues[0] + ->setScatterlines(false); // disable connecting lines +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); + +// Build the dataseries +// must now use LineChart instead of ScatterChart, since ScatterChart does not +// support "dateAx" axis type. +$series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + 'standard', // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + // DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title = new Title('Test Line-Chart with Date Axis - metric3 values'); + +// X axis (date) settings +$xAxisLabel = new Title('Game Date'); +$xAxis = new Axis(); +// date axis values are Excel numbers, not yyyy-mm-dd Date strings +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + +$xAxis->setAxisType('dateAx'); // dateAx available ONLY for LINECHART, not SCATTERCHART + +// measure the time span in Quarters, of data. +$dateMinMax = dateRange(8, $spreadsheet); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q +// change xAxis tick marks to match Qtr boundaries + +$nQtrs = sprintf('%3.2f', (($dateMinMax['max'] - $dateMinMax['min']) / 30.5) / 4); +$tickMarkInterval = ($nQtrs > 20) ? 6 : 3; // tick marks every ? months + +$xAxis->setAxisOptionsProperties( + Properties::AXIS_LABELS_NEXT_TO, // axis_label pos + null, // horizontalCrossesValue + null, // horizontalCrosses + null, // axisOrientation + 'in', // major_tick_mark + null, // minor_tick_mark + $dateMinMax['min'], // minimum calculate this from the earliest data: 'Data!$A$2' + $dateMinMax['max'], // maximum calculate this from the last data: 'Data!$A$'.($nrows+1) + $tickMarkInterval, // majorUnit determines tickmarks & Gridlines ? + null, // minorUnit + null, // textRotation + null, // hidden + 'days', // baseTimeUnit + 'months', // majorTimeUnit, + 'months', // minorTimeUnit +); + +$yAxisLabel = new Title('Value ($k)'); +$yAxis = new Axis(); +$yAxis->setMajorGridlines(new GridLines()); +$xAxis->setMajorGridlines(new GridLines()); +$minorGridLines = new GridLines(); +$minorGridLines->activateObject(); +$xAxis->setMinorGridlines($minorGridLines); + +// Create the chart +$chart = new Chart( + 'chart2', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet below the first chart +$chart->setTopLeftPosition('A13'); +$chart->setBottomRightPosition('P25'); +$chart->setRoundedCorners('true'); // Rounded corners in Chart Outline + +// Add the chart to the worksheet $chartSheet +$chartSheet->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); +$spreadsheet->disconnectWorksheets(); + +function dateRange(int $nrows, Spreadsheet $wrkbk): array +{ + $dataSheet = $wrkbk->getSheetByName('Data'); + + // start the xaxis at the beginning of the quarter of the first date + $startDateStr = $dataSheet->getCell('B2')->getValue(); // yyyy-mm-dd date string + $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj + + // get date of first day of the quarter of the start date + $startMonth = $startDate->format('n'); // suppress leading zero + $startYr = $startDate->format('Y'); + $qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0); + $qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3)); + $qtrStartStr = "$startYr-$qtrStartMonth-01"; + $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr); + + // end the xaxis at the end of the quarter of the last date + $lastDateStr = $dataSheet->getCellByColumnAndRow(2, $nrows + 1)->getValue(); + $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); + $lastMonth = $lastDate->format('n'); + $lastYr = $lastDate->format('Y'); + $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); + $qtrEndMonth = 3 + (($qtr - 1) * 3); + $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); + $qtrEndMonth = sprintf('%02d', $qtrEndMonth); + $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; + $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); + + $minMaxDates = ['min' => $ExcelQtrStartDateVal, 'max' => $ExcelQtrEndDateVal]; + + return $minMaxDates; +} diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index da831fa9..a640f735 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -263,6 +263,7 @@ $chart->setBottomRightPosition('P25'); // Add the chart to the worksheet $chartSheet $chartSheet->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); // Save Excel 2007 file $filename = $helper->getFilename(__FILE__); diff --git a/samples/templates/32readwriteLineDateAxisChart1.xlsx b/samples/templates/32readwriteLineDateAxisChart1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..42470303aa766a74253901e16ead0683db938d13 GIT binary patch literal 12114 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 null, 'textRotation' => null, 'hidden' => null, + 'majorTimeUnit' => self::TIME_UNIT_YEARS, + 'minorTimeUnit' => self::TIME_UNIT_MONTHS, + 'baseTimeUnit' => self::TIME_UNIT_DAYS, ]; /** @@ -74,6 +85,7 @@ class Axis extends Properties private const NUMERIC_FORMAT = [ Properties::FORMAT_CODE_NUMBER, Properties::FORMAT_CODE_DATE, + Properties::FORMAT_CODE_DATE_ISO8601, ]; /** @@ -115,12 +127,12 @@ class Axis extends Properties public function getAxisIsNumericFormat(): bool { - return (bool) $this->axisNumber['numeric']; + return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; } public function setAxisOption(string $key, ?string $value): void { - if (!empty($value)) { + if ($value !== null && $value !== '') { $this->axisOptions[$key] = $value; } } @@ -140,7 +152,10 @@ class Axis extends Properties ?string $majorUnit = null, ?string $minorUnit = null, ?string $textRotation = null, - ?string $hidden = null + ?string $hidden = null, + ?string $baseTimeUnit = null, + ?string $majorTimeUnit = null, + ?string $minorTimeUnit = null ): void { $this->axisOptions['axis_labels'] = $axisLabels; $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); @@ -154,6 +169,9 @@ class Axis extends Properties $this->setAxisOption('minor_unit', $minorUnit); $this->setAxisOption('textRotation', $textRotation); $this->setAxisOption('hidden', $hidden); + $this->setAxisOption('baseTimeUnit', $baseTimeUnit); + $this->setAxisOption('majorTimeUnit', $majorTimeUnit); + $this->setAxisOption('minorTimeUnit', $minorTimeUnit); } /** @@ -185,7 +203,7 @@ class Axis extends Properties public function setAxisType(string $type): self { - if ($type === 'catAx' || $type === 'valAx') { + if ($type === self::AXIS_TYPE_CATEGORY || $type === self::AXIS_TYPE_VALUE || $type === self::AXIS_TYPE_DATE) { $this->axisType = $type; } else { $this->axisType = ''; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index dab2b410..76316db9 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -139,12 +139,13 @@ class Chart $plotAreaLayout = $this->chartLayoutDetails($chartDetail); break; - case 'catAx': + case Axis::AXIS_TYPE_CATEGORY: + case Axis::AXIS_TYPE_DATE: $catAxRead = true; if (isset($chartDetail->title)) { $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } - $xAxis->setAxisType('catAx'); + $xAxis->setAxisType($chartDetailKey); $this->readEffects($chartDetail, $xAxis); if (isset($chartDetail->spPr)) { $sppr = $chartDetail->spPr->children($this->aNamespace); @@ -173,13 +174,7 @@ class Chart $this->setAxisProperties($chartDetail, $xAxis); break; - case 'dateAx': - if (isset($chartDetail->title)) { - $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); - } - - break; - case 'valAx': + case Axis::AXIS_TYPE_VALUE: $whichAxis = null; $axPos = null; if (isset($chartDetail->axPos)) { @@ -1375,6 +1370,15 @@ class Chart if (isset($chartDetail->minorUnit)) { $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); } + if (isset($chartDetail->baseTimeUnit)) { + $whichAxis->setAxisOption('baseTimeUnit', (string) self::getAttribute($chartDetail->baseTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->majorTimeUnit)) { + $whichAxis->setAxisOption('majorTimeUnit', (string) self::getAttribute($chartDetail->majorTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->minorTimeUnit)) { + $whichAxis->setAxisOption('minorTimeUnit', (string) self::getAttribute($chartDetail->minorTimeUnit, 'val', 'string')); + } if (isset($chartDetail->txPr)) { $children = $chartDetail->txPr->children($this->aNamespace); if (isset($children->bodyPr)) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index ad746a0a..48f7d255 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -490,13 +490,14 @@ class Chart extends WriterPart private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc - // In that case, xAxis is NOT a category. - if ($yAxis->getAxisType() !== '') { - $objWriter->startElement('c:' . $yAxis->getAxisType()); + // In that case, xAxis may contain values like the yAxis, or it may be a date axis (LINECHART). + $axisType = $yAxis->getAxisType(); + if ($axisType !== '') { + $objWriter->startElement("c:$axisType"); } elseif ($yAxis->getAxisIsNumericFormat()) { - $objWriter->startElement('c:valAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); } else { - $objWriter->startElement('c:catAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_CATEGORY); } $majorGridlines = $yAxis->getMajorGridlines(); $minorGridlines = $yAxis->getMinorGridlines(); @@ -654,7 +655,8 @@ class Chart extends WriterPart } $objWriter->startElement('c:auto'); - $objWriter->writeAttribute('val', '1'); + // LineChart with dateAx wants '0' + $objWriter->writeAttribute('val', ($axisType === Axis::AXIS_TYPE_DATE) ? '0' : '1'); $objWriter->endElement(); $objWriter->startElement('c:lblAlgn'); @@ -665,6 +667,30 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); + if ($axisType === Axis::AXIS_TYPE_DATE) { + $property = 'baseTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'majorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'minorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + } + if ($isMultiLevelSeries) { $objWriter->startElement('c:noMultiLvlLbl'); $objWriter->writeAttribute('val', '0'); @@ -683,7 +709,7 @@ class Chart extends WriterPart */ private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis): void { - $objWriter->startElement('c:valAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); $majorGridlines = $xAxis->getMajorGridlines(); $minorGridlines = $xAxis->getMinorGridlines(); @@ -1079,7 +1105,7 @@ class Chart extends WriterPart $objWriter->endElement(); // a:ln } } - $nofill = $groupType == DataSeries::TYPE_STOCKCHART || ($groupType === DataSeries::TYPE_SCATTERCHART && !$plotSeriesValues->getScatterLines()); + $nofill = $groupType === DataSeries::TYPE_STOCKCHART || (($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) && !$plotSeriesValues->getScatterLines()); if ($callLineStyles) { $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill); $this->writeEffects($objWriter, $plotSeriesValues); diff --git a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php index 91df25cb..0bfc2966 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php @@ -109,7 +109,9 @@ class AxisPropertiesTest extends AbstractFunctional '8', //minimum '68', //maximum '20', //majorUnit - '5' //minorUnit + '5', //minorUnit + '6', //textRotation + '0', //hidden ); self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis->getAxisOptionsProperty('axis_labels')); self::assertNull($xAxis->getAxisOptionsProperty('horizontal_crosses_value')); @@ -121,6 +123,8 @@ class AxisPropertiesTest extends AbstractFunctional self::assertSame('68', $xAxis->getAxisOptionsProperty('maximum')); self::assertSame('20', $xAxis->getAxisOptionsProperty('major_unit')); self::assertSame('5', $xAxis->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis->getAxisOptionsProperty('hidden')); $yAxis = new Axis(); $yAxis->setFillParameters('accent1', 30, 'schemeClr'); @@ -158,6 +162,8 @@ class AxisPropertiesTest extends AbstractFunctional self::assertSame('68', $xAxis2->getAxisOptionsProperty('maximum')); self::assertSame('20', $xAxis2->getAxisOptionsProperty('major_unit')); self::assertSame('5', $xAxis2->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis2->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis2->getAxisOptionsProperty('hidden')); $yAxis2 = $chart->getChartAxisY(); self::assertSame('accent1', $yAxis2->getFillProperty('value')); @@ -198,6 +204,8 @@ class AxisPropertiesTest extends AbstractFunctional self::assertSame('68', $xAxis3->getAxisOptionsProperty('maximum')); self::assertSame('20', $xAxis3->getAxisOptionsProperty('major_unit')); self::assertSame('5', $xAxis3->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis3->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis3->getAxisOptionsProperty('hidden')); $yAxis3 = $chart2->getChartAxisY(); self::assertSame('accent1', $yAxis3->getFillProperty('value')); diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php index 268ee094..1f046af9 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php @@ -23,6 +23,8 @@ class Charts32CatAxValAxTest extends TestCase /** @var string */ private $outputFileName = ''; + private const FORMAT_CODE_DATE_ISO8601_SLASH = 'yyyy/mm/dd'; // not automatically treated as numeric + protected function tearDown(): void { if ($this->outputFileName !== '') { @@ -48,7 +50,7 @@ class Charts32CatAxValAxTest extends TestCase ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], ] ); - $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); + $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(self::FORMAT_CODE_DATE_ISO8601_SLASH); $worksheet->getColumnDimension('A')->setAutoSize(true); $worksheet->setSelectedCells('A1'); @@ -91,9 +93,9 @@ class Charts32CatAxValAxTest extends TestCase $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); if (is_bool($numeric)) { - $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, $numeric); + $xAxis->setAxisNumberProperties(self::FORMAT_CODE_DATE_ISO8601_SLASH, $numeric); } else { - $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + $xAxis->setAxisNumberProperties(self::FORMAT_CODE_DATE_ISO8601_SLASH); } // Build the dataseries diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php index 6a4673fd..4cc62360 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php @@ -177,4 +177,45 @@ class Charts32XmlTest extends TestCase ) ); } + + public function testDateAx(): void + { + $file = self::DIRECTORY . '32readwriteLineDateAxisChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(2, $charts); + $chart = $charts[1]; + self::assertNotNull($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + } } From 131708409b7a48949838e13bf528f1dca7947fc7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 25 Aug 2022 23:22:59 -0700 Subject: [PATCH 38/69] Correct Very Minor Error / Php8.2 Deprecation (#3021) When Reader/Xlsx/WorkbookView was split off from Reader/Xlsx.php, one statement accidentally brought `!empty($this->loadSheetsOnly)` with it. That property does not exist in WorkbookView, so the test is useless (it is always empty); and, in fact, the caller passes its own version of loadSheetsOnly as a parameter, so it isn't needed even it did exist. In Php8.2, this might be a deprecation, although it hasn't shown up in the GitHub 8.2 tests. Fix it anyhow. --- src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php b/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php index 9d61e3d3..4743afbf 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php @@ -29,7 +29,7 @@ class WorkbookView $this->spreadsheet->setActiveSheetIndex(0); $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView; - if (($readDataOnly !== true || !empty($this->loadSheetsOnly)) && !empty($workbookView)) { + if ($readDataOnly !== true && !empty($workbookView)) { $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView)); // active sheet index $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index From f7a35349289532a2ee3912f2464d9722826152a8 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 27 Aug 2022 16:23:55 +0200 Subject: [PATCH 39/69] Implementation of the `VALUETOTEXT()` Excel Function --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 4 +-- .../Calculation/Engineering/ConvertUOM.php | 1 + .../Calculation/TextData/Format.php | 34 +++++++++++++++++++ .../Functions/TextData/ValueToTextTest.php | 28 +++++++++++++++ .../data/Calculation/TextData/VALUETOTEXT.php | 27 +++++++++++++++ 6 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php create mode 100644 tests/data/Calculation/TextData/VALUETOTEXT.php diff --git a/CHANGELOG.md b/CHANGELOG.md index da29bc8f..dc17d852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions -- Implementation of the `ARRAYTOTEXT()` Excel Function +- Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. - Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines. diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index d9485ad9..b5a26f62 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2659,8 +2659,8 @@ class Calculation ], 'VALUETOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Format::class, 'valueToText'], + 'argumentCount' => '1,2', ], 'VAR' => [ 'category' => Category::CATEGORY_STATISTICAL, diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php index 677fb0fb..b7c298db 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php @@ -106,6 +106,7 @@ class ConvertUOM 'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false], + // Magnetism 'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true], 'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true], // Temperature diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index bec11496..03e75d1d 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -4,11 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; @@ -208,6 +210,38 @@ class Format return (float) $value; } + /** + * TEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $format + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function valueToText($value, $format = false) + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + $format = (bool) $format; + + if (is_object($value) && $value instanceof RichText) { + $value = $value->getPlainText(); + } + if (is_string($value)) { + $value = ($format === true) ? Calculation::wrapResult($value) : $value; + $value = str_replace("\n", '', $value); + } elseif (is_bool($value)) { + $value = Calculation::$localeBoolean[$value === true ? 'TRUE' : 'FALSE']; + } + + return (string) $value; + } + /** * @param mixed $decimalSeparator */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php new file mode 100644 index 00000000..574d4f56 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php @@ -0,0 +1,28 @@ +getSheet(); + $this->setCell('A1', $value); + $sheet->getCell('B1')->setValue("=VALUETOTEXT(A1, {$format})"); + + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerVALUE(): array + { + return require 'tests/data/Calculation/TextData/VALUETOTEXT.php'; + } +} diff --git a/tests/data/Calculation/TextData/VALUETOTEXT.php b/tests/data/Calculation/TextData/VALUETOTEXT.php new file mode 100644 index 00000000..13df19ab --- /dev/null +++ b/tests/data/Calculation/TextData/VALUETOTEXT.php @@ -0,0 +1,27 @@ +createTextRun('Hello'); +$richText1->createText(' World'); + +$richText2 = new RichText(); +$richText2->createTextRun('Hello'); +$richText2->createText("\nWorld"); + +return [ + ['1', 1, 0], + ['1.23', 1.23, 0], + ['-123.456', -123.456, 0], + ['TRUE', true, 0], + ['FALSE', false, 0], + ['Hello World', 'Hello World', 0], + ['HelloWorld', "Hello\nWorld", 0], + ['"Hello World"', 'Hello World', 1], + ['"HelloWorld"', "Hello\nWorld", 1], + ['Hello World', $richText1, 0], + ['HelloWorld', $richText2, 0], + ['"Hello World"', $richText1, 1], + ['"HelloWorld"', $richText2, 1], +]; From 389ca80e00e833031e1c6ff2adff5c9de08668c8 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 Aug 2022 22:59:35 -0700 Subject: [PATCH 40/69] Additional Properties for Trendlines (#3028) Fix #3011. Some properties for Trendlines were omitted in the original request for this feature. Also, the trendlines sample spreadsheet included two charts. The rendering script 35_Chart_render handles this, but overlays the first output file with the second. It is changed to produce files with different names. --- .../33_Chart_create_scatter5_trendlines.php | 2 +- samples/Chart/35_Chart_render.php | 3 + .../32readwriteScatterChartTrendlines1.xlsx | Bin 15275 -> 15334 bytes src/PhpSpreadsheet/Chart/TrendLine.php | 108 +++++++++++++++++- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 20 +++- src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 24 ++++ .../Chart/TrendLineTest.php | 12 ++ 7 files changed, 163 insertions(+), 6 deletions(-) diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index a640f735..5beb82cd 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -193,7 +193,7 @@ $dataSeriesValues = [ // 3- moving Avg (period=2) single-arrow trendline, w=1.5, same color as marker fill; no dispRSqr, no dispEq $trendLines = [ new TrendLine(TrendLine::TRENDLINE_LINEAR, null, null, false, false), - new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true), + new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true, 20.0, 28.0, 44104.5, 'metric3 polynomial fit'), new TrendLine(TrendLine::TRENDLINE_MOVING_AVG, null, 2, true), ]; $dataSeriesValues[0]->setTrendLines($trendLines); diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 2376008c..891cd27c 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -73,6 +73,9 @@ foreach ($inputFileNames as $inputFileName) { $helper->log(' ' . $chartName . ' - ' . $caption); $jpegFile = $helper->getFilename('35-' . $inputFileNameShort, 'png'); + if ($i !== 0) { + $jpegFile = substr($jpegFile, 0, -3) . "$i.png"; + } if (file_exists($jpegFile)) { unlink($jpegFile); } diff --git a/samples/templates/32readwriteScatterChartTrendlines1.xlsx b/samples/templates/32readwriteScatterChartTrendlines1.xlsx index f48366fe0a3705bfaa1c91671a26c72b054b72e1..c8411d4daf83f0d6095c3d8721efaa54423bc5cb 100644 GIT binary patch delta 3291 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 Z00000#FIliJ^_-Gf;%GyU^f5&004C^C3^q> delta 3230 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 Q0auf~J0k`qHvj+t0D=4oYybcN diff --git a/src/PhpSpreadsheet/Chart/TrendLine.php b/src/PhpSpreadsheet/Chart/TrendLine.php index e177f819..75a5896c 100644 --- a/src/PhpSpreadsheet/Chart/TrendLine.php +++ b/src/PhpSpreadsheet/Chart/TrendLine.php @@ -34,13 +34,44 @@ class TrendLine extends Properties /** @var bool */ private $dispEq = false; + /** @var string */ + private $name = ''; + + /** @var float */ + private $backward = 0.0; + + /** @var float */ + private $forward = 0.0; + + /** @var float */ + private $intercept = 0.0; + /** * Create a new TrendLine object. */ - public function __construct(string $trendLineType = '', ?int $order = null, ?int $period = null, bool $dispRSqr = false, bool $dispEq = false) - { + public function __construct( + string $trendLineType = '', + ?int $order = null, + ?int $period = null, + bool $dispRSqr = false, + bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ) { parent::__construct(); - $this->setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + $this->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); } public function getTrendLineType(): string @@ -103,8 +134,65 @@ class TrendLine extends Properties return $this; } - public function setTrendLineProperties(?string $trendLineType = null, ?int $order = 0, ?int $period = 0, ?bool $dispRSqr = false, ?bool $dispEq = false): self + public function getName(): string { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getBackward(): float + { + return $this->backward; + } + + public function setBackward(float $backward): self + { + $this->backward = $backward; + + return $this; + } + + public function getForward(): float + { + return $this->forward; + } + + public function setForward(float $forward): self + { + $this->forward = $forward; + + return $this; + } + + public function getIntercept(): float + { + return $this->intercept; + } + + public function setIntercept(float $intercept): self + { + $this->intercept = $intercept; + + return $this; + } + + public function setTrendLineProperties( + ?string $trendLineType = null, + ?int $order = 0, + ?int $period = 0, + ?bool $dispRSqr = false, + ?bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ): self { if (!empty($trendLineType)) { $this->setTrendLineType($trendLineType); } @@ -120,6 +208,18 @@ class TrendLine extends Properties if ($dispEq !== null) { $this->setDispEq($dispEq); } + if ($backward !== null) { + $this->setBackward($backward); + } + if ($forward !== null) { + $this->setForward($forward); + } + if ($intercept !== null) { + $this->setIntercept($intercept); + } + if ($name !== null) { + $this->setName($name); + } return $this; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 76316db9..9eb30256 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -531,7 +531,25 @@ class Chart $order = self::getAttribute($seriesDetail->order, 'val', 'integer'); /** @var ?int */ $period = self::getAttribute($seriesDetail->period, 'val', 'integer'); - $trendLine->setTrendLineProperties($trendLineType, $order, $period, $dispRSqr, $dispEq); + /** @var ?float */ + $forward = self::getAttribute($seriesDetail->forward, 'val', 'float'); + /** @var ?float */ + $backward = self::getAttribute($seriesDetail->backward, 'val', 'float'); + /** @var ?float */ + $intercept = self::getAttribute($seriesDetail->intercept, 'val', 'float'); + /** @var ?string */ + $name = (string) $seriesDetail->name; + $trendLine->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); $trendLines[] = $trendLine; break; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 48f7d255..8dab9529 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -1158,10 +1158,19 @@ class Chart extends WriterPart $period = $trendLine->getPeriod(); $dispRSqr = $trendLine->getDispRSqr(); $dispEq = $trendLine->getDispEq(); + $forward = $trendLine->getForward(); + $backward = $trendLine->getBackward(); + $intercept = $trendLine->getIntercept(); + $name = $trendLine->getName(); $trendLineColor = $trendLine->getLineColor(); // ChartColor $trendLineWidth = $trendLine->getLineStyleProperty('width'); $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' + if ($name !== '') { + $objWriter->startElement('c:name'); + $objWriter->writeRawData($name); + $objWriter->endElement(); // c:name + } $objWriter->startElement('c:spPr'); if (!$trendLineColor->isUsable()) { @@ -1181,6 +1190,21 @@ class Chart extends WriterPart $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell' $objWriter->writeAttribute('val', $trendLineType); $objWriter->endElement(); // trendlineType + if ($backward !== 0.0) { + $objWriter->startElement('c:backward'); + $objWriter->writeAttribute('val', "$backward"); + $objWriter->endElement(); // c:backward + } + if ($forward !== 0.0) { + $objWriter->startElement('c:forward'); + $objWriter->writeAttribute('val', "$forward"); + $objWriter->endElement(); // c:forward + } + if ($intercept !== 0.0) { + $objWriter->startElement('c:intercept'); + $objWriter->writeAttribute('val', "$intercept"); + $objWriter->endElement(); // c:intercept + } if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) { $objWriter->startElement('c:order'); $objWriter->writeAttribute('val', $order); diff --git a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php index c8550d83..954a7336 100644 --- a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php +++ b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php @@ -73,6 +73,10 @@ class TrendLineTest extends AbstractFunctional self::assertSame('accent4', $lineColor->getValue()); self::assertSame('stealth', $trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); self::assertEquals(0.5, $trendLine->getLineStyleProperty('width')); + self::assertSame('', $trendLine->getName()); + self::assertSame(0.0, $trendLine->getBackward()); + self::assertSame(0.0, $trendLine->getForward()); + self::assertSame(0.0, $trendLine->getIntercept()); $trendLine = $trendLines[1]; self::assertSame('poly', $trendLine->getTrendLineType()); @@ -82,6 +86,10 @@ class TrendLineTest extends AbstractFunctional self::assertSame('accent3', $lineColor->getValue()); self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); self::assertEquals(1.25, $trendLine->getLineStyleProperty('width')); + self::assertSame('metric3 polynomial', $trendLine->getName()); + self::assertSame(20.0, $trendLine->getBackward()); + self::assertSame(28.0, $trendLine->getForward()); + self::assertSame(14400.5, $trendLine->getIntercept()); $trendLine = $trendLines[2]; self::assertSame('movingAvg', $trendLine->getTrendLineType()); @@ -91,6 +99,10 @@ class TrendLineTest extends AbstractFunctional self::assertSame('accent2', $lineColor->getValue()); self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); self::assertEquals(1.5, $trendLine->getLineStyleProperty('width')); + self::assertSame('', $trendLine->getName()); + self::assertSame(0.0, $trendLine->getBackward()); + self::assertSame(0.0, $trendLine->getForward()); + self::assertSame(0.0, $trendLine->getIntercept()); $reloadedSpreadsheet->disconnectWorksheets(); } From ca90379dc421c6a05059890961cbc0cde5af40ac Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 27 Aug 2022 23:15:16 -0700 Subject: [PATCH 41/69] 2 Minor Phpstan-related Fixes (#3030) For one of the Phpstan upgrades, some message text had changed so drastically that the only practical solution at the time was to move the messages from phpstan-baseline.neon to phpstan.neon.dist. This was not ideal, but it allowed us time to move on and study the errors, which I have now done. At one point, Parser is expecting a variable to be an array, and that was not clear from the code. If not an array, the code will error out (which was Phpstan's concern); I have changed it to throw an exception instead. This satisfies Phpstan, and I can get the message out of neon.dist (without needing to restore it to baseline). Unsurprisingly, the exception was never thrown in the existing test suite, although I added a couple of tests to exercise that code. In Helper/Dimension, Phpstan flagged a statement inappropriately. I suppressed the message using an annotation and filed a bug report https://github.com/phpstan/phpstan/issues/7563. A fix for the problem was merged yesterday, which is good, but it puts us in a tenuous position. The annotation is needed now, but, when the fix is inevitably pushed to the version we use, the no-longer-needed annotation will trigger a different message. Recode so that neither the current nor the future versions will issue a message, eliminating the annotation in the process. --- phpstan.neon.dist | 5 -- src/PhpSpreadsheet/Helper/Dimension.php | 14 ++++- src/PhpSpreadsheet/Writer/Xls/Parser.php | 7 ++- .../Writer/Xls/ParserTest.php | 56 +++++++++++++++++++ 4 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 5cac36a1..64b325c6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -21,8 +21,3 @@ parameters: # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' - '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' - - # Some issues in Xls/Parser between 1.6.3 and 1.7.7 - - - message: "#^Offset '(left|right|value)' does not exist on (non-empty-array\\|string|array\\|null)\\.$#" - path: src/PhpSpreadsheet/Writer/Xls/Parser.php diff --git a/src/PhpSpreadsheet/Helper/Dimension.php b/src/PhpSpreadsheet/Helper/Dimension.php index 425c9a61..ff07ce5b 100644 --- a/src/PhpSpreadsheet/Helper/Dimension.php +++ b/src/PhpSpreadsheet/Helper/Dimension.php @@ -55,10 +55,20 @@ class Dimension */ protected $unit; + /** + * Phpstan bug has been fixed; this function allows us to + * pass Phpstan whether fixed or not. + * + * @param mixed $value + */ + private static function stanBugFixed($value): array + { + return is_array($value) ? $value : [null, null]; + } + public function __construct(string $dimension) { - // @phpstan-ignore-next-line - [$size, $unit] = sscanf($dimension, '%[1234567890.]%s'); + [$size, $unit] = self::stanBugFixed(sscanf($dimension, '%[1234567890.]%s')); $unit = strtolower(trim($unit ?? '')); $size = (float) $size; diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index ca9b67b5..ca407d2a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -78,7 +78,7 @@ class Parser /** * The parse tree to be generated. * - * @var string + * @var array|string */ public $parseTree; @@ -1445,6 +1445,9 @@ class Parser if (empty($tree)) { // If it's the first call use parseTree $tree = $this->parseTree; } + if (!is_array($tree) || !isset($tree['left'], $tree['right'], $tree['value'])) { + throw new WriterException('Unexpected non-array'); + } if (is_array($tree['left'])) { $converted_tree = $this->toReversePolish($tree['left']); @@ -1475,7 +1478,7 @@ class Parser $left_tree = ''; } - // add it's left subtree and return. + // add its left subtree and return. return $left_tree . $this->convertFunction($tree['value'], $tree['right']); } $converted_tree = $this->convert($tree['value']); diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php new file mode 100644 index 00000000..38fd29d3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php @@ -0,0 +1,56 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + public function testNonArray(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unexpected non-array'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(); + } + + public function testMissingIndex(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unexpected non-array'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(['left' => 0]); + } + + public function testParseError(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unknown token +'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => '+']); + } + + public function testGoodParse(): void + { + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + self::assertSame('1e01001e02001e0300', bin2hex($parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => 3]))); + } +} From 026f699a6503f352d8c359dc3e867bc14f8f5e3b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 29 Aug 2022 17:14:03 +0200 Subject: [PATCH 42/69] Minor documentation updates --- docs/topics/calculation-engine.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/topics/calculation-engine.md b/docs/topics/calculation-engine.md index 4fd300e8..7dc838f7 100644 --- a/docs/topics/calculation-engine.md +++ b/docs/topics/calculation-engine.md @@ -22,6 +22,13 @@ with PhpSpreadsheet, it evaluates to the value "64": ![09-command-line-calculation.png](./images/09-command-line-calculation.png) +When writing a formula to a cell, formulae should always be set as they would appear in an English version of Microsoft Office Excel, and PhpSpreadsheet handles all formulae internally in this format. This means that the following rules hold: + + - Decimal separator is `.` (period) + - Function argument separator is `,` (comma) + - Matrix row separator is `;` (semicolon) + - English function names must be used + Another nice feature of PhpSpreadsheet's formula parser, is that it can automatically adjust a formula when inserting/removing rows/columns. Here's an example: @@ -43,6 +50,11 @@ inserted 2 new rows), changed to "SUM(E4:E11)". Also, the inserted cells duplicate style information of the previous cell, just like Excel's behaviour. Note that you can both insert rows and columns. +If you want to "anchor" a specific cell for a formula, then you prefix the column and/or the row with a `$` symbol, exactly as you would in MS Excel itself. +So if a formula contains "SUM(E$4:E9)", and you insert 2 new rows after row 1, the formula will be adjusted to read "SUM(E$4:E11)", with the `$` fixing row 4 as the start of the range. + + + ## Calculation Cache Once the Calculation engine has evaluated the formula in a cell, the result From 9eb5e7e976d1fa59742008ce39c704a334355ac4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 29 Aug 2022 22:15:50 -0700 Subject: [PATCH 43/69] Phpstan Baseline < 4000 Lines Part 1 (#3023) A lot of easily fixed problems throughout Writer/Xlsx/*, mostly supplying int rather than string as input to WriteAttribute/WriteElement. There are, in fact, so many opportunities, that I will split it over 2 or 3 PRs. But this first one will get Phpstan baseline down to the goal on its own. Some of the other problems are also easily fixed. In particular, the docBlocks in Style/ConditionalFormatting/ConditionalDataBar do not allow for null values, and should. --- phpstan-baseline.neon | 91 ------------------- .../ConditionalDataBar.php | 21 ++--- src/PhpSpreadsheet/Writer/Xlsx/Comments.php | 6 +- src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 6 +- src/PhpSpreadsheet/Writer/Xlsx/Workbook.php | 14 +-- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 78 ++++++++-------- .../ConditionalFormattingDataBarXlsxTest.php | 24 +++-- 7 files changed, 71 insertions(+), 169 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7c0070d7..f78c4fae 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3810,31 +3810,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Writer/Xlsx.php - - - message: "#^Parameter \\#1 \\$string of function substr expects string, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Comments.php - - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Comments.php - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#" count: 1 @@ -3999,74 +3979,3 @@ parameters: message: "#^Result of \\|\\| is always true\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Writer/Xlsx/Workbook.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^If condition is always true\\.$#" - count: 6 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeAttributeIf\\(\\) has parameter \\$condition with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeDataBarElements\\(\\) has parameter \\$dataBar with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeElementIf\\(\\) has parameter \\$condition with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 15 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 3 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<1, max\\> given\\.$#" - count: 9 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#3 \\$stringTable of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeSheetData\\(\\) expects array\\, array\\\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - - - message: "#^Parameter \\#4 \\$val of static method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Worksheet\\:\\:writeAttributeIf\\(\\) expects string, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php index 54513670..f7a2eee1 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -11,10 +11,10 @@ class ConditionalDataBar /** children */ - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $minimumConditionalFormatValueObject; - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $maximumConditionalFormatValueObject; /** @var string */ @@ -22,7 +22,7 @@ class ConditionalDataBar /** */ - /** @var ConditionalFormattingRuleExtension */ + /** @var ?ConditionalFormattingRuleExtension */ private $conditionalFormattingRuleExt; /** @@ -43,10 +43,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMinimumConditionalFormatValueObject() + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->minimumConditionalFormatValueObject; } @@ -58,10 +55,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMaximumConditionalFormatValueObject() + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->maximumConditionalFormatValueObject; } @@ -85,10 +79,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormattingRuleExtension - */ - public function getConditionalFormattingRuleExt() + public function getConditionalFormattingRuleExt(): ?ConditionalFormattingRuleExtension { return $this->conditionalFormattingRuleExt; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php index ea0f1faa..5045e8f3 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php @@ -165,7 +165,7 @@ class Comments extends WriterPart // Metadata [$column, $row] = Coordinate::indexesFromString($cellReference); $id = 1024 + $column + $row; - $id = substr($id, 0, 4); + $id = substr("$id", 0, 4); // v:shape $objWriter->startElement('v:shape'); @@ -223,10 +223,10 @@ class Comments extends WriterPart $objWriter->writeElement('x:AutoFill', 'False'); // x:Row - $objWriter->writeElement('x:Row', ($row - 1)); + $objWriter->writeElement('x:Row', (string) ($row - 1)); // x:Column - $objWriter->writeElement('x:Column', ($column - 1)); + $objWriter->writeElement('x:Column', (string) ($column - 1)); $objWriter->endElement(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 43ce442f..cb8758c2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -56,7 +56,7 @@ class DocProps extends WriterPart // Variant $objWriter->startElement('vt:variant'); - $objWriter->writeElement('vt:i4', $spreadsheet->getSheetCount()); + $objWriter->writeElement('vt:i4', (string) $spreadsheet->getSheetCount()); $objWriter->endElement(); $objWriter->endElement(); @@ -68,7 +68,7 @@ class DocProps extends WriterPart // Vector $objWriter->startElement('vt:vector'); - $objWriter->writeAttribute('size', $spreadsheet->getSheetCount()); + $objWriter->writeAttribute('size', (string) $spreadsheet->getSheetCount()); $objWriter->writeAttribute('baseType', 'lpstr'); $sheetCount = $spreadsheet->getSheetCount(); @@ -207,7 +207,7 @@ class DocProps extends WriterPart $objWriter->startElement('property'); $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'); - $objWriter->writeAttribute('pid', $key + 2); + $objWriter->writeAttribute('pid', (string) ($key + 2)); $objWriter->writeAttribute('name', $customProperty); switch ($propertyType) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index f9d7197d..7d08388d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -104,14 +104,14 @@ class Workbook extends WriterPart // workbookView $objWriter->startElement('workbookView'); - $objWriter->writeAttribute('activeTab', $spreadsheet->getActiveSheetIndex()); + $objWriter->writeAttribute('activeTab', (string) $spreadsheet->getActiveSheetIndex()); $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false')); - $objWriter->writeAttribute('firstSheet', $spreadsheet->getFirstSheetIndex()); + $objWriter->writeAttribute('firstSheet', (string) $spreadsheet->getFirstSheetIndex()); $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false')); $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false')); $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false')); $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false')); - $objWriter->writeAttribute('tabRatio', $spreadsheet->getTabRatio()); + $objWriter->writeAttribute('tabRatio', (string) $spreadsheet->getTabRatio()); $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility()); $objWriter->endElement(); @@ -157,9 +157,9 @@ class Workbook extends WriterPart $objWriter->writeAttribute('calcId', '999999'); $objWriter->writeAttribute('calcMode', 'auto'); // fullCalcOnLoad isn't needed if we've recalculating for the save - $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? 1 : 0); - $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? 0 : 1); - $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? 0 : 1); + $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? '1' : '0'); + $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? '0' : '1'); + $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? '0' : '1'); $objWriter->endElement(); } @@ -200,7 +200,7 @@ class Workbook extends WriterPart // Write sheet $objWriter->startElement('sheet'); $objWriter->writeAttribute('name', $worksheetName); - $objWriter->writeAttribute('sheetId', $worksheetId); + $objWriter->writeAttribute('sheetId', (string) $worksheetId); if ($sheetState !== 'visible' && $sheetState != '') { $objWriter->writeAttribute('state', $sheetState); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 4b0cb632..5680281f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -28,7 +28,7 @@ class Worksheet extends WriterPart * * @return string XML Output */ - public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = null, $includeCharts = false) + public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false) { // Create XML writer $objWriter = null; @@ -149,7 +149,7 @@ class Worksheet extends WriterPart } $autoFilterRange = $worksheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { - $objWriter->writeAttribute('filterMode', 1); + $objWriter->writeAttribute('filterMode', '1'); if (!$worksheet->getAutoFilter()->getEvaluated()) { $worksheet->getAutoFilter()->showHideRows(); } @@ -158,7 +158,7 @@ class Worksheet extends WriterPart // tabColor if ($worksheet->isTabColorSet()) { $objWriter->startElement('tabColor'); - $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB()); + $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB() ?? ''); $objWriter->endElement(); } @@ -218,7 +218,7 @@ class Worksheet extends WriterPart // Show zeros (Excel also writes this attribute only if set to false) if ($worksheet->getSheetView()->getShowZeros() === false) { - $objWriter->writeAttribute('showZeros', 0); + $objWriter->writeAttribute('showZeros', '0'); } // View Layout Type @@ -252,7 +252,7 @@ class Worksheet extends WriterPart // Pane $pane = ''; if ($worksheet->getFreezePane()) { - [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane() ?? ''); + [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane()); $xSplit = Coordinate::columnIndexFromString($xSplit); --$xSplit; --$ySplit; @@ -261,7 +261,7 @@ class Worksheet extends WriterPart $pane = 'topRight'; $objWriter->startElement('pane'); if ($xSplit > 0) { - $objWriter->writeAttribute('xSplit', $xSplit); + $objWriter->writeAttribute('xSplit', "$xSplit"); } if ($ySplit > 0) { $objWriter->writeAttribute('ySplit', $ySplit); @@ -334,7 +334,7 @@ class Worksheet extends WriterPart $outlineLevelRow = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow); + $objWriter->writeAttribute('outlineLevelRow', (string) (int) $outlineLevelRow); // Outline level - column $outlineLevelCol = 0; @@ -343,7 +343,7 @@ class Worksheet extends WriterPart $outlineLevelCol = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol); + $objWriter->writeAttribute('outlineLevelCol', (string) (int) $outlineLevelCol); $objWriter->endElement(); } @@ -363,8 +363,8 @@ class Worksheet extends WriterPart foreach ($worksheet->getColumnDimensions() as $colDimension) { // col $objWriter->startElement('col'); - $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); - $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('min', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('max', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); if ($colDimension->getWidth() < 0) { // No width set, apply default of 10 @@ -396,11 +396,11 @@ class Worksheet extends WriterPart // Outline level if ($colDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel()); + $objWriter->writeAttribute('outlineLevel', (string) $colDimension->getOutlineLevel()); } // Style - $objWriter->writeAttribute('style', $colDimension->getXfIndex()); + $objWriter->writeAttribute('style', (string) $colDimension->getXfIndex()); $objWriter->endElement(); } @@ -423,7 +423,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm()); $objWriter->writeAttribute('hashValue', $protection->getPassword()); $objWriter->writeAttribute('saltValue', $protection->getSalt()); - $objWriter->writeAttribute('spinCount', $protection->getSpinCount()); + $objWriter->writeAttribute('spinCount', (string) $protection->getSpinCount()); } elseif ($protection->getPassword() !== '') { $objWriter->writeAttribute('password', $protection->getPassword()); } @@ -447,7 +447,7 @@ class Worksheet extends WriterPart $objWriter->endElement(); } - private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeAttribute($attr, $val); @@ -461,7 +461,7 @@ class Worksheet extends WriterPart } } - private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeElementIf(XMLWriter $objWriter, bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeElement($attr, $val); @@ -568,7 +568,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute($attrKey, $val); } $minCfvo = $dataBar->getMinimumConditionalFormatValueObject(); - if ($minCfvo) { + if ($minCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $minCfvo->getType()); if ($minCfvo->getCellFormula()) { @@ -578,7 +578,7 @@ class Worksheet extends WriterPart } $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject(); - if ($maxCfvo) { + if ($maxCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $maxCfvo->getType()); if ($maxCfvo->getCellFormula()) { @@ -600,9 +600,8 @@ class Worksheet extends WriterPart $objWriter->endElement(); //end conditionalFormatting } - private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void + private static function writeDataBarElements(XMLWriter $objWriter, ?ConditionalDataBar $dataBar): void { - /** @var ConditionalDataBar $dataBar */ if ($dataBar) { $objWriter->startElement('dataBar'); self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0'); @@ -669,7 +668,7 @@ class Worksheet extends WriterPart 'dxfId', (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) ); - $objWriter->writeAttribute('priority', $id++); + $objWriter->writeAttribute('priority', (string) $id++); self::writeAttributeif( $objWriter, @@ -724,7 +723,7 @@ class Worksheet extends WriterPart if (!empty($dataValidationCollection)) { $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); $objWriter->startElement('dataValidations'); - $objWriter->writeAttribute('count', count($dataValidationCollection)); + $objWriter->writeAttribute('count', (string) count($dataValidationCollection)); foreach ($dataValidationCollection as $coordinate => $dv) { $objWriter->startElement('dataValidation'); @@ -922,11 +921,11 @@ class Worksheet extends WriterPart $rules = $column->getRules(); if (count($rules) > 0) { $objWriter->startElement('filterColumn'); - $objWriter->writeAttribute('colId', $worksheet->getAutoFilter()->getColumnOffset($columnID)); + $objWriter->writeAttribute('colId', (string) $worksheet->getAutoFilter()->getColumnOffset($columnID)); $objWriter->startElement($column->getFilterType()); if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) { - $objWriter->writeAttribute('and', 1); + $objWriter->writeAttribute('and', '1'); } foreach ($rules as $rule) { @@ -936,7 +935,7 @@ class Worksheet extends WriterPart ($rule->getValue() === '') ) { // Filter rule for Blanks - $objWriter->writeAttribute('blank', 1); + $objWriter->writeAttribute('blank', '1'); } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) { // Dynamic Filter Rule $objWriter->writeAttribute('type', $rule->getGrouping()); @@ -1019,24 +1018,24 @@ class Worksheet extends WriterPart { // pageSetup $objWriter->startElement('pageSetup'); - $objWriter->writeAttribute('paperSize', $worksheet->getPageSetup()->getPaperSize()); + $objWriter->writeAttribute('paperSize', (string) $worksheet->getPageSetup()->getPaperSize()); $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation()); if ($worksheet->getPageSetup()->getScale() !== null) { - $objWriter->writeAttribute('scale', $worksheet->getPageSetup()->getScale()); + $objWriter->writeAttribute('scale', (string) $worksheet->getPageSetup()->getScale()); } if ($worksheet->getPageSetup()->getFitToHeight() !== null) { - $objWriter->writeAttribute('fitToHeight', $worksheet->getPageSetup()->getFitToHeight()); + $objWriter->writeAttribute('fitToHeight', (string) $worksheet->getPageSetup()->getFitToHeight()); } else { $objWriter->writeAttribute('fitToHeight', '0'); } if ($worksheet->getPageSetup()->getFitToWidth() !== null) { - $objWriter->writeAttribute('fitToWidth', $worksheet->getPageSetup()->getFitToWidth()); + $objWriter->writeAttribute('fitToWidth', (string) $worksheet->getPageSetup()->getFitToWidth()); } else { $objWriter->writeAttribute('fitToWidth', '0'); } if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) { - $objWriter->writeAttribute('firstPageNumber', $worksheet->getPageSetup()->getFirstPageNumber()); + $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder()); @@ -1089,8 +1088,8 @@ class Worksheet extends WriterPart // rowBreaks if (!empty($aRowBreaks)) { $objWriter->startElement('rowBreaks'); - $objWriter->writeAttribute('count', count($aRowBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks)); + $objWriter->writeAttribute('count', (string) count($aRowBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aRowBreaks)); foreach ($aRowBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); @@ -1107,14 +1106,14 @@ class Worksheet extends WriterPart // Second, write column breaks if (!empty($aColumnBreaks)) { $objWriter->startElement('colBreaks'); - $objWriter->writeAttribute('count', count($aColumnBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks)); + $objWriter->writeAttribute('count', (string) count($aColumnBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks)); foreach ($aColumnBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); $objWriter->startElement('brk'); - $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1); + $objWriter->writeAttribute('id', (string) (Coordinate::columnIndexFromString($coords[0]) - 1)); $objWriter->writeAttribute('man', '1'); $objWriter->endElement(); } @@ -1166,7 +1165,7 @@ class Worksheet extends WriterPart if ($writeCurrentRow) { // Start a new row $objWriter->startElement('row'); - $objWriter->writeAttribute('r', $currentRow); + $objWriter->writeAttribute('r', "$currentRow"); $objWriter->writeAttribute('spans', '1:' . $colCount); // Row dimensions @@ -1187,12 +1186,12 @@ class Worksheet extends WriterPart // Outline level if ($rowDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); + $objWriter->writeAttribute('outlineLevel', (string) $rowDimension->getOutlineLevel()); } // Style if ($rowDimension->getXfIndex() !== null) { - $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); + $objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex()); $objWriter->writeAttribute('customFormat', '1'); } @@ -1263,7 +1262,7 @@ class Worksheet extends WriterPart $cellValue = $cellValue . '.0'; } } - $objWriter->writeElement('v', $cellValue); + $objWriter->writeElement('v', "$cellValue"); } private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void @@ -1332,7 +1331,7 @@ class Worksheet extends WriterPart // Sheet styles $xfi = $pCell->getXfIndex(); - self::writeAttributeIf($objWriter, $xfi, 's', $xfi); + self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi"); // If cell value is supplied, write cell value $cellValue = $pCell->getValue(); @@ -1449,7 +1448,6 @@ class Worksheet extends WriterPart /** @var Conditional $conditional */ foreach ($conditionalStyles as $conditional) { $dataBar = $conditional->getDataBar(); - // @phpstan-ignore-next-line if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) { $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php index 4b051dd0..0ec25d8e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php @@ -86,8 +86,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase $dataBar = $conditionalRule->getDataBar(); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals(Color::COLOR_GREEN, $dataBar->getColor()); @@ -109,8 +109,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); @@ -118,6 +118,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar->getConditionalFormattingRuleExt()); //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{72C64AE0-5CD9-164F-83D1-AB720F263E79}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('A3:A23', $rule1ext->getSqref()); @@ -165,8 +166,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('num', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('num', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals('-5', $dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -175,6 +176,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar->getConditionalFormattingRuleExt()); //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{98904F60-57F0-DF47-B480-691B20D325E3}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('B3:B23', $rule1ext->getSqref()); @@ -224,8 +226,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEmpty($dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -235,6 +237,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{453C04BA-7ABD-8548-8A17-D9CFD2BDABE9}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('C3:C23', $rule1ext->getSqref()); @@ -286,8 +289,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotNull($dataBar); self::assertTrue($dataBar->getShowValue()); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('formula', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('formula', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals('3+2', $dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -297,6 +300,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{6C1E066A-E240-3D4A-98F8-8CC218B0DFD2}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('D3:D23', $rule1ext->getSqref()); From 4f8aa806bc8b95e9d2633e91aeb0a822f12eb89a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 30 Aug 2022 22:34:20 -0700 Subject: [PATCH 44/69] Phpstan Baseline < 4000 Lines Part 2 Html (#3037) Continue to reduce the size of Phpstan Baseline by fixing problems reported for Writer/Html. --- phpstan-baseline.neon | 310 ----------------------------- phpstan.neon.dist | 2 +- src/PhpSpreadsheet/Writer/Html.php | 102 +++++----- 3 files changed, 54 insertions(+), 360 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f78c4fae..cfaed694 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3155,316 +3155,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Worksheet/Worksheet.php - - - message: "#^Call to function array_key_exists\\(\\) with int and array\\{none\\: 'none', dashDot\\: '1px dashed', dashDotDot\\: '1px dotted', dashed\\: '1px dashed', dotted\\: '1px dotted', double\\: '3px double', hair\\: '1px solid', medium\\: '2px solid', \\.\\.\\.\\} will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 'mime' on array\\|false\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 0 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot call method getSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot call method getSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$candidateSpannedRow with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$sheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateHTMLFooter\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has parameter \\$desc with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has parameter \\$val with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$cellAddress with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$columnNumber with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cellType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cssClass with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValue\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValue\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValueRich\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValueRich\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowIncludeCharts\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowIncludeCharts\\(\\) has parameter \\$coordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$colSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$rowSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cellType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$colNum with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$colSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$coordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cssClass with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$rowSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetPrep\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has parameter \\$rowMin with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has parameter \\$sheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$tbodyStart with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$theadEnd with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$theadStart with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableFooter\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$id with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTagInline\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTagInline\\(\\) has parameter \\$id with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$borderStyle of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapBorderStyle\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$font of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:createCSSStyleFont\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$hAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapHAlign\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$vAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapVAlign\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int\\<0, max\\>\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#3 \\$use_include_path of function fopen expects bool, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - message: "#^Negated boolean expression is always false\\.$#" count: 1 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 64b325c6..30bd6c2f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -16,7 +16,7 @@ parameters: processTimeout: 300.0 checkMissingIterableValueType: false ignoreErrors: - - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy) expects (GdImage|resource), GdImage\|resource given\.$~' + - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy|imagepng) expects (GdImage|resource), GdImage\|resource given\.$~' - '~^Parameter \#2 \$src_im(age)? of function imagecopy expects (GdImage|resource), GdImage\|resource given\.$~' # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 68e200f5..6a400673 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -287,7 +287,7 @@ class Html extends BaseWriter /** * Map border style. * - * @param int $borderStyle Sheet index + * @param int|string $borderStyle Sheet index * * @return string */ @@ -354,7 +354,7 @@ class Html extends BaseWriter return $this; } - private static function generateMeta($val, $desc) + private static function generateMeta(?string $val, string $desc): string { return $val ? (' ' . PHP_EOL) @@ -398,7 +398,7 @@ class Html extends BaseWriter return $html; } - private function generateSheetPrep() + private function generateSheetPrep(): array { // Ensure that Spans have been calculated? $this->calculateSpans(); @@ -413,7 +413,7 @@ class Html extends BaseWriter return $sheets; } - private function generateSheetStarts($sheet, $rowMin) + private function generateSheetStarts(Worksheet $sheet, int $rowMin): array { // calculate start of , $tbodyStart = $rowMin; @@ -432,7 +432,7 @@ class Html extends BaseWriter return [$theadStart, $theadEnd, $tbodyStart]; } - private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart) + private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array { // ? $startTag = ($row == $theadStart) ? (' ' . PHP_EOL) : ''; @@ -682,7 +682,7 @@ class Html extends BaseWriter if ($this->embedImages || substr($imageData, 0, 6) === 'zip://') { $picture = @file_get_contents($filename); if ($picture !== false) { - $imageDetails = getimagesize($filename); + $imageDetails = getimagesize($filename) ?: []; // base64 encode the binary data $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; @@ -697,12 +697,10 @@ class Html extends BaseWriter $imageResource = $drawing->getImageResource(); if ($imageResource) { ob_start(); // Let's start output buffering. - // @phpstan-ignore-next-line imagepng($imageResource); // This will normally output the image, but because of ob_start(), it won't. - $contents = ob_get_contents(); // Instead, output above is saved to $contents + $contents = (string) ob_get_contents(); // Instead, output above is saved to $contents ob_end_clean(); // End the output buffer. - /** @phpstan-ignore-next-line */ $dataUri = 'data:image/png;base64,' . base64_encode($contents); // Because of the nature of tables, width is more important than height. @@ -738,21 +736,18 @@ class Html extends BaseWriter } $html .= PHP_EOL; - $imageDetails = getimagesize($chartFileName); + $imageDetails = getimagesize($chartFileName) ?: []; $filedesc = $chart->getTitle(); $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; - if ($fp = fopen($chartFileName, 'rb', 0)) { - $picture = fread($fp, filesize($chartFileName)); - fclose($fp); - /** @phpstan-ignore-next-line */ + $picture = file_get_contents($chartFileName); + if ($picture !== false) { $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; $html .= '' . $filedesc . '' . PHP_EOL; - - unlink($chartFileName); } + unlink($chartFileName); } } } @@ -993,8 +988,8 @@ class Html extends BaseWriter $css = []; // Create CSS - $css['vertical-align'] = $this->mapVAlign($alignment->getVertical()); - $textAlign = $this->mapHAlign($alignment->getHorizontal()); + $css['vertical-align'] = $this->mapVAlign($alignment->getVertical() ?? ''); + $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? ''); if ($textAlign) { $css['text-align'] = $textAlign; if (in_array($textAlign, ['left', 'right'])) { @@ -1070,10 +1065,8 @@ class Html extends BaseWriter * Create CSS style. * * @param Border $border Border - * - * @return string */ - private function createCSSStyleBorder(Border $border) + private function createCSSStyleBorder(Border $border): string { // Create CSS - add !important to non-none border styles for merged cells $borderStyle = $this->mapBorderStyle($border->getBorderStyle()); @@ -1095,7 +1088,8 @@ class Html extends BaseWriter // Create CSS if ($fill->getFillType() !== Fill::FILL_NONE) { - $value = '#' . $fill->getStartColor()->getRGB(); + $value = $fill->getFillType() == Fill::FILL_NONE ? + 'white' : '#' . $fill->getStartColor()->getRGB(); $css['background-color'] = $value; } @@ -1105,7 +1099,7 @@ class Html extends BaseWriter /** * Generate HTML footer. */ - public function generateHTMLFooter() + public function generateHTMLFooter(): string { // Construct HTML $html = ''; @@ -1115,7 +1109,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTagInline(Worksheet $worksheet, $id) + private function generateTableTagInline(Worksheet $worksheet, string $id): string { $style = isset($this->cssStyles['table']) ? $this->assembleCSS($this->cssStyles['table']) : ''; @@ -1135,7 +1129,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void + private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void { if (!$this->useInlineCss) { $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; @@ -1188,7 +1182,7 @@ class Html extends BaseWriter /** * Generate table footer. */ - private function generateTableFooter() + private function generateTableFooter(): string { return ' ' . PHP_EOL . '
' . PHP_EOL; } @@ -1234,7 +1228,7 @@ class Html extends BaseWriter return $html; } - private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $row, $columnNumber) + private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array { $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : ''; $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1); @@ -1260,22 +1254,24 @@ class Html extends BaseWriter return [$cell, $cssClass, $coordinate]; } - private function generateRowCellDataValueRich($cell, &$cellData): void + private function generateRowCellDataValueRich(Cell $cell, string &$cellData): void { // Loop through rich text elements $elements = $cell->getValue()->getRichTextElements(); foreach ($elements as $element) { // Rich text start? if ($element instanceof Run) { - $cellData .= ''; - $cellEnd = ''; - if ($element->getFont()->getSuperscript()) { - $cellData .= ''; - $cellEnd = ''; - } elseif ($element->getFont()->getSubscript()) { - $cellData .= ''; - $cellEnd = ''; + if ($element->getFont() !== null) { + $cellData .= ''; + + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + $cellEnd = ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + $cellEnd = ''; + } } // Convert UTF8 data to PCDATA @@ -1293,7 +1289,7 @@ class Html extends BaseWriter } } - private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void + private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, ?string &$cellData): void { if ($cell->getValue() instanceof RichText) { $this->generateRowCellDataValueRich($cell, $cellData); @@ -1319,7 +1315,11 @@ class Html extends BaseWriter } } - private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType) + /** + * @param null|Cell|string $cell + * @param array|string $cssClass + */ + private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, string $cellType): string { $cellData = ' '; if ($cell instanceof Cell) { @@ -1339,10 +1339,10 @@ class Html extends BaseWriter $cellData = nl2br($cellData); // Extend CSS class? - if (!$this->useInlineCss) { + if (!$this->useInlineCss && is_string($cssClass)) { $cssClass .= ' style' . $cell->getXfIndex(); $cssClass .= ' ' . $cell->getDataType(); - } else { + } elseif (is_array($cssClass)) { if ($cellType == 'th') { if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); @@ -1372,12 +1372,12 @@ class Html extends BaseWriter return $cellData; } - private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate) + private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string { return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : ''; } - private function generateRowSpans($html, $rowSpan, $colSpan) + private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string { $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : ''; $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : ''; @@ -1385,7 +1385,10 @@ class Html extends BaseWriter return $html; } - private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row): void + /** + * @param array|string $cssClass + */ + private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void { // Image? $htmlx = $this->writeImageInCell($worksheet, $coordinate); @@ -1393,7 +1396,7 @@ class Html extends BaseWriter $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start $html .= ' <' . $cellType; - if (!$this->useInlineCss && !$this->isPdf) { + if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) { $html .= ' class="' . $cssClass . '"'; if ($htmlx) { $html .= " style='position: relative;'"; @@ -1403,9 +1406,11 @@ class Html extends BaseWriter // We must explicitly write the width of the element because TCPDF // does not recognize e.g. if ($this->useInlineCss) { - $xcssClass = $cssClass; + $xcssClass = is_array($cssClass) ? $cssClass : []; } else { - $html .= ' class="' . $cssClass . '"'; + if (is_string($cssClass)) { + $html .= ' class="' . $cssClass . '"'; + } $xcssClass = []; } $width = 0; @@ -1416,8 +1421,7 @@ class Html extends BaseWriter $width += $this->columnWidths[$sheetIndex][$i]; } } - $xcssClass['width'] = $width . 'pt'; - + $xcssClass['width'] = (string) $width . 'pt'; // We must also explicitly write the height of the element because TCPDF // does not recognize e.g. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) { @@ -1736,7 +1740,7 @@ class Html extends BaseWriter $this->spansAreCalculated = true; } - private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void + private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void { // Identify which rows should be omitted in HTML. These are the rows where all the cells // participate in a merge and the where base cells are somewhere above. From ad2d3df2f7ab48ca9aa7b92cfc2cdc38a4e78cb9 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 3 Sep 2022 12:58:23 +0200 Subject: [PATCH 45/69] Update Excel function samples for Database functions --- samples/Calculations/Database/DAVERAGE.php | 18 +-- samples/Calculations/Database/DCOUNT.php | 33 ++--- samples/Calculations/Database/DCOUNTA.php | 58 +++++++++ samples/Calculations/Database/DGET.php | 24 ++-- samples/Calculations/Database/DMAX.php | 16 +-- samples/Calculations/Database/DMIN.php | 16 +-- samples/Calculations/Database/DPRODUCT.php | 24 ++-- samples/Calculations/Database/DSTDEV.php | 18 +-- samples/Calculations/Database/DSTDEVP.php | 19 +-- samples/Calculations/Database/DSUM.php | 58 +++++++++ samples/Calculations/Database/DVAR.php | 19 +-- samples/Calculations/Database/DVARP.php | 18 +-- src/PhpSpreadsheet/Helper/Sample.php | 28 +++++ src/PhpSpreadsheet/Helper/TextGrid.php | 139 +++++++++++++++++++++ 14 files changed, 401 insertions(+), 87 deletions(-) create mode 100644 samples/Calculations/Database/DCOUNTA.php create mode 100644 samples/Calculations/Database/DSUM.php create mode 100644 src/PhpSpreadsheet/Helper/TextGrid.php diff --git a/samples/Calculations/Database/DAVERAGE.php b/samples/Calculations/Database/DAVERAGE.php index 92d84014..fa388c8b 100644 --- a/samples/Calculations/Database/DAVERAGE.php +++ b/samples/Calculations/Database/DAVERAGE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the average of selected database entries.'); +$category = 'Database'; +$functionName = 'DAVERAGE'; +$description = 'Returns the average of selected database entries that match criteria'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DAVERAGE(A4:E10,3,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DAVERAGE() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DAVERAGE() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DCOUNT.php b/samples/Calculations/Database/DCOUNT.php index d869a4bc..68d6be18 100644 --- a/samples/Calculations/Database/DCOUNT.php +++ b/samples/Calculations/Database/DCOUNT.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Counts the cells that contain numbers in a database.'); + +$category = 'Database'; +$functionName = 'DCOUNT'; +$description = 'Counts the cells that contain numbers in a set of database records that match criteria'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -14,9 +19,9 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ['Apple', 18, 20, 14, 105.00], ['Pear', 12, 12, 10, 96.00], ['Cherry', 13, 14, 9, 105.00], - ['Apple', 14, 15, 10, 75.00], - ['Pear', 9, 8, 8, 76.80], - ['Apple', 8, 9, 6, 45.00], + ['Apple', 14, 'N/A', 10, 75.00], + ['Pear', 9, 8, 8, 77.00], + ['Apple', 12, 11, 6, 45.00], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], ['="=Apple"', '>10', null, null, null, '<16'], @@ -26,30 +31,28 @@ $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], $worksheet->fromArray($criteria, null, 'A1'); $worksheet->fromArray($database, null, 'A4'); -$worksheet->setCellValue('A12', 'The Number of Apple trees over 10\' in height'); -$worksheet->setCellValue('B12', '=DCOUNT(A4:E10,"Yield",A1:B2)'); +$worksheet->setCellValue('A12', 'The Number of Apple trees between 10\' and 16\' in height whose age is known'); +$worksheet->setCellValue('B12', '=DCOUNT(A4:E10,"Age",A1:F2)'); -$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard'); +$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard with a numeric value in column 3 ("Age")'); $worksheet->setCellValue('B13', '=DCOUNT(A4:E10,3,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); -var_dump($criteriaData); +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DCOUNT() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DCOUNT() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DCOUNTA.php b/samples/Calculations/Database/DCOUNTA.php new file mode 100644 index 00000000..3b4cc16e --- /dev/null +++ b/samples/Calculations/Database/DCOUNTA.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], + ['Apple', 18, 20, 14, 105.00], + ['Pear', 12, 12, 10, 96.00], + ['Cherry', 13, 14, 9, 105.00], + ['Apple', 14, 'N/A', 10, 75.00], + ['Pear', 9, 8, 8, 77.00], + ['Apple', 12, 11, 6, 45.00], +]; +$criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], + ['="=Apple"', '>10', null, null, null, '<16'], + ['="=Pear"', null, null, null, null, null], +]; + +$worksheet->fromArray($criteria, null, 'A1'); +$worksheet->fromArray($database, null, 'A4'); + +$worksheet->setCellValue('A12', 'The Number of Apple trees between 10\' and 16\' in height'); +$worksheet->setCellValue('B12', '=DCOUNTA(A4:E10,"Age",A1:F2)'); + +$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard'); +$worksheet->setCellValue('B13', '=DCOUNTA(A4:E10,3,A1:A3)'); + +$helper->log('Database'); + +$databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); +$helper->displayGrid($databaseData); + +// Test the formulae +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); + +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DGET.php b/samples/Calculations/Database/DGET.php index 9f543c91..1b41b777 100644 --- a/samples/Calculations/Database/DGET.php +++ b/samples/Calculations/Database/DGET.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Extracts a single value from a column of a list or database that matches conditions that you specify.'); +$category = 'Database'; +$functionName = 'DGET'; +$description = 'Extracts a single value from a column of a list or database that matches criteria that you specify'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -21,7 +25,7 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], ['="=Apple"', '>10', null, null, null, '<16'], - ['="=Pear"', null, null, null, null, null], + ['="=Pear"', '>12', null, null, null, null], ]; $worksheet->fromArray($criteria, null, 'A1'); @@ -30,23 +34,25 @@ $worksheet->fromArray($database, null, 'A4'); $worksheet->setCellValue('A12', 'The height of the Apple tree between 10\' and 16\' tall'); $worksheet->setCellValue('B12', '=DGET(A4:E10,"Height",A1:F2)'); +$worksheet->setCellValue('A13', 'The height of the Apple tree (will return an Excel error, because there is more than one apple tree)'); +$worksheet->setCellValue('B13', '=DGET(A4:E10,"Height",A1:A2)'); + $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$helper->log('ALL'); +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DMAX.php b/samples/Calculations/Database/DMAX.php index c48928d4..0998904b 100644 --- a/samples/Calculations/Database/DMAX.php +++ b/samples/Calculations/Database/DMAX.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the maximum value from selected database entries.'); +$category = 'Database'; +$functionName = 'DMAX'; +$description = 'Returns the maximum value from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,20 +40,18 @@ $worksheet->setCellValue('B13', '=DMAX(A4:E10,3,A1:A2)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $helper->log('ALL'); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DMIN.php b/samples/Calculations/Database/DMIN.php index 7bcaa206..c0e0a8d8 100644 --- a/samples/Calculations/Database/DMIN.php +++ b/samples/Calculations/Database/DMIN.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the minimum value from selected database entries.'); +$category = 'Database'; +$functionName = 'DMIN'; +$description = 'Returns the minimum value from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,20 +40,18 @@ $worksheet->setCellValue('B13', '=DMIN(A4:E10,3,A1:A2)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $helper->log('ALL'); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMIN() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMIN() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DPRODUCT.php b/samples/Calculations/Database/DPRODUCT.php index 7c14ded6..fa666ffe 100644 --- a/samples/Calculations/Database/DPRODUCT.php +++ b/samples/Calculations/Database/DPRODUCT.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Multiplies the values in a column of a list or database that match conditions that you specify.'); +$category = 'Database'; +$functionName = 'DPRODUCT'; +$description = 'Multiplies the values in a column of a list or database that match conditions that you specify'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -16,7 +20,7 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ['Pear', 12, 12, 10, 96.00], ['Cherry', 13, 14, 9, 105.00], ['Apple', 14, 15, 10, 75.00], - ['Pear', 9, 8, 8, 76.80], + ['Pear', 9, 8, 8, 77.00], ['Apple', 8, 9, 6, 45.00], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], @@ -30,23 +34,25 @@ $worksheet->fromArray($database, null, 'A4'); $worksheet->setCellValue('A12', 'The product of the yields of all Apple trees over 10\' in the orchard'); $worksheet->setCellValue('B12', '=DPRODUCT(A4:E10,"Yield",A1:B2)'); +$worksheet->setCellValue('A13', 'The product of the yields of all Apple trees in the orchard'); +$worksheet->setCellValue('B13', '=DPRODUCT(A4:E10,"Yield",A1:A2)'); + $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$helper->log('ALL'); +$criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSTDEV.php b/samples/Calculations/Database/DSTDEV.php index 7f09fa59..b6499741 100644 --- a/samples/Calculations/Database/DSTDEV.php +++ b/samples/Calculations/Database/DSTDEV.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Estimates the standard deviation based on a sample of selected database entries.'); +$category = 'Database'; +$functionName = 'DSTDEV'; +$description = 'Estimates the standard deviation based on a sample of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DSTDEV(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DSTDEV() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DSTDEV() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSTDEVP.php b/samples/Calculations/Database/DSTDEVP.php index 9e999a80..d8bcec4b 100644 --- a/samples/Calculations/Database/DSTDEVP.php +++ b/samples/Calculations/Database/DSTDEVP.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Calculates the standard deviation based on the entire population of selected database entries.'); + +$category = 'Database'; +$functionName = 'DSTDEVP'; +$description = 'Calculates the standard deviation based on the entire population of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -35,21 +40,19 @@ $worksheet->setCellValue('B13', '=DSTDEVP(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DSTDEVP() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DSTDEVP() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSUM.php b/samples/Calculations/Database/DSUM.php new file mode 100644 index 00000000..d2d773c9 --- /dev/null +++ b/samples/Calculations/Database/DSUM.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], + ['Apple', 18, 20, 14, 105.00], + ['Pear', 12, 12, 10, 96.00], + ['Cherry', 13, 14, 9, 105.00], + ['Apple', 14, 15, 10, 75.00], + ['Pear', 9, 8, 8, 76.80], + ['Apple', 8, 9, 6, 45.00], +]; +$criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], + ['="=Apple"', '>10', null, null, null, '<16'], + ['="=Pear"', null, null, null, null, null], +]; + +$worksheet->fromArray($criteria, null, 'A1'); +$worksheet->fromArray($database, null, 'A4'); + +$worksheet->setCellValue('A12', 'The total profit from apple trees'); +$worksheet->setCellValue('B12', '=DSUM(A4:E10,"Profit",A1:A2)'); + +$worksheet->setCellValue('A13', 'Total profit from apple trees with a height between 10 and 16 feet, and all pear trees'); +$worksheet->setCellValue('B13', '=DSUM(A4:E10,"Profit",A1:F3)'); + +$helper->log('Database'); + +$databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); +$helper->displayGrid($databaseData); + +// Test the formulae +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); + +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:F3', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DVAR.php b/samples/Calculations/Database/DVAR.php index 2a5f8749..4165d076 100644 --- a/samples/Calculations/Database/DVAR.php +++ b/samples/Calculations/Database/DVAR.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Estimates variance based on a sample from selected database entries.'); + +$category = 'Database'; +$functionName = 'DVAR'; +$description = 'Estimates variance based on a sample from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -35,21 +40,19 @@ $worksheet->setCellValue('B13', '=DVAR(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DVAR() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DVAR() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DVARP.php b/samples/Calculations/Database/DVARP.php index 4f57113b..5a17415e 100644 --- a/samples/Calculations/Database/DVARP.php +++ b/samples/Calculations/Database/DVARP.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Calculates variance based on the entire population of selected database entries,'); +$category = 'Database'; +$functionName = 'DVARP'; +$description = 'Calculates variance based on the entire population of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DVARP(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DVARP() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DVARP() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 8ce37003..aeb2bea6 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Helper; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\IWriter; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -182,6 +183,33 @@ class Sample echo date('H:i:s ') . $message . $eol; } + public function titles(string $category, string $functionName, ?string $description = null): void + { + $this->log(sprintf('%s Functions:', $category)); + $description === null + ? $this->log(sprintf('%s()', rtrim($functionName, '()'))) + : $this->log(sprintf('%s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + } + + public function displayGrid(array $matrix): void + { + $renderer = new TextGrid($matrix, $this->isCli()); + echo $renderer->render(); + } + + public function logCalculationResult( + Worksheet $worksheet, + string $functionName, + string $formulaCell, + ?string $descriptionCell = null + ): void { + if ($descriptionCell !== null) { + $this->log($worksheet->getCell($descriptionCell)->getValue()); + } + $this->log($worksheet->getCell($formulaCell)->getValue()); + $this->log(sprintf('%s() Result is ', $functionName) . $worksheet->getCell($formulaCell)->getCalculatedValue()); + } + /** * Log ending notes. */ diff --git a/src/PhpSpreadsheet/Helper/TextGrid.php b/src/PhpSpreadsheet/Helper/TextGrid.php new file mode 100644 index 00000000..acb9ae60 --- /dev/null +++ b/src/PhpSpreadsheet/Helper/TextGrid.php @@ -0,0 +1,139 @@ +rows = array_keys($matrix); + $this->columns = array_keys($matrix[$this->rows[0]]); + + $matrix = array_values($matrix); + array_walk( + $matrix, + function (&$row): void { + $row = array_values($row); + } + ); + + $this->matrix = $matrix; + $this->isCli = $isCli; + } + + public function render(): string + { + $this->gridDisplay = $this->isCli ? '' : ''; + + $maxRow = max($this->rows); + $maxRowLength = strlen((string) $maxRow) + 1; + $columnWidths = $this->getColumnWidths($this->matrix); + + $this->renderColumnHeader($maxRowLength, $columnWidths); + $this->renderRows($maxRowLength, $columnWidths); + $this->renderFooter($maxRowLength, $columnWidths); + + $this->gridDisplay .= $this->isCli ? '' : ''; + + return $this->gridDisplay; + } + + private function renderRows(int $maxRowLength, array $columnWidths): void + { + foreach ($this->matrix as $row => $rowData) { + $this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' '; + $this->renderCells($rowData, $columnWidths); + $this->gridDisplay .= '|' . PHP_EOL; + } + } + + private function renderCells(array $rowData, array $columnWidths): void + { + foreach ($rowData as $column => $cell) { + $cell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell); + $this->gridDisplay .= '| '; + $this->gridDisplay .= str_pad($cell, $columnWidths[$column] + 1, ' '); + } + } + + private function renderColumnHeader(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1); + } + $this->gridDisplay .= '+' . PHP_EOL; + + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' '); + } + $this->gridDisplay .= '|' . PHP_EOL; + + $this->renderFooter($maxRowLength, $columnWidths); + } + + private function renderFooter(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-'; + $this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-'); + } + $this->gridDisplay .= '+' . PHP_EOL; + } + + private function getColumnWidths(array $matrix): array + { + $columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix); + $columnWidths = []; + for ($column = 0; $column < $columnCount; ++$column) { + $columnWidths[] = $this->getColumnWidth(array_column($this->matrix, $column)); + } + + return $columnWidths; + } + + private function getColumnWidth(array $columnData): int + { + $columnWidth = 0; + $columnData = array_values($columnData); + + foreach ($columnData as $columnValue) { + if (is_string($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue)); + } elseif (is_bool($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue ? 'TRUE' : 'FALSE')); + } + + $columnWidth = max($columnWidth, strlen((string) $columnWidth)); + } + + return $columnWidth; + } +} From 5f33ec0eeafed69bc19e1d0c25b1995650964979 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:00:01 -0700 Subject: [PATCH 46/69] Phpstan Baseline < 4000 Lines Part 3 (#3041) The last of these changes for now. No remaining Phpstan complaints in any Writer/Xlsx. Number of lines remaining in Phpstan baseline is now below 3500. --- phpstan-baseline.neon | 195 ------------------ src/PhpSpreadsheet/Writer/Xlsx.php | 8 +- .../Writer/Xlsx/DefinedNames.php | 2 +- src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 7 +- .../Writer/Xlsx/StringTable.php | 54 +++-- src/PhpSpreadsheet/Writer/Xlsx/Style.php | 118 ++++++----- 6 files changed, 111 insertions(+), 273 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cfaed694..53fa9057 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3474,198 +3474,3 @@ parameters: message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xls\\\\Xf\\:\\:\\$diag is never read, only written\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xls/Xf.php - - - - message: "#^Argument of an invalid type array\\|null supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Parameter \\#1 \\$path of function basename expects string, array\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Parameter \\#1 \\$path of function dirname expects string, array\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Possibly invalid array key type array\\|string\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\:\\:\\$pathNames has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx.php - - - - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$type with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Parameter \\#2 \\$id of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeRelationship\\(\\) expects int, string given\\.$#" - count: 5 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Parameter \\#4 \\$target of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeRelationship\\(\\) expects string, array\\|string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php - - - - message: "#^Cannot call method getBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Instanceof between \\*NEVER\\* and PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\RichText will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Instanceof between string and PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\RichText will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#1 \\$text of method PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\RichText\\:\\:createTextRun\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 4 - path: src/PhpSpreadsheet/Writer/Xlsx/StringTable.php - - - - message: "#^Cannot call method getStyle\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Conditional\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Comparison operation \"\\<\" between int\\ and 0 is always true\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$borders of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeBorder\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Borders, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Borders\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$fill of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeFill\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Fill, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Fill\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$font of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeFont\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$numberFormat of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Style\\:\\:writeNumFmt\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\NumberFormat\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 22 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<1, max\\> given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, string\\|null given\\.$#" - count: 7 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php - - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Style.php diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 5aca5117..c9446e70 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -349,12 +349,15 @@ class Xlsx extends BaseWriter //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) if ($this->spreadSheet->hasRibbon()) { $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); + $tmpRibbonTarget = is_string($tmpRibbonTarget) ? $tmpRibbonTarget : ''; $zipContent[$tmpRibbonTarget] = $this->spreadSheet->getRibbonXMLData('data'); if ($this->spreadSheet->hasRibbonBinObjects()) { $tmpRootPath = dirname($tmpRibbonTarget) . '/'; $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write - foreach ($ribbonBinObjects as $aPath => $aContent) { - $zipContent[$tmpRootPath . $aPath] = $aContent; + if (is_array($ribbonBinObjects)) { + foreach ($ribbonBinObjects as $aPath => $aContent) { + $zipContent[$tmpRootPath . $aPath] = $aContent; + } } //the rels for files $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet); @@ -684,6 +687,7 @@ class Xlsx extends BaseWriter return $this; } + /** @var array */ private $pathNames = []; private function addZipFile(string $path, string $content): void diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index b8285fcb..b3338c07 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -118,7 +118,7 @@ class DefinedNames $range[1] = Coordinate::absoluteCoordinate($range[1]); $range = implode(':', $range); - $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle() ?? '') . '\'!' . $range); + $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range); $this->objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 238fb5bf..99fa2d34 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -67,12 +67,13 @@ class Rels extends WriterPart 'xl/workbook.xml' ); // a custom UI in workbook ? + $target = $spreadsheet->getRibbonXMLData('target'); if ($spreadsheet->hasRibbon()) { $this->writeRelationShip( $objWriter, 5, 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility', - $spreadsheet->getRibbonXMLData('target') + is_string($target) ? $target : '' ); } @@ -284,7 +285,7 @@ class Rels extends WriterPart return $objWriter->getData(); } - private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, $relationship, $type): void + private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, string $relationship, string $type): void { $unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData(); if (!isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()][$relationship])) { @@ -448,7 +449,7 @@ class Rels extends WriterPart /** * Write Override content type. * - * @param int $id Relationship ID. rId will be prepended! + * @param int|string $id Relationship ID. rId will be prepended! * @param string $type Relationship type * @param string $target Relationship target * @param string $targetMode Relationship target mode diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index 8b293bc1..078f940a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -66,7 +66,7 @@ class StringTable extends WriterPart /** * Write string table to XML format. * - * @param string[] $stringTable + * @param (string|RichText)[] $stringTable * * @return string XML Output */ @@ -86,13 +86,13 @@ class StringTable extends WriterPart // String table $objWriter->startElement('sst'); $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); - $objWriter->writeAttribute('uniqueCount', count($stringTable)); + $objWriter->writeAttribute('uniqueCount', (string) count($stringTable)); // Loop through string table foreach ($stringTable as $textElement) { $objWriter->startElement('si'); - if (!$textElement instanceof RichText) { + if (!($textElement instanceof RichText)) { $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement); $objWriter->startElement('t'); if ($textToWrite !== trim($textToWrite)) { @@ -100,7 +100,7 @@ class StringTable extends WriterPart } $objWriter->writeRawData($textToWrite); $objWriter->endElement(); - } elseif ($textElement instanceof RichText) { + } else { $this->writeRichText($objWriter, $textElement); } @@ -130,14 +130,16 @@ class StringTable extends WriterPart $objWriter->startElement($prefix . 'r'); // rPr - if ($element instanceof Run) { + if ($element instanceof Run && $element->getFont() !== null) { // rPr $objWriter->startElement($prefix . 'rPr'); // rFont - $objWriter->startElement($prefix . 'rFont'); - $objWriter->writeAttribute('val', $element->getFont()->getName()); - $objWriter->endElement(); + if ($element->getFont()->getName() !== null) { + $objWriter->startElement($prefix . 'rFont'); + $objWriter->writeAttribute('val', $element->getFont()->getName()); + $objWriter->endElement(); + } // Bold $objWriter->startElement($prefix . 'b'); @@ -166,19 +168,25 @@ class StringTable extends WriterPart $objWriter->endElement(); // Color - $objWriter->startElement($prefix . 'color'); - $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); - $objWriter->endElement(); + if ($element->getFont()->getColor()->getARGB() !== null) { + $objWriter->startElement($prefix . 'color'); + $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); + $objWriter->endElement(); + } // Size - $objWriter->startElement($prefix . 'sz'); - $objWriter->writeAttribute('val', $element->getFont()->getSize()); - $objWriter->endElement(); + if ($element->getFont()->getSize() !== null) { + $objWriter->startElement($prefix . 'sz'); + $objWriter->writeAttribute('val', (string) $element->getFont()->getSize()); + $objWriter->endElement(); + } // Underline - $objWriter->startElement($prefix . 'u'); - $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); - $objWriter->endElement(); + if ($element->getFont()->getUnderline() !== null) { + $objWriter->startElement($prefix . 'u'); + $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); + $objWriter->endElement(); + } $objWriter->endElement(); } @@ -201,10 +209,10 @@ class StringTable extends WriterPart */ public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = ''): void { - if (!$richText instanceof RichText) { + if (!($richText instanceof RichText)) { $textRun = $richText; $richText = new RichText(); - $run = $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun ?? ''); $run->setFont(null); } @@ -226,9 +234,9 @@ class StringTable extends WriterPart } // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? '1' : '0')); // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? '1' : '0')); // Underline $underlineType = $element->getFont()->getUnderline(); switch ($underlineType) { @@ -241,7 +249,9 @@ class StringTable extends WriterPart break; } - $objWriter->writeAttribute('u', $underlineType); + if ($underlineType !== null) { + $objWriter->writeAttribute('u', $underlineType); + } // Strikethrough $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); // Superscript/subscript diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php index cb2e3850..b24b7533 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -40,7 +40,7 @@ class Style extends WriterPart // numFmts $objWriter->startElement('numFmts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getNumFmtHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getNumFmtHashTable()->count()); // numFmt for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) { @@ -51,54 +51,63 @@ class Style extends WriterPart // fonts $objWriter->startElement('fonts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFontHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFontHashTable()->count()); // font for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) { - $this->writeFont($objWriter, $this->getParentWriter()->getFontHashTable()->getByIndex($i)); + $thisfont = $this->getParentWriter()->getFontHashTable()->getByIndex($i); + if ($thisfont !== null) { + $this->writeFont($objWriter, $thisfont); + } } $objWriter->endElement(); // fills $objWriter->startElement('fills'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFillHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFillHashTable()->count()); // fill for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) { - $this->writeFill($objWriter, $this->getParentWriter()->getFillHashTable()->getByIndex($i)); + $thisfill = $this->getParentWriter()->getFillHashTable()->getByIndex($i); + if ($thisfill !== null) { + $this->writeFill($objWriter, $thisfill); + } } $objWriter->endElement(); // borders $objWriter->startElement('borders'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getBordersHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getBordersHashTable()->count()); // border for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) { - $this->writeBorder($objWriter, $this->getParentWriter()->getBordersHashTable()->getByIndex($i)); + $thisborder = $this->getParentWriter()->getBordersHashTable()->getByIndex($i); + if ($thisborder !== null) { + $this->writeBorder($objWriter, $thisborder); + } } $objWriter->endElement(); // cellStyleXfs $objWriter->startElement('cellStyleXfs'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('numFmtId', 0); - $objWriter->writeAttribute('fontId', 0); - $objWriter->writeAttribute('fillId', 0); - $objWriter->writeAttribute('borderId', 0); + $objWriter->writeAttribute('numFmtId', '0'); + $objWriter->writeAttribute('fontId', '0'); + $objWriter->writeAttribute('fillId', '0'); + $objWriter->writeAttribute('borderId', '0'); $objWriter->endElement(); $objWriter->endElement(); // cellXfs $objWriter->startElement('cellXfs'); - $objWriter->writeAttribute('count', count($spreadsheet->getCellXfCollection())); + $objWriter->writeAttribute('count', (string) count($spreadsheet->getCellXfCollection())); // xf foreach ($spreadsheet->getCellXfCollection() as $cellXf) { @@ -109,24 +118,27 @@ class Style extends WriterPart // cellStyles $objWriter->startElement('cellStyles'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // cellStyle $objWriter->startElement('cellStyle'); $objWriter->writeAttribute('name', 'Normal'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('builtinId', 0); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('builtinId', '0'); $objWriter->endElement(); $objWriter->endElement(); // dxfs $objWriter->startElement('dxfs'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getStylesConditionalHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getStylesConditionalHashTable()->count()); // dxf for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) { - $this->writeCellStyleDxf($objWriter, $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i)->getStyle()); + $thisstyle = $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i); + if ($thisstyle !== null) { + $this->writeCellStyleDxf($objWriter, $thisstyle->getStyle()); + } } $objWriter->endElement(); @@ -171,17 +183,19 @@ class Style extends WriterPart // gradientFill $objWriter->startElement('gradientFill'); - $objWriter->writeAttribute('type', $fill->getFillType()); - $objWriter->writeAttribute('degree', $fill->getRotation()); + $objWriter->writeAttribute('type', (string) $fill->getFillType()); + $objWriter->writeAttribute('degree', (string) $fill->getRotation()); // stop $objWriter->startElement('stop'); $objWriter->writeAttribute('position', '0'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getStartColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -190,9 +204,11 @@ class Style extends WriterPart $objWriter->writeAttribute('position', '1'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getEndColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -220,7 +236,7 @@ class Style extends WriterPart // patternFill $objWriter->startElement('patternFill'); - $objWriter->writeAttribute('patternType', $fill->getFillType()); + $objWriter->writeAttribute('patternType', (string) $fill->getFillType()); if (self::writePatternColors($fill)) { // fgColor @@ -360,20 +376,20 @@ class Style extends WriterPart { // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('fontId', (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('fontId', (string) (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); if ($style->getQuotePrefix()) { - $objWriter->writeAttribute('quotePrefix', 1); + $objWriter->writeAttribute('quotePrefix', '1'); } if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { - $objWriter->writeAttribute('numFmtId', (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); + $objWriter->writeAttribute('numFmtId', (string) (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); } else { - $objWriter->writeAttribute('numFmtId', (int) $style->getNumberFormat()->getBuiltInFormatCode()); + $objWriter->writeAttribute('numFmtId', (string) (int) $style->getNumberFormat()->getBuiltInFormatCode()); } - $objWriter->writeAttribute('fillId', (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); - $objWriter->writeAttribute('borderId', (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); + $objWriter->writeAttribute('fillId', (string) (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); + $objWriter->writeAttribute('borderId', (string) (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); // Apply styles? $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $style->getFont()->getHashCode()) ? '1' : '0'); @@ -387,25 +403,25 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $objWriter->writeAttribute('horizontal', (string) $style->getAlignment()->getHorizontal()); + $objWriter->writeAttribute('vertical', (string) $style->getAlignment()->getVertical()); $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); $objWriter->writeAttribute('wrapText', ($style->getAlignment()->getWrapText() ? 'true' : 'false')); $objWriter->writeAttribute('shrinkToFit', ($style->getAlignment()->getShrinkToFit() ? 'true' : 'false')); if ($style->getAlignment()->getIndent() > 0) { - $objWriter->writeAttribute('indent', $style->getAlignment()->getIndent()); + $objWriter->writeAttribute('indent', (string) $style->getAlignment()->getIndent()); } if ($style->getAlignment()->getReadOrder() > 0) { - $objWriter->writeAttribute('readingOrder', $style->getAlignment()->getReadOrder()); + $objWriter->writeAttribute('readingOrder', (string) $style->getAlignment()->getReadOrder()); } $objWriter->endElement(); @@ -454,10 +470,10 @@ class Style extends WriterPart $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); } $objWriter->endElement(); @@ -465,7 +481,7 @@ class Style extends WriterPart $this->writeBorder($objWriter, $style->getBorders()); // protection - if (($style->getProtection()->getLocked() !== null) || ($style->getProtection()->getHidden() !== null)) { + if ((!empty($style->getProtection()->getLocked())) || (!empty($style->getProtection()->getHidden()))) { if ( $style->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT || $style->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT @@ -503,11 +519,13 @@ class Style extends WriterPart $objWriter->writeAttribute('style', $border->getBorderStyle()); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); - $objWriter->endElement(); + if ($border->getColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); + $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); + } } } @@ -516,15 +534,15 @@ class Style extends WriterPart * * @param int $id Number Format identifier */ - private function writeNumFmt(XMLWriter $objWriter, NumberFormat $numberFormat, $id = 0): void + private function writeNumFmt(XMLWriter $objWriter, ?NumberFormat $numberFormat, $id = 0): void { // Translate formatcode - $formatCode = $numberFormat->getFormatCode(); + $formatCode = ($numberFormat === null) ? null : $numberFormat->getFormatCode(); // numFmt if ($formatCode !== null) { $objWriter->startElement('numFmt'); - $objWriter->writeAttribute('numFmtId', ($id + 164)); + $objWriter->writeAttribute('numFmtId', (string) ($id + 164)); $objWriter->writeAttribute('formatCode', $formatCode); $objWriter->endElement(); } From 57a72037b5c8976e28e305ce72d45bc59369250a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 4 Sep 2022 09:45:29 -0700 Subject: [PATCH 47/69] Phpstan and Xlsx Reader (#3044) Eliminate most Phpstan messages in Xlsx Reader. In combination with similar changes to Xlsx Writer, baseline will shrink to just over 3,000 lines. --- phpstan-baseline.neon | 450 ------------------ src/PhpSpreadsheet/Reader/Xlsx.php | 82 ++-- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 14 +- .../Reader/Xlsx/BaseParserClass.php | 5 +- .../Reader/Xlsx/ColumnAndRowAttributes.php | 12 +- .../Reader/Xlsx/ConditionalStyles.php | 22 +- .../Reader/Xlsx/DataValidations.php | 4 +- src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php | 2 + src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php | 17 +- .../Reader/Xlsx/SheetViewOptions.php | 11 +- 10 files changed, 109 insertions(+), 510 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 53fa9057..cc2eaa3d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1660,456 +1660,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot access offset 0 on array\\\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot access property \\$r on SimpleXMLElement\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method addChart\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Comparison operation \"\\>\" between SimpleXMLElement\\|null and 0 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToBoolean\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToBoolean\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToError\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToError\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$calculatedValue with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$castBaseType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$cellDataType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$r with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$sharedFormulas with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToFormula\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToString\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:castToString\\(\\) has parameter \\$c with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has parameter \\$add with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:dirAdd\\(\\) has parameter \\$base with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getArrayItem\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getArrayItem\\(\\) has parameter \\$array with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getArrayItem\\(\\) has parameter \\$key with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:getFromZipArchive\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readFormControlProperties\\(\\) has parameter \\$dir with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readFormControlProperties\\(\\) has parameter \\$docSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readFormControlProperties\\(\\) has parameter \\$fileWorksheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readPrinterSettings\\(\\) has parameter \\$dir with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readPrinterSettings\\(\\) has parameter \\$docSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:readPrinterSettings\\(\\) has parameter \\$fileWorksheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:stripWhiteSpaceFromStyleString\\(\\) has parameter \\$string with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:toCSSArray\\(\\) has parameter \\$style with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Negated boolean expression is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$fontSizeInPoints of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:fontSizeToPixels\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$sizeInCm of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:centimeterSizeToPixels\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$sizeInInch of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:inchSizeToPixels\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#1 \\$worksheetName of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getSheetByName\\(\\) expects string, array\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, int\\|string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:readAutoFilter\\(\\) has parameter \\$autoFilterRange with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:readAutoFilter\\(\\) has parameter \\$xmlSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Parameter \\#1 \\$operator of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\AutoFilter\\\\Column\\\\Rule\\:\\:setRule\\(\\) expects string, null given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\AutoFilter\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\BaseParserClass\\:\\:boolean\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\BaseParserClass\\:\\:boolean\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredColumn\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredColumn\\(\\) has parameter \\$columnCoordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredRow\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:isFilteredRow\\(\\) has parameter \\$rowCoordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnAttributes\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnRangeAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readColumnRangeAttributes\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readRowAttributes\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:readRowAttributes\\(\\) has parameter \\$readDataOnly with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ColumnAndRowAttributes\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readConditionalStyles\\(\\) has parameter \\$xmlSheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarExtLstOfConditionalRule\\(\\) has parameter \\$cfRule with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarExtLstOfConditionalRule\\(\\) has parameter \\$conditionalFormattingRuleExtensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarOfConditionalRule\\(\\) has parameter \\$cfRule with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readDataBarOfConditionalRule\\(\\) has parameter \\$conditionalFormattingRuleExtensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readStyleRules\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readStyleRules\\(\\) has parameter \\$cfRules with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:readStyleRules\\(\\) has parameter \\$extLst with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:setConditionalStyles\\(\\) has parameter \\$xmlExtLst with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:\\$dxfs has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\ConditionalStyles\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\DataValidations\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\DataValidations\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Hyperlinks\\:\\:\\$hyperlinks has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\Hyperlinks\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:load\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:pageSetup\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\PageSetup\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\SheetViewOptions\\:\\:\\$worksheet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\\\SheetViewOptions\\:\\:\\$worksheetXml has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 630fd1dd..244baddd 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -31,6 +31,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Font; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Color; +use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; @@ -293,7 +294,7 @@ class Xlsx extends BaseReader return $worksheetInfo; } - private static function castToBoolean($c) + private static function castToBoolean(SimpleXMLElement $c): bool { $value = isset($c->v) ? (string) $c->v : null; if ($value == '0') { @@ -305,17 +306,21 @@ class Xlsx extends BaseReader return (bool) $c->v; } - private static function castToError($c) + private static function castToError(SimpleXMLElement $c): ?string { return isset($c->v) ? (string) $c->v : null; } - private static function castToString($c) + private static function castToString(SimpleXMLElement $c): ?string { return isset($c->v) ? (string) $c->v : null; } - private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType): void + /** + * @param mixed $value + * @param mixed $calculatedValue + */ + private function castToFormula(SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void { $attr = $c->f->attributes(); $cellDataType = 'f'; @@ -389,7 +394,7 @@ class Xlsx extends BaseReader $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); } - return $contents; + return ($contents === false) ? '' : $contents; } /** @@ -1143,7 +1148,7 @@ class Xlsx extends BaseReader } // Header/footer images - if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) { + if ($xmlSheet && $xmlSheet->legacyDrawingHF) { if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS); $vmlRelationship = ''; @@ -1550,7 +1555,7 @@ class Xlsx extends BaseReader break; case '_xlnm.Print_Area': - $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; $newRangeSets = []; foreach ($rangeSets as $rangeSet) { [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); @@ -1605,7 +1610,7 @@ class Xlsx extends BaseReader if (strpos((string) $definedName, '!') !== false) { $range[0] = str_replace("''", "'", $range[0]); $range[0] = str_replace("'", '', $range[0]); - if ($worksheet = $excel->getSheetByName($range[0])) { + if ($worksheet = $excel->getSheetByName($range[0])) { // @phpstan-ignore-line $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); } else { $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope)); @@ -1626,7 +1631,7 @@ class Xlsx extends BaseReader // Need to split on a comma or a space if not in quotes, and extract the first part. $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $definedRange); // Extract sheet name - [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); + [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); // @phpstan-ignore-line $extractedSheetName = trim($extractedSheetName, "'"); // Locate sheet @@ -1673,7 +1678,7 @@ class Xlsx extends BaseReader if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { - $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); + $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); // @phpstan-ignore-line $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); // For oneCellAnchor or absoluteAnchor positioned charts, // toCoordinate is not in the data. Does it need to be calculated? @@ -1721,28 +1726,29 @@ class Xlsx extends BaseReader if (isset($is->t)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { - if (is_object($is->r)) { + if (isset($is->r) && is_object($is->r)) { /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); } else { $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + $objFont = $objText->getFont() ?? new StyleFont(); if (isset($run->rPr->rFont)) { $attr = $run->rPr->rFont->attributes(); if (isset($attr['val'])) { - $objText->getFont()->setName((string) $attr['val']); + $objFont->setName((string) $attr['val']); } } if (isset($run->rPr->sz)) { $attr = $run->rPr->sz->attributes(); if (isset($attr['val'])) { - $objText->getFont()->setSize((float) $attr['val']); + $objFont->setSize((float) $attr['val']); } } if (isset($run->rPr->color)) { - $objText->getFont()->setColor(new Color($this->styleReader->readColor($run->rPr->color))); + $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color))); } if (isset($run->rPr->b)) { $attr = $run->rPr->b->attributes(); @@ -1750,7 +1756,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setBold(true); + $objFont->setBold(true); } } if (isset($run->rPr->i)) { @@ -1759,7 +1765,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setItalic(true); + $objFont->setItalic(true); } } if (isset($run->rPr->vertAlign)) { @@ -1767,19 +1773,19 @@ class Xlsx extends BaseReader if (isset($attr['val'])) { $vertAlign = strtolower((string) $attr['val']); if ($vertAlign == 'superscript') { - $objText->getFont()->setSuperscript(true); + $objFont->setSuperscript(true); } if ($vertAlign == 'subscript') { - $objText->getFont()->setSubscript(true); + $objFont->setSubscript(true); } } } if (isset($run->rPr->u)) { $attr = $run->rPr->u->attributes(); if (!isset($attr['val'])) { - $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); + $objFont->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); } else { - $objText->getFont()->setUnderline((string) $attr['val']); + $objFont->setUnderline((string) $attr['val']); } } if (isset($run->rPr->strike)) { @@ -1788,7 +1794,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setStrikethrough(true); + $objFont->setStrikethrough(true); } } } @@ -1841,17 +1847,30 @@ class Xlsx extends BaseReader } } + /** + * @param null|array|bool|SimpleXMLElement $array + * @param int|string $key + * + * @return mixed + */ private static function getArrayItem($array, $key = 0) { - return $array[$key] ?? null; + return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); } + /** + * @param null|SimpleXMLElement|string $base + * @param null|SimpleXMLElement|string $add + */ private static function dirAdd($base, $add): string { + $base = (string) $base; + $add = (string) $add; + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } - private static function toCSSArray($style): array + private static function toCSSArray(string $style): array { $style = self::stripWhiteSpaceFromStyleString($style); @@ -1865,15 +1884,15 @@ class Xlsx extends BaseReader } if (strpos($item[1], 'pt') !== false) { $item[1] = str_replace('pt', '', $item[1]); - $item[1] = Font::fontSizeToPixels($item[1]); + $item[1] = (string) Font::fontSizeToPixels((int) $item[1]); } if (strpos($item[1], 'in') !== false) { $item[1] = str_replace('in', '', $item[1]); - $item[1] = Font::inchSizeToPixels($item[1]); + $item[1] = (string) Font::inchSizeToPixels((int) $item[1]); } if (strpos($item[1], 'cm') !== false) { $item[1] = str_replace('cm', '', $item[1]); - $item[1] = Font::centimeterSizeToPixels($item[1]); + $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]); } $style[$item[0]] = $item[1]; @@ -1882,11 +1901,14 @@ class Xlsx extends BaseReader return $style; } - public static function stripWhiteSpaceFromStyleString($string): string + public static function stripWhiteSpaceFromStyleString(string $string): string { return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); } + /** + * @param mixed $value + */ private static function boolean($value): bool { if (is_object($value)) { @@ -1955,7 +1977,7 @@ class Xlsx extends BaseReader return $returnValue; } - private function readFormControlProperties(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -1982,7 +2004,7 @@ class Xlsx extends BaseReader unset($unparsedCtrlProps); } - private function readPrinterSettings(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -2070,7 +2092,7 @@ class Xlsx extends BaseReader if ($xmlSheet && $xmlSheet->autoFilter) { // In older files, autofilter structure is defined in the worksheet file (new AutoFilter($docSheet, $xmlSheet))->load(); - } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) { + } elseif ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { // But for Office365, MS decided to make it all just a bit more complicated $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 374da9f6..623c6691 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class AutoFilter { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -28,7 +30,7 @@ class AutoFilter } } - private function readAutoFilter($autoFilterRange, $xmlSheet): void + private function readAutoFilter(string $autoFilterRange, SimpleXMLElement $xmlSheet): void { $autoFilter = $this->worksheet->getAutoFilter(); $autoFilter->setRange($autoFilterRange); @@ -39,15 +41,15 @@ class AutoFilter if ($filterColumn->filters) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); $filters = $filterColumn->filters; - if ((isset($filters['blank'])) && ($filters['blank'] == 1)) { + if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Standard filters are always an OR join, so no join rule needs to be set // Entries can be either filter elements foreach ($filters->filter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Or Date Group elements @@ -69,7 +71,7 @@ class AutoFilter foreach ($filters->dateGroupItem as $dateGroupItem) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', [ 'year' => (string) $dateGroupItem['year'], 'month' => (string) $dateGroupItem['month'], @@ -110,7 +112,7 @@ class AutoFilter foreach ($filterColumn->dynamicFilter as $filterRule) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', (string) $filterRule['val'], (string) $filterRule['type'] )->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php b/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php index 1679f01f..2f146458 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; class BaseParserClass { - protected static function boolean($value) + /** + * @param mixed $value + */ + protected static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php index 2a1e2afd..34705733 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -10,8 +10,10 @@ use SimpleXMLElement; class ColumnAndRowAttributes extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -120,7 +122,7 @@ class ColumnAndRowAttributes extends BaseParserClass } } - private function isFilteredColumn(IReadFilter $readFilter, $columnCoordinate, array $rowsAttributes) + private function isFilteredColumn(IReadFilter $readFilter, string $columnCoordinate, array $rowsAttributes): bool { foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -131,7 +133,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readColumnAttributes(SimpleXMLElement $worksheetCols, $readDataOnly) + private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array { $columnAttributes = []; @@ -151,7 +153,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function readColumnRangeAttributes(SimpleXMLElement $column, $readDataOnly) + private function readColumnRangeAttributes(SimpleXMLElement $column, bool $readDataOnly): array { $columnAttributes = []; @@ -172,7 +174,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function isFilteredRow(IReadFilter $readFilter, $rowCoordinate, array $columnsAttributes) + private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool { foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -183,7 +185,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readRowAttributes(SimpleXMLElement $worksheetRow, $readDataOnly) + private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly): array { $rowAttributes = []; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index c631a0fe..7d947bac 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -10,11 +10,14 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueO use PhpOffice\PhpSpreadsheet\Style\Style as Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; +use stdClass; class ConditionalStyles { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; /** @@ -22,6 +25,7 @@ class ConditionalStyles */ private $ns; + /** @var array */ private $dxfs; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs = []) @@ -146,7 +150,7 @@ class ConditionalStyles return $cfStyle; } - private function readConditionalStyles($xmlSheet): array + private function readConditionalStyles(SimpleXMLElement $xmlSheet): array { $conditionals = []; foreach ($xmlSheet->conditionalFormatting as $conditional) { @@ -162,7 +166,7 @@ class ConditionalStyles return $conditionals; } - private function setConditionalStyles(Worksheet $worksheet, array $conditionals, $xmlExtLst): void + private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void { foreach ($conditionals as $cellRangeReference => $cfRules) { ksort($cfRules); @@ -176,7 +180,7 @@ class ConditionalStyles } } - private function readStyleRules($cfRules, $extLst) + private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array { $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); $conditionalStyles = []; @@ -213,7 +217,7 @@ class ConditionalStyles if (isset($cfRule->dataBar)) { $objConditional->setDataBar( - $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) + $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) // @phpstan-ignore-line ); } else { $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); @@ -225,7 +229,10 @@ class ConditionalStyles return $conditionalStyles; } - private function readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions): ConditionalDataBar + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarOfConditionalRule($cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar { $dataBar = new ConditionalDataBar(); //dataBar attribute @@ -257,7 +264,10 @@ class ConditionalStyles return $dataBar; } - private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, $conditionalFormattingRuleExtensions): void + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, array $conditionalFormattingRuleExtensions): void { if (isset($cfRule->extLst)) { $ns = $cfRule->extLst->getNamespaces(true); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index b699cb57..dac76230 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class DataValidations { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -22,7 +24,7 @@ class DataValidations { foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { // Uppercase coordinate - $range = strtoupper($dataValidation['sqref']); + $range = strtoupper((string) $dataValidation['sqref']); $rangeSet = explode(' ', $range); foreach ($rangeSet as $range) { $stRange = $this->worksheet->shrinkRangeToFit($range); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php index 84884996..7d48c796 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class Hyperlinks { + /** @var Worksheet */ private $worksheet; + /** @var array */ private $hyperlinks = []; public function __construct(Worksheet $workSheet) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php index 56f18f98..08decd6e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class PageSetup extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -18,16 +20,17 @@ class PageSetup extends BaseParserClass $this->worksheetXml = $worksheetXml; } - public function load(array $unparsedLoadedData) + public function load(array $unparsedLoadedData): array { - if (!$this->worksheetXml) { + $worksheetXml = $this->worksheetXml; + if ($worksheetXml === null) { return $unparsedLoadedData; } - $this->margins($this->worksheetXml, $this->worksheet); - $unparsedLoadedData = $this->pageSetup($this->worksheetXml, $this->worksheet, $unparsedLoadedData); - $this->headerFooter($this->worksheetXml, $this->worksheet); - $this->pageBreaks($this->worksheetXml, $this->worksheet); + $this->margins($worksheetXml, $this->worksheet); + $unparsedLoadedData = $this->pageSetup($worksheetXml, $this->worksheet, $unparsedLoadedData); + $this->headerFooter($worksheetXml, $this->worksheet); + $this->pageBreaks($worksheetXml, $this->worksheet); return $unparsedLoadedData; } @@ -45,7 +48,7 @@ class PageSetup extends BaseParserClass } } - private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData) + private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array { if ($xmlSheet->pageSetup) { $docPageSetup = $worksheet->getPageSetup(); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php index a302cc56..9c02da9f 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -7,8 +7,10 @@ use SimpleXMLElement; class SheetViewOptions extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -24,10 +26,11 @@ class SheetViewOptions extends BaseParserClass } if (isset($this->worksheetXml->sheetPr)) { - $this->tabColor($this->worksheetXml->sheetPr, $styleReader); - $this->codeName($this->worksheetXml->sheetPr); - $this->outlines($this->worksheetXml->sheetPr); - $this->pageSetup($this->worksheetXml->sheetPr); + $sheetPr = $this->worksheetXml->sheetPr; + $this->tabColor($sheetPr, $styleReader); + $this->codeName($sheetPr); + $this->outlines($sheetPr); + $this->pageSetup($sheetPr); } if (isset($this->worksheetXml->sheetFormatPr)) { From adbda6391257041f4925c0f497bf0c17af0bfd67 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 4 Sep 2022 10:43:31 -0700 Subject: [PATCH 48/69] Phpstan and Xlsx Reader (#3043) Eliminate most Phpstan messages in Xlsx Reader. In combination with similar changes to Xlsx Writer, baseline will shrink to just over 3,000 lines. --- samples/Chart/33_Chart_create_scatter2.php | 6 +- samples/Chart/33_Chart_create_scatter3.php | 6 +- .../33_Chart_create_scatter5_trendlines.php | 18 +-- src/PhpSpreadsheet/Chart/DataSeriesValues.php | 10 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 4 +- src/PhpSpreadsheet/Shared/Font.php | 2 +- src/PhpSpreadsheet/Shared/StringHelper.php | 16 ++- src/PhpSpreadsheet/Style/Font.php | 8 +- src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 3 - src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 3 - .../Calculation/FormulaParserTest.php | 2 +- .../Chart/AxisGlowTest.php | 6 +- .../Chart/AxisShadowTest.php | 6 +- .../Chart/GridlinesShadowGlowTest.php | 6 +- .../Chart/Issue2506Test.php | 2 +- .../Chart/MultiplierTest.php | 4 +- .../Chart/ShadowPresetsTest.php | 7 +- .../Reader/Xlsx/AutoFilter2Test.php | 13 +- .../Reader/Xlsx/RibbonTest.php | 10 +- tests/PhpSpreadsheetTests/RichTextTest.php | 4 +- .../SpreadsheetCoverageTest.php | 135 +++++++++--------- .../Worksheet/ColumnCellIteratorTest.php | 3 + .../Worksheet/RowCellIteratorTest.php | 3 + 23 files changed, 146 insertions(+), 131 deletions(-) diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index 59310620..cbc3ec1a 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -1,6 +1,6 @@ setScatterLines(false); // points not connected // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers -$xAxis = new Axis(); +$xAxis = new ChartAxis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); $xAxis->setAxisOption('textRotation', '45'); -$yAxis = new Axis(); +$yAxis = new ChartAxis(); $yAxis->setLineStyleProperties( 2.5, // width in points Properties::LINE_STYLE_COMPOUND_SIMPLE, diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php index f6f9a6f4..899fca79 100644 --- a/samples/Chart/33_Chart_create_scatter3.php +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -1,6 +1,6 @@ setScatterLines(false); // points not connected // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers -$xAxis = new Axis(); +$xAxis = new ChartAxis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); $xAxis->setAxisOption('textRotation', '45'); $xAxis->setAxisOption('hidden', '1'); -$yAxis = new Axis(); +$yAxis = new ChartAxis(); $yAxis->setLineStyleProperties( 2.5, // width in points Properties::LINE_STYLE_COMPOUND_SIMPLE, diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php index 5beb82cd..dcee3e14 100644 --- a/samples/Chart/33_Chart_create_scatter5_trendlines.php +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -1,6 +1,6 @@ getMarkerFillColor() - ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0] ->getMarkerBorderColor() - ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); // line details - dashed, smooth line (Bezier) with arrows, 40% transparent $dataSeriesValues[0] @@ -105,24 +105,24 @@ $dataSeriesValues[1] // square marker border color ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[1] // square marker fill color ->getMarkerFillColor() - ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1] ->setScatterLines(true) ->setSmoothLine(false) - ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1]->setLineWidth(2.0); // series 3 - metric3, markers, no line $dataSeriesValues[2] // triangle? fill //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle ->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[2] // triangle border ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[2]->setScatterLines(false); // points not connected // Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers -$xAxis = new Axis(); +$xAxis = new ChartAxis(); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); // Build the dataseries @@ -204,7 +204,7 @@ $dataSeriesValues[0]->setTrendLines($trendLines); $dataSeriesValues[0]->setScatterLines(false); // points not connected $dataSeriesValues[0]->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0]->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); @@ -218,7 +218,7 @@ $dataSeriesValues[0]->getTrendLines()[1]->setLineStyleProperties(1.25); $dataSeriesValues[0]->getTrendLines()[2]->getLineColor()->setColorProperties('accent2', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[0]->getTrendLines()[2]->setLineStyleProperties(1.5, null, null, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); -$xAxis = new Axis(); +$xAxis = new ChartAxis(); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // m/d/yyyy // Build the dataseries diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 7d29e9c4..cd166b23 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -324,13 +324,13 @@ class DataSeriesValues extends Properties if (is_array($this->fillColor)) { $array = []; foreach ($this->fillColor as $chartColor) { - $array[] = self::chartColorToString($chartColor); + $array[] = $this->chartColorToString($chartColor); } return $array; } - return self::chartColorToString($this->fillColor); + return $this->chartColorToString($this->fillColor); } /** @@ -348,13 +348,13 @@ class DataSeriesValues extends Properties if ($fillString instanceof ChartColor) { $this->fillColor[] = $fillString; } else { - $this->fillColor[] = self::stringToChartColor($fillString); + $this->fillColor[] = $this->stringToChartColor($fillString); } } } elseif ($color instanceof ChartColor) { $this->fillColor = $color; - } elseif (is_string($color)) { - $this->fillColor = self::stringToChartColor($color); + } else { + $this->fillColor = $this->stringToChartColor($color); } return $this; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 9eb30256..507c8f5b 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -311,7 +311,7 @@ class Chart break; case 'stockChart': $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); - $plotAttributes = $this->readChartAttributes($plotAreaLayout); + $plotAttributes = $this->readChartAttributes($chartDetail); break; } @@ -1068,7 +1068,7 @@ class Chart } /** - * @param null|Layout|SimpleXMLElement $chartDetail + * @param ?SimpleXMLElement $chartDetail */ private function readChartAttributes($chartDetail): array { diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 33796b4c..e90c679b 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -349,7 +349,7 @@ class Font // Special case if there are one or more newline characters ("\n") $cellText = $cellText ?? ''; - if (strpos($cellText, "\n") !== false) { + if (strpos(/** @scrutinizer ignore-type */ $cellText, "\n") !== false) { $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 0fe10e4d..16026c3c 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -337,9 +337,19 @@ class StringHelper mb_substitute_character(65533); // Unicode substitution character // Phpstan does not think this can return false. $returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); - mb_substitute_character($subst); + mb_substitute_character(/** @scrutinizer ignore-type */ $subst); - return $returnValue; + return self::returnString($returnValue); + } + + /** + * Strictly to satisfy Scrutinizer. + * + * @param mixed $value + */ + private static function returnString($value): string + { + return is_string($value) ? $value : ''; } /** @@ -433,7 +443,7 @@ class StringHelper } } - return mb_convert_encoding($textValue, $to, $from); + return self::returnString(mb_convert_encoding($textValue, $to, $from)); } /** diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 19d67563..3d7bc1bc 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -743,14 +743,14 @@ class Font extends Supervisor private function hashChartColor(?ChartColor $underlineColor): string { - if ($this->underlineColor === null) { + if ($underlineColor === null) { return ''; } return - $this->underlineColor->getValue() - . $this->underlineColor->getType() - . (string) $this->underlineColor->getAlpha(); + $underlineColor->getValue() + . $underlineColor->getType() + . (string) $underlineColor->getAlpha(); } /** diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index cd17cccf..690b0c54 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -33,9 +33,6 @@ class Dompdf extends Pdf { $fileHandle = parent::prepareForSave($filename); - // Default PDF paper size - $paperSize = 'LETTER'; // Letter (8.5 in. by 11 in.) - // Check for paper size and page orientation $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup(); $orientation = $this->getOrientation() ?? $setup->getOrientation(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 8dab9529..3b64bd8e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -233,8 +233,6 @@ class Chart extends WriterPart if ($plotArea === null) { return; } - $majorGridlines = ($yAxis === null) ? null : $yAxis->getMajorGridlines(); - $minorGridlines = ($yAxis === null) ? null : $yAxis->getMinorGridlines(); $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; @@ -1163,7 +1161,6 @@ class Chart extends WriterPart $intercept = $trendLine->getIntercept(); $name = $trendLine->getName(); $trendLineColor = $trendLine->getLineColor(); // ChartColor - $trendLineWidth = $trendLine->getLineStyleProperty('width'); $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' if ($name !== '') { diff --git a/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php index 4682c6b2..d401f9c9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php @@ -12,7 +12,7 @@ class FormulaParserTest extends TestCase { $this->expectException(CalcException::class); $this->expectExceptionMessage('Invalid parameter passed: formula'); - $result = new FormulaParser(null); + new FormulaParser(null); } public function testInvalidTokenId(): void diff --git a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php index 0ed4a1b4..b14d63fb 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -107,7 +107,7 @@ class AxisGlowTest extends AbstractFunctional $yAxis = $chart->getChartAxisY(); $xAxis = $chart->getChartAxisX(); $yGlowSize = 10.0; - $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, ChartColor::EXCEL_COLOR_TYPE_RGB); $expectedGlowColor = [ 'type' => 'srgbClr', 'value' => 'FFFF00', @@ -231,7 +231,7 @@ class AxisGlowTest extends AbstractFunctional ); $yAxis = $chart->getChartAxisX(); // deliberate $yGlowSize = 20.0; - $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, Properties::EXCEL_COLOR_TYPE_SCHEME); + $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $expectedGlowColor = [ 'type' => 'schemeClr', 'value' => 'accent1', diff --git a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php index d6f122ef..78deece4 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -113,7 +113,7 @@ class AxisShadowTest extends AbstractFunctional 'distance' => 3, 'rotWithShape' => 0, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -139,7 +139,7 @@ class AxisShadowTest extends AbstractFunctional 'ky' => null, ], 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'type' => ChartColor::EXCEL_COLOR_TYPE_RGB, 'value' => 'FF0000', 'alpha' => 20, ], diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php index e2c91eba..e1215441 100644 --- a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php @@ -4,12 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -98,7 +98,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional $majorGridlines = new GridLines(); $yAxis->setMajorGridlines($majorGridlines); $majorGlowSize = 10.0; - $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, ChartColor::EXCEL_COLOR_TYPE_RGB); $softEdgeSize = 2.5; $majorGridlines->setSoftEdges($softEdgeSize); $expectedGlowColor = [ @@ -122,7 +122,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional 'distance' => 3, 'rotWithShape' => 0, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php index a2a14c9d..e1c30770 100644 --- a/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php +++ b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php @@ -23,7 +23,7 @@ class Issue2506Test extends AbstractFunctional public function testDataSeriesValues(): void { $reader = new XlsxReader(); - self::readCharts($reader); + $this->readCharts($reader); $spreadsheet = $reader->load(self::DIRECTORY . 'issue.2506.xlsx'); $worksheet = $spreadsheet->getActiveSheet(); $charts = $worksheet->getChartCollection(); diff --git a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php index 35161ff7..db602e8a 100644 --- a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php +++ b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -109,7 +109,7 @@ class MultiplierTest extends TestCase 'ky' => null, ], 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'type' => ChartColor::EXCEL_COLOR_TYPE_RGB, 'value' => 'FF0000', 'alpha' => 20, ], diff --git a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php index e96d6c14..20cacbc1 100644 --- a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PHPUnit\Framework\TestCase; @@ -131,7 +132,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -160,7 +161,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -189,7 +190,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 6d6949d8..0bb9f130 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -11,12 +11,14 @@ class AutoFilter2Test extends TestCase { private const TESTBOOK = 'tests/data/Reader/XLSX/autofilter2.xlsx'; - public function getVisibleSheet(Worksheet $sheet, int $maxRow): array + public function getVisibleSheet(?Worksheet $sheet, int $maxRow): array { $actualVisible = []; - for ($row = 2; $row <= $maxRow; ++$row) { - if ($sheet->getRowDimension($row)->getVisible()) { - $actualVisible[] = $row; + if ($sheet !== null) { + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } } } @@ -35,13 +37,14 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; self::assertSame(Rule::AUTOFILTER_RULETYPE_DATEGROUP, $rule->getRuleType()); $value = $rule->getValue(); self::assertIsArray($value); - self::assertCount(6, $value); + self::assertCount(6, /** @scrutinizer ignore-type */ $value); self::assertSame('2002', $value['year']); self::assertSame('', $value['month']); self::assertSame('', $value['day']); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php index ab304e7b..68b3bb01 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -24,14 +24,14 @@ class RibbonTest extends AbstractFunctional self::assertSame('customUI/customUI.xml', $target); $data = $spreadsheet->getRibbonXMLData('data'); self::assertIsString($data); - self::assertSame(1522, strlen($data)); + self::assertSame(1522, strlen(/** @scrutinizer ignore-type */ $data)); $vbaCode = (string) $spreadsheet->getMacrosCode(); self::assertSame(13312, strlen($vbaCode)); self::assertNull($spreadsheet->getRibbonBinObjects()); - self::assertNull($spreadsheet->getRibbonBinObjects('names')); - self::assertNull($spreadsheet->getRibbonBinObjects('data')); + foreach (['names', 'data', 'xxxxx'] as $type) { + self::assertNull($spreadsheet->getRibbonBinObjects($type), "Expecting null when type is $type"); + } self::assertEmpty($spreadsheet->getRibbonBinObjects('types')); - self::assertNull($spreadsheet->getRibbonBinObjects('xxxxx')); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); $spreadsheet->disconnectWorksheets(); @@ -58,7 +58,7 @@ class RibbonTest extends AbstractFunctional self::assertSame('customUI/customUI.xml', $target); $data = $spreadsheet->getRibbonXMLData('data'); self::assertIsString($data); - self::assertSame(1522, strlen($data)); + self::assertSame(1522, strlen(/** @scrutinizer ignore-type */ $data)); $vbaCode = (string) $spreadsheet->getMacrosCode(); self::assertSame(13312, strlen($vbaCode)); $spreadsheet->discardMacros(); diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index e2dfcce7..49878529 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -28,7 +28,9 @@ class RichTextTest extends TestCase public function testTextElements(): void { $element1 = new TextElement('A'); - self::assertNull($element1->getFont()); + if ($element1->getFont() !== null) { + self::fail('Expected font to be null'); + } $element2 = new TextElement('B'); $element3 = new TextElement('C'); $richText = new RichText(); diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php index 584c53fe..a455a7d2 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -9,81 +9,93 @@ use PHPUnit\Framework\TestCase; class SpreadsheetCoverageTest extends TestCase { + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var ?Spreadsheet */ + private $spreadsheet2; + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->spreadsheet2 !== null) { + $this->spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2 = null; + } + } + public function testDocumentProperties(): void { - $spreadsheet = new Spreadsheet(); - $properties = $spreadsheet->getProperties(); + $this->spreadsheet = new Spreadsheet(); + $properties = $this->spreadsheet->getProperties(); $properties->setCreator('Anyone'); $properties->setTitle('Description'); - $spreadsheet2 = new Spreadsheet(); - self::assertNotEquals($properties, $spreadsheet2->getProperties()); + $this->spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $this->spreadsheet2->getProperties()); $properties2 = clone $properties; - $spreadsheet2->setProperties($properties2); - self::assertEquals($properties, $spreadsheet2->getProperties()); - $spreadsheet->disconnectWorksheets(); - $spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $this->spreadsheet2->getProperties()); } public function testDocumentSecurity(): void { - $spreadsheet = new Spreadsheet(); - $security = $spreadsheet->getSecurity(); + $this->spreadsheet = new Spreadsheet(); + $security = $this->spreadsheet->getSecurity(); $security->setLockRevision(true); $revisionsPassword = 'revpasswd'; $security->setRevisionsPassword($revisionsPassword); - $spreadsheet2 = new Spreadsheet(); - self::assertNotEquals($security, $spreadsheet2->getSecurity()); + $this->spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $this->spreadsheet2->getSecurity()); $security2 = clone $security; - $spreadsheet2->setSecurity($security2); - self::assertEquals($security, $spreadsheet2->getSecurity()); - $spreadsheet->disconnectWorksheets(); - $spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2->setSecurity($security2); + self::assertEquals($security, $this->spreadsheet2->getSecurity()); } public function testCellXfCollection(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $sheet->getStyle('A1')->getFont()->setName('font1'); $sheet->getStyle('A2')->getFont()->setName('font2'); $sheet->getStyle('A3')->getFont()->setName('font3'); $sheet->getStyle('B1')->getFont()->setName('font1'); $sheet->getStyle('B2')->getFont()->setName('font2'); - $collection = $spreadsheet->getCellXfCollection(); + $collection = $this->spreadsheet->getCellXfCollection(); self::assertCount(4, $collection); $font1Style = $collection[1]; - self::assertTrue($spreadsheet->cellXfExists($font1Style)); - self::assertSame('font1', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertTrue($this->spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $this->spreadsheet->getCellXfCollection()[1]->getFont()->getName()); self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); - $spreadsheet->removeCellXfByIndex(1); - self::assertFalse($spreadsheet->cellXfExists($font1Style)); - self::assertSame('font2', $spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + $this->spreadsheet->removeCellXfByIndex(1); + self::assertFalse($this->spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $this->spreadsheet->getCellXfCollection()[1]->getFont()->getName()); self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); - $spreadsheet->disconnectWorksheets(); } public function testInvalidRemoveCellXfByIndex(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('CellXf index is out of bounds.'); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $sheet->getStyle('A1')->getFont()->setName('font1'); $sheet->getStyle('A2')->getFont()->setName('font2'); $sheet->getStyle('A3')->getFont()->setName('font3'); $sheet->getStyle('B1')->getFont()->setName('font1'); $sheet->getStyle('B2')->getFont()->setName('font2'); - $spreadsheet->removeCellXfByIndex(5); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet->removeCellXfByIndex(5); } public function testInvalidRemoveDefaultStyle(): void @@ -91,71 +103,63 @@ class SpreadsheetCoverageTest extends TestCase $this->expectException(SSException::class); $this->expectExceptionMessage('No default style found for this workbook'); // Removing default style probably should be disallowed. - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $spreadsheet->removeCellXfByIndex(0); - $style = $spreadsheet->getDefaultStyle(); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->removeCellXfByIndex(0); + $this->spreadsheet->getDefaultStyle(); } public function testCellStyleXF(): void { - $spreadsheet = new Spreadsheet(); - $collection = $spreadsheet->getCellStyleXfCollection(); + $this->spreadsheet = new Spreadsheet(); + $collection = $this->spreadsheet->getCellStyleXfCollection(); self::assertCount(1, $collection); $styleXf = $collection[0]; - self::assertSame($styleXf, $spreadsheet->getCellStyleXfByIndex(0)); + self::assertSame($styleXf, $this->spreadsheet->getCellStyleXfByIndex(0)); $hash = $styleXf->getHashCode(); - self::assertSame($styleXf, $spreadsheet->getCellStyleXfByHashCode($hash)); - self::assertFalse($spreadsheet->getCellStyleXfByHashCode($hash . 'x')); - $spreadsheet->disconnectWorksheets(); + self::assertSame($styleXf, $this->spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($this->spreadsheet->getCellStyleXfByHashCode($hash . 'x')); } public function testInvalidRemoveCellStyleXfByIndex(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $spreadsheet->removeCellStyleXfByIndex(5); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->removeCellStyleXfByIndex(5); } public function testInvalidFirstSheetIndex(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('First sheet index must be a positive integer.'); - $spreadsheet = new Spreadsheet(); - $spreadsheet->setFirstSheetIndex(-1); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setFirstSheetIndex(-1); } public function testInvalidVisibility(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('Invalid visibility value.'); - $spreadsheet = new Spreadsheet(); - $spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); - self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $spreadsheet->getVisibility()); - $spreadsheet->setVisibility(null); - self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $spreadsheet->getVisibility()); - $spreadsheet->setVisibility('badvalue'); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $this->spreadsheet->getVisibility()); + $this->spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $this->spreadsheet->getVisibility()); + $this->spreadsheet->setVisibility('badvalue'); } public function testInvalidTabRatio(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); - $spreadsheet = new Spreadsheet(); - $spreadsheet->setTabRatio(2000); - $spreadsheet->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setTabRatio(2000); } public function testCopy(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $sheet->getStyle('A1')->getFont()->setName('font1'); $sheet->getStyle('A2')->getFont()->setName('font2'); $sheet->getStyle('A3')->getFont()->setName('font3'); @@ -166,8 +170,8 @@ class SpreadsheetCoverageTest extends TestCase $sheet->getCell('A3')->setValue('this is a3'); $sheet->getCell('B1')->setValue('this is b1'); $sheet->getCell('B2')->setValue('this is b2'); - $copied = $spreadsheet->copy(); - $copysheet = $copied->getActiveSheet(); + $this->spreadsheet2 = $this->spreadsheet->copy(); + $copysheet = $this->spreadsheet2->getActiveSheet(); $copysheet->getStyle('A2')->getFont()->setName('font12'); $copysheet->getCell('A2')->setValue('this was a2'); @@ -192,18 +196,13 @@ class SpreadsheetCoverageTest extends TestCase self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); - - $spreadsheet->disconnectWorksheets(); - $copied->disconnectWorksheets(); } public function testClone(): void { $this->expectException(SSException::class); $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); - $spreadsheet = new Spreadsheet(); - $clone = clone $spreadsheet; - $spreadsheet->disconnectWorksheets(); - $clone->disconnectWorksheets(); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet2 = clone $this->spreadsheet; } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php index 1a7ff675..02b96426 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php @@ -40,6 +40,7 @@ class ColumnCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $ColumnCell) { self::assertNotNull($ColumnCell); + /** @scrutinizer ignore-call */ $values[] = $ColumnCell->getValue(); self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); @@ -60,6 +61,7 @@ class ColumnCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $ColumnCell) { self::assertNotNull($ColumnCell); + /** @scrutinizer ignore-call */ $values[] = $ColumnCell->getValue(); self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); @@ -81,6 +83,7 @@ class ColumnCellIteratorTest extends TestCase while ($iterator->valid()) { $current = $iterator->current(); self::assertNotNull($current); + /** @scrutinizer ignore-call */ $cell = $current->getCoordinate(); $values[] = $sheet->getCell($cell)->getValue(); $iterator->prev(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php index b0606804..9afda3cb 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php @@ -40,6 +40,7 @@ class RowCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $RowCell) { self::assertNotNull($RowCell); + /** @scrutinizer ignore-call */ $values[] = $RowCell->getValue(); self::assertEquals($RowCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $RowCell); @@ -59,6 +60,7 @@ class RowCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $RowCell) { self::assertNotNull($RowCell); + /** @scrutinizer ignore-call */ $values[] = $RowCell->getValue(); self::assertEquals($RowCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $RowCell); @@ -80,6 +82,7 @@ class RowCellIteratorTest extends TestCase while ($iterator->valid()) { $current = $iterator->current(); self::assertNotNull($current); + /** @scrutinizer ignore-call */ $cell = $current->getCoordinate(); $values[] = $sheet->getCell($cell)->getValue(); $iterator->prev(); From 3884aa74773d4b9b3275176c633cf9f8f3610245 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 3 Sep 2022 22:03:16 +0200 Subject: [PATCH 49/69] Update Excel function samples for Date/Time functions --- samples/Calculations/DateTime/DATE.php | 16 ++++-- samples/Calculations/DateTime/DATEDIF.php | 60 ++++++++++++++++++++ samples/Calculations/DateTime/DATEVALUE.php | 18 +++--- samples/Calculations/DateTime/DAY.php | 50 ++++++++++++++++ samples/Calculations/DateTime/DAYS.php | 52 +++++++++++++++++ samples/Calculations/DateTime/DAYS360.php | 57 +++++++++++++++++++ samples/Calculations/DateTime/EDATE.php | 43 ++++++++++++++ samples/Calculations/DateTime/EOMONTH.php | 43 ++++++++++++++ samples/Calculations/DateTime/HOUR.php | 48 ++++++++++++++++ samples/Calculations/DateTime/ISOWEEKNUM.php | 50 ++++++++++++++++ samples/Calculations/DateTime/MINUTE.php | 48 ++++++++++++++++ samples/Calculations/DateTime/MONTH.php | 50 ++++++++++++++++ samples/Calculations/DateTime/NOW.php | 27 +++++++++ samples/Calculations/DateTime/SECOND.php | 48 ++++++++++++++++ samples/Calculations/DateTime/TIME.php | 14 +++-- samples/Calculations/DateTime/TIMEVALUE.php | 8 ++- samples/Calculations/DateTime/TODAY.php | 27 +++++++++ samples/Calculations/DateTime/WEEKDAY.php | 58 +++++++++++++++++++ samples/Calculations/DateTime/WEEKNUM.php | 52 +++++++++++++++++ samples/Calculations/DateTime/YEAR.php | 50 ++++++++++++++++ src/PhpSpreadsheet/Helper/Sample.php | 4 +- 21 files changed, 801 insertions(+), 22 deletions(-) create mode 100644 samples/Calculations/DateTime/DATEDIF.php create mode 100644 samples/Calculations/DateTime/DAY.php create mode 100644 samples/Calculations/DateTime/DAYS.php create mode 100644 samples/Calculations/DateTime/DAYS360.php create mode 100644 samples/Calculations/DateTime/EDATE.php create mode 100644 samples/Calculations/DateTime/EOMONTH.php create mode 100644 samples/Calculations/DateTime/HOUR.php create mode 100644 samples/Calculations/DateTime/ISOWEEKNUM.php create mode 100644 samples/Calculations/DateTime/MINUTE.php create mode 100644 samples/Calculations/DateTime/MONTH.php create mode 100644 samples/Calculations/DateTime/NOW.php create mode 100644 samples/Calculations/DateTime/SECOND.php create mode 100644 samples/Calculations/DateTime/TODAY.php create mode 100644 samples/Calculations/DateTime/WEEKDAY.php create mode 100644 samples/Calculations/DateTime/WEEKNUM.php create mode 100644 samples/Calculations/DateTime/YEAR.php diff --git a/samples/Calculations/DateTime/DATE.php b/samples/Calculations/DateTime/DATE.php index 5d758f76..d526cbdb 100644 --- a/samples/Calculations/DateTime/DATE.php +++ b/samples/Calculations/DateTime/DATE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the serial number of a particular date.'); +$category = 'Date/Time'; +$functionName = 'DATE'; +$description = 'Returns the Excel serial number of a particular date'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -27,15 +31,15 @@ for ($row = 1; $row <= $testDateCount; ++$row) { } $worksheet->getStyle('E1:E' . $testDateCount) ->getNumberFormat() - ->setFormatCode('yyyy-mmm-dd'); + ->setFormatCode('yyyy-mm-dd'); // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Year: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); - $helper->log('Month: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Day: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Year: " . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(B{$row}) Month: " . $worksheet->getCell('B' . $row)->getFormattedValue()); + $helper->log("(C{$row}) Day: " . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('D' . $row)->getValue()); - $helper->log('Excel DateStamp: ' . $worksheet->getCell('D' . $row)->getFormattedValue()); + $helper->log('Excel DateStamp: ' . $worksheet->getCell('D' . $row)->getCalculatedValue()); $helper->log('Formatted DateStamp: ' . $worksheet->getCell('E' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/DATEDIF.php b/samples/Calculations/DateTime/DATEDIF.php new file mode 100644 index 00000000..7bc077c9 --- /dev/null +++ b/samples/Calculations/DateTime/DATEDIF.php @@ -0,0 +1,60 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=TODAY()'); + $worksheet->setCellValue('G' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "Y")'); + $worksheet->setCellValue('H' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "M")'); + $worksheet->setCellValue('I' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "D")'); + $worksheet->setCellValue('J' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "MD")'); + $worksheet->setCellValue('K' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "YM")'); + $worksheet->setCellValue('L' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "YD")'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log('In years ("Y"): ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); + $helper->log('In months ("M"): ' . $worksheet->getCell('H' . $row)->getCalculatedValue()); + $helper->log('In days ("D"): ' . $worksheet->getCell('I' . $row)->getCalculatedValue()); + $helper->log('In days ignoring months and years ("MD"): ' . $worksheet->getCell('J' . $row)->getCalculatedValue()); + $helper->log('In months ignoring days and years ("YM"): ' . $worksheet->getCell('K' . $row)->getCalculatedValue()); + $helper->log('In days ignoring years ("YD"): ' . $worksheet->getCell('L' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DATEVALUE.php b/samples/Calculations/DateTime/DATEVALUE.php index 5cdb936d..c506c6f2 100644 --- a/samples/Calculations/DateTime/DATEVALUE.php +++ b/samples/Calculations/DateTime/DATEVALUE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Converts a date in the form of text to a serial number.'); +$category = 'Date/Time'; +$functionName = 'DATEVALUE'; +$description = 'Converts a date in the form of text to an Excel serial number'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -13,8 +17,8 @@ $worksheet = $spreadsheet->getActiveSheet(); // Add some data $testDates = ['26 March 2012', '29 Feb 2012', 'April 1, 2012', '25/12/2012', '2012-Oct-31', '5th November', 'January 1st', 'April 2012', - '17-03', '03-2012', '29 Feb 2011', '03-05-07', - '03-MAY-07', '03-13-07', + '17-03', '03-17', '03-2012', '29 Feb 2011', '03-05-07', + '03-MAY-07', '03-13-07', '13-03-07', '03/13/07', '13/03/07', ]; $testDateCount = count($testDates); @@ -26,14 +30,14 @@ for ($row = 1; $row <= $testDateCount; ++$row) { $worksheet->getStyle('C1:C' . $testDateCount) ->getNumberFormat() - ->setFormatCode('yyyy-mmm-dd'); + ->setFormatCode('yyyy-mm-dd'); // Test the formulae $helper->log('Warning: The PhpSpreadsheet DATEVALUE() function accepts a wider range of date formats than MS Excel DATEFORMAT() function.'); for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Date String: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Date String: " . $worksheet->getCell('A' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('B' . $row)->getValue()); - $helper->log('Excel DateStamp: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Formatted DateStamp' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log('Excel DateStamp: ' . $worksheet->getCell('B' . $row)->getCalculatedValue()); + $helper->log('Formatted DateStamp: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/DAY.php b/samples/Calculations/DateTime/DAY.php new file mode 100644 index 00000000..3a4da7e6 --- /dev/null +++ b/samples/Calculations/DateTime/DAY.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DAY(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Day is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DAYS.php b/samples/Calculations/DateTime/DAYS.php new file mode 100644 index 00000000..ddd47f37 --- /dev/null +++ b/samples/Calculations/DateTime/DAYS.php @@ -0,0 +1,52 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=TODAY()'); + $worksheet->setCellValue('G' . $row, '=DAYS(D' . $row . ', F' . $row . ')'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log('Days: ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DAYS360.php b/samples/Calculations/DateTime/DAYS360.php new file mode 100644 index 00000000..b0e2fdbb --- /dev/null +++ b/samples/Calculations/DateTime/DAYS360.php @@ -0,0 +1,57 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DATE(2022,12,31)'); + $worksheet->setCellValue('G' . $row, '=DAYS360(D' . $row . ', F' . $row . ', FALSE)'); + $worksheet->setCellValue('H' . $row, '=DAYS360(D' . $row . ', F' . $row . ', TRUE)'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log(sprintf( + 'Days: %d (US) %d (European)', + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/EDATE.php b/samples/Calculations/DateTime/EDATE.php new file mode 100644 index 00000000..be6e4d19 --- /dev/null +++ b/samples/Calculations/DateTime/EDATE.php @@ -0,0 +1,43 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$months = range(-12, 12); +$testDateCount = count($months); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=DATE(2020,12,31)'); + $worksheet->setCellValue('B' . $row, '=A' . $row); + $worksheet->setCellValue('C' . $row, $months[$row - 1]); + $worksheet->setCellValue('D' . $row, '=EDATE(B' . $row . ', C' . $row . ')'); +} +$worksheet->getStyle('B1:B' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('D1:D' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + '%s and %d months is %d (%s)', + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getFormattedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/EOMONTH.php b/samples/Calculations/DateTime/EOMONTH.php new file mode 100644 index 00000000..e0b7568a --- /dev/null +++ b/samples/Calculations/DateTime/EOMONTH.php @@ -0,0 +1,43 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$months = range(-12, 12); +$testDateCount = count($months); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=DATE(2020,1,1)'); + $worksheet->setCellValue('B' . $row, '=A' . $row); + $worksheet->setCellValue('C' . $row, $months[$row - 1]); + $worksheet->setCellValue('D' . $row, '=EOMONTH(B' . $row . ', C' . $row . ')'); +} +$worksheet->getStyle('B1:B' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('D1:D' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + '%s and %d months is %d (%s)', + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getFormattedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/HOUR.php b/samples/Calculations/DateTime/HOUR.php new file mode 100644 index 00000000..53c21644 --- /dev/null +++ b/samples/Calculations/DateTime/HOUR.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=HOUR(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Hour is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/ISOWEEKNUM.php b/samples/Calculations/DateTime/ISOWEEKNUM.php new file mode 100644 index 00000000..4a989fbd --- /dev/null +++ b/samples/Calculations/DateTime/ISOWEEKNUM.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=ISOWEEKNUM(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('ISO Week number is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/MINUTE.php b/samples/Calculations/DateTime/MINUTE.php new file mode 100644 index 00000000..11e90e13 --- /dev/null +++ b/samples/Calculations/DateTime/MINUTE.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=MINUTE(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Minute is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/MONTH.php b/samples/Calculations/DateTime/MONTH.php new file mode 100644 index 00000000..8cceaf4c --- /dev/null +++ b/samples/Calculations/DateTime/MONTH.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=MONTH(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Month is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/NOW.php b/samples/Calculations/DateTime/NOW.php new file mode 100644 index 00000000..858a3162 --- /dev/null +++ b/samples/Calculations/DateTime/NOW.php @@ -0,0 +1,27 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->setCellValue('A1', '=NOW()'); +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd hh:mm:ss'); + +// Test the formulae +$helper->log(sprintf( + 'Today is %f (%s)', + $worksheet->getCell('A1')->getCalculatedValue(), + $worksheet->getCell('A1')->getFormattedValue() +)); diff --git a/samples/Calculations/DateTime/SECOND.php b/samples/Calculations/DateTime/SECOND.php new file mode 100644 index 00000000..33806fd5 --- /dev/null +++ b/samples/Calculations/DateTime/SECOND.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=SECOND(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Second is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/TIME.php b/samples/Calculations/DateTime/TIME.php index 3d4208ad..42c45488 100644 --- a/samples/Calculations/DateTime/TIME.php +++ b/samples/Calculations/DateTime/TIME.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the serial number of a particular time.'); +$category = 'Date/Time'; +$functionName = 'TIME'; +$description = 'Returns the Excel serial number of a particular time'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -29,11 +33,11 @@ $worksheet->getStyle('E1:E' . $testDateCount) // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Hour: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); - $helper->log('Minute: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Second: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Hour: " . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(B{$row}) Minute: " . $worksheet->getCell('B' . $row)->getFormattedValue()); + $helper->log("(C{$row}) Second: " . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('D' . $row)->getValue()); - $helper->log('Excel TimeStamp: ' . $worksheet->getCell('D' . $row)->getFormattedValue()); + $helper->log('Excel TimeStamp: ' . $worksheet->getCell('D' . $row)->getCalculatedValue()); $helper->log('Formatted TimeStamp: ' . $worksheet->getCell('E' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/TIMEVALUE.php b/samples/Calculations/DateTime/TIMEVALUE.php index f75393cd..15ea8cd9 100644 --- a/samples/Calculations/DateTime/TIMEVALUE.php +++ b/samples/Calculations/DateTime/TIMEVALUE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Converts a time in the form of text to a serial number.'); +$category = 'Date/Time'; +$functionName = 'DATEVALUE'; +$description = 'Converts a time in the form of text to an Excel serial number'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -27,7 +31,7 @@ $worksheet->getStyle('C1:C' . $testDateCount) // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Time String: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Time String: " . $worksheet->getCell('A' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('B' . $row)->getValue()); $helper->log('Excel TimeStamp: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); $helper->log('Formatted TimeStamp: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); diff --git a/samples/Calculations/DateTime/TODAY.php b/samples/Calculations/DateTime/TODAY.php new file mode 100644 index 00000000..031149d5 --- /dev/null +++ b/samples/Calculations/DateTime/TODAY.php @@ -0,0 +1,27 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->setCellValue('A1', '=TODAY()'); +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log(sprintf( + 'Today is %d (%s)', + $worksheet->getCell('A1')->getCalculatedValue(), + $worksheet->getCell('A1')->getFormattedValue() +)); diff --git a/samples/Calculations/DateTime/WEEKDAY.php b/samples/Calculations/DateTime/WEEKDAY.php new file mode 100644 index 00000000..7d4b4288 --- /dev/null +++ b/samples/Calculations/DateTime/WEEKDAY.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=WEEKDAY(D' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=WEEKDAY(D' . $row . ', 2)'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log(sprintf( + 'Weekday is: %d (1-7 = Sun-Sat)', + $worksheet->getCell('F' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Weekday is: %d (1-7 = Mon-Sun)', + $worksheet->getCell('G' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/WEEKNUM.php b/samples/Calculations/DateTime/WEEKNUM.php new file mode 100644 index 00000000..1d4c4a12 --- /dev/null +++ b/samples/Calculations/DateTime/WEEKNUM.php @@ -0,0 +1,52 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=WEEKNUM(D' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=WEEKNUM(D' . $row . ', 21)'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('System 1 Week number is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); + $helper->log('System 2 (ISO-8601) Week number is: ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/YEAR.php b/samples/Calculations/DateTime/YEAR.php new file mode 100644 index 00000000..f7bfa6ea --- /dev/null +++ b/samples/Calculations/DateTime/YEAR.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=YEAR(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Year is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index aeb2bea6..9f7563d6 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -187,8 +187,8 @@ class Sample { $this->log(sprintf('%s Functions:', $category)); $description === null - ? $this->log(sprintf('%s()', rtrim($functionName, '()'))) - : $this->log(sprintf('%s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + ? $this->log(sprintf('Function: %s()', rtrim($functionName, '()'))) + : $this->log(sprintf('Function: %s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); } public function displayGrid(array $matrix): void From 7e3807309de6794fb277ceb8ef9af8bb6969e775 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 9 Sep 2022 07:34:36 -0700 Subject: [PATCH 50/69] Reconcile Differences between Css and Excel For Cell Alignment (#3048) This PR expands on PR #2195 from @nkjackzhang. That PR has been stalled for some time awaiting requested fixes. Those fixes are part of this PR, and additional tests and samples are added. The original request was to handle `vertical-align:middle` in Css (Excel uses `center`). This PR does its best to also handle vertical alignment Excel values not found in Css - `justify` (as `middle`) and `distributed` (as `middle`). It likewises handles valid Css values not found in Excel (`baseline`, `sub`, and `text-bottom` as `bottom`; `super` and `text-top` as `top`; `middle` as `center`). It also handles horizontal alignment Excel values not found in Css - `center-continuous` as `center` and `distributed` as `justify`; I couldn't think of a reasonable equivalent for `fill`, so it is ignored. The values assigned for vertical and horizontal alignment are now lower-cased (special handling required for `centerContinuous`). --- samples/Basic/49_alignment.php | 80 +++++++++++++++++ src/PhpSpreadsheet/Style/Alignment.php | 69 +++++++++++++-- src/PhpSpreadsheet/Writer/Html.php | 24 ++--- src/PhpSpreadsheet/Writer/Xlsx/Style.php | 21 +++-- .../Style/AlignmentMiddleTest.php | 87 +++++++++++++++++++ .../Style/AlignmentTest.php | 31 ++++--- 6 files changed, 273 insertions(+), 39 deletions(-) create mode 100644 samples/Basic/49_alignment.php create mode 100644 tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php diff --git a/samples/Basic/49_alignment.php b/samples/Basic/49_alignment.php new file mode 100644 index 00000000..83fdb3d4 --- /dev/null +++ b/samples/Basic/49_alignment.php @@ -0,0 +1,80 @@ +log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); +$spreadsheet->getProperties()->setTitle('Alignment'); +$sheet = $spreadsheet->getActiveSheet(); +$hi = 'Hi There'; +$ju = 'This is a longer than normal sentence'; +$sheet->fromArray([ + ['', 'default', 'bottom', 'top', 'center', 'justify', 'distributed'], + ['default', $hi, $hi, $hi, $hi, $hi, $hi], + ['left', $hi, $hi, $hi, $hi, $hi, $hi], + ['right', $hi, $hi, $hi, $hi, $hi, $hi], + ['center', $hi, $hi, $hi, $hi, $hi, $hi], + ['justify', $ju, $ju, $ju, $ju, $ju, $ju], + ['distributed', $ju, $ju, $ju, $ju, $ju, $ju], +]); +$sheet->getColumnDimension('B')->setWidth(20); +$sheet->getColumnDimension('C')->setWidth(20); +$sheet->getColumnDimension('D')->setWidth(20); +$sheet->getColumnDimension('E')->setWidth(20); +$sheet->getColumnDimension('F')->setWidth(20); +$sheet->getColumnDimension('G')->setWidth(20); +$sheet->getRowDimension(2)->setRowHeight(30); +$sheet->getRowDimension(3)->setRowHeight(30); +$sheet->getRowDimension(4)->setRowHeight(30); +$sheet->getRowDimension(5)->setRowHeight(30); +$sheet->getRowDimension(6)->setRowHeight(40); +$sheet->getRowDimension(7)->setRowHeight(40); +$minRow = 2; +$maxRow = 7; +$minCol = 'B'; +$maxCol = 'g'; +$sheet->getStyle("C$minRow:C$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_BOTTOM); +$sheet->getStyle("D$minRow:D$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_TOP); +$sheet->getStyle("E$minRow:E$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); +$sheet->getStyle("F$minRow:F$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_JUSTIFY); +$sheet->getStyle("G$minRow:G$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_DISTRIBUTED); +$sheet->getStyle("{$minCol}3:{$maxCol}3") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_LEFT); +$sheet->getStyle("{$minCol}4:{$maxCol}4") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_RIGHT); +$sheet->getStyle("{$minCol}5:{$maxCol}5") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER); +$sheet->getStyle("{$minCol}6:{$maxCol}6") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_JUSTIFY); +$sheet->getStyle("{$minCol}7:{$maxCol}7") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_DISTRIBUTED); + +$sheet->getCell('A9')->setValue('Center Continuous A9-C9'); +$sheet->getStyle('A9:C9') + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER_CONTINUOUS); +$sheet->getCell('A10')->setValue('Fill'); +$sheet->getStyle('A10') + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_FILL); +$sheet->setSelectedCells('A1'); + +$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Html', 'Xls']); diff --git a/src/PhpSpreadsheet/Style/Alignment.php b/src/PhpSpreadsheet/Style/Alignment.php index 83ac5b0d..68edfaca 100644 --- a/src/PhpSpreadsheet/Style/Alignment.php +++ b/src/PhpSpreadsheet/Style/Alignment.php @@ -15,6 +15,27 @@ class Alignment extends Supervisor const HORIZONTAL_JUSTIFY = 'justify'; const HORIZONTAL_FILL = 'fill'; const HORIZONTAL_DISTRIBUTED = 'distributed'; // Excel2007 only + private const HORIZONTAL_CENTER_CONTINUOUS_LC = 'centercontinuous'; + // Mapping for horizontal alignment + const HORIZONTAL_ALIGNMENT_FOR_XLSX = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER_CONTINUOUS, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_DISTRIBUTED, + ]; + // Mapping for horizontal alignment CSS + const HORIZONTAL_ALIGNMENT_FOR_HTML = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + //self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, // no reasonable equivalent for fill + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_JUSTIFY, + ]; // Vertical alignment styles const VERTICAL_BOTTOM = 'bottom'; @@ -22,6 +43,45 @@ class Alignment extends Supervisor const VERTICAL_CENTER = 'center'; const VERTICAL_JUSTIFY = 'justify'; const VERTICAL_DISTRIBUTED = 'distributed'; // Excel2007 only + // Vertical alignment CSS + private const VERTICAL_BASELINE = 'baseline'; + private const VERTICAL_MIDDLE = 'middle'; + private const VERTICAL_SUB = 'sub'; + private const VERTICAL_SUPER = 'super'; + private const VERTICAL_TEXT_BOTTOM = 'text-bottom'; + private const VERTICAL_TEXT_TOP = 'text-top'; + + // Mapping for vertical alignment + const VERTICAL_ALIGNMENT_FOR_XLSX = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_CENTER, + self::VERTICAL_JUSTIFY => self::VERTICAL_JUSTIFY, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_DISTRIBUTED, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BOTTOM, + self::VERTICAL_MIDDLE => self::VERTICAL_CENTER, + self::VERTICAL_SUB => self::VERTICAL_BOTTOM, + self::VERTICAL_SUPER => self::VERTICAL_TOP, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TOP, + ]; + + // Mapping for vertical alignment for Html + const VERTICAL_ALIGNMENT_FOR_HTML = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_MIDDLE, + self::VERTICAL_JUSTIFY => self::VERTICAL_MIDDLE, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_MIDDLE, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BASELINE, + self::VERTICAL_MIDDLE => self::VERTICAL_MIDDLE, + self::VERTICAL_SUB => self::VERTICAL_SUB, + self::VERTICAL_SUPER => self::VERTICAL_SUPER, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_TEXT_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TEXT_TOP, + ]; // Read order const READORDER_CONTEXT = 0; @@ -202,8 +262,9 @@ class Alignment extends Supervisor */ public function setHorizontal(string $horizontalAlignment) { - if ($horizontalAlignment == '') { - $horizontalAlignment = self::HORIZONTAL_GENERAL; + $horizontalAlignment = strtolower($horizontalAlignment); + if ($horizontalAlignment === self::HORIZONTAL_CENTER_CONTINUOUS_LC) { + $horizontalAlignment = self::HORIZONTAL_CENTER_CONTINUOUS; } if ($this->isSupervisor) { @@ -239,9 +300,7 @@ class Alignment extends Supervisor */ public function setVertical($verticalAlignment) { - if ($verticalAlignment == '') { - $verticalAlignment = self::VERTICAL_BOTTOM; - } + $verticalAlignment = strtolower($verticalAlignment); if ($this->isSupervisor) { $styleArray = $this->getStyleArray(['vertical' => $verticalAlignment]); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 6a400673..c115cabb 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -230,13 +230,6 @@ class Html extends BaseWriter $this->editHtmlCallback = $callback; } - const VALIGN_ARR = [ - Alignment::VERTICAL_BOTTOM => 'bottom', - Alignment::VERTICAL_TOP => 'top', - Alignment::VERTICAL_CENTER => 'middle', - Alignment::VERTICAL_JUSTIFY => 'middle', - ]; - /** * Map VAlign. * @@ -246,17 +239,9 @@ class Html extends BaseWriter */ private function mapVAlign($vAlign) { - return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline'; + return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? ''; } - const HALIGN_ARR = [ - Alignment::HORIZONTAL_LEFT => 'left', - Alignment::HORIZONTAL_RIGHT => 'right', - Alignment::HORIZONTAL_CENTER => 'center', - Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center', - Alignment::HORIZONTAL_JUSTIFY => 'justify', - ]; - /** * Map HAlign. * @@ -266,7 +251,7 @@ class Html extends BaseWriter */ private function mapHAlign($hAlign) { - return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : ''; + return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? ''; } const BORDER_ARR = [ @@ -988,7 +973,10 @@ class Html extends BaseWriter $css = []; // Create CSS - $css['vertical-align'] = $this->mapVAlign($alignment->getVertical() ?? ''); + $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? ''); + if ($verticalAlign) { + $css['vertical-align'] = $verticalAlign; + } $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? ''); if ($textAlign) { $css['text-align'] = $textAlign; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php index b24b7533..0442c25d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Conditional; @@ -403,8 +404,14 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - $objWriter->writeAttribute('horizontal', (string) $style->getAlignment()->getHorizontal()); - $objWriter->writeAttribute('vertical', (string) $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal !== '') { + $objWriter->writeAttribute('horizontal', $horizontal); + } + if ($vertical !== '') { + $objWriter->writeAttribute('vertical', $vertical); + } $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { @@ -459,11 +466,13 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - if ($style->getAlignment()->getHorizontal() !== null) { - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal) { + $objWriter->writeAttribute('horizontal', $horizontal); } - if ($style->getAlignment()->getVertical() !== null) { - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + if ($vertical) { + $objWriter->writeAttribute('vertical', $vertical); } if ($style->getAlignment()->getTextRotation() !== null) { diff --git a/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php b/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php new file mode 100644 index 00000000..1d260a6e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php @@ -0,0 +1,87 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + public function testCenterWriteHtml(): void + { + // Html Writer changes vertical align center to middle + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + $writer = new HTML($this->spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('vertical-align:middle', $html); + self::assertStringNotContainsString('vertical-align:center', $html); + } + + public function testCenterWriteXlsx(): void + { + // Xlsx Writer uses vertical align center unchanged + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + $this->outputFileName = File::temporaryFilename(); + $writer = new Xlsx($this->spreadsheet); + $writer->save($this->outputFileName); + $zip = new ZipArchive(); + $zip->open($this->outputFileName); + $html = $zip->getFromName('xl/styles.xml'); + $zip->close(); + self::assertStringContainsString('vertical="center"', $html); + self::assertStringNotContainsString('vertical="middle"', $html); + } + + public function testCenterWriteXlsx2(): void + { + // Xlsx Writer changes vertical align middle to center + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical('middle'); + $this->outputFileName = File::temporaryFilename(); + $writer = new Xlsx($this->spreadsheet); + $writer->save($this->outputFileName); + $zip = new ZipArchive(); + $zip->open($this->outputFileName); + $html = $zip->getFromName('xl/styles.xml'); + $zip->close(); + self::assertStringContainsString('vertical="center"', $html); + self::assertStringNotContainsString('vertical="middle"', $html); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/AlignmentTest.php b/tests/PhpSpreadsheetTests/Style/AlignmentTest.php index d10e3211..6f50ce22 100644 --- a/tests/PhpSpreadsheetTests/Style/AlignmentTest.php +++ b/tests/PhpSpreadsheetTests/Style/AlignmentTest.php @@ -9,10 +9,21 @@ use PHPUnit\Framework\TestCase; class AlignmentTest extends TestCase { + /** @var ?Spreadsheet */ + private $spreadsheet; + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + public function testAlignment(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(45); @@ -31,8 +42,8 @@ class AlignmentTest extends TestCase public function testRotationTooHigh(): void { $this->expectException(PhpSpreadsheetException::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(91); @@ -42,8 +53,8 @@ class AlignmentTest extends TestCase public function testRotationTooLow(): void { $this->expectException(PhpSpreadsheetException::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(-91); @@ -52,8 +63,8 @@ class AlignmentTest extends TestCase public function testHorizontal(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('X'); $cell1->getStyle()->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT)->setIndent(1); @@ -74,8 +85,8 @@ class AlignmentTest extends TestCase public function testReadOrder(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('ABC'); $cell1->getStyle()->getAlignment()->setReadOrder(0); From b5f70de61d3ebe5ee09220e9c7f47d8a60f976a4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 9 Sep 2022 07:56:11 -0700 Subject: [PATCH 51/69] More Scrutinizer Catch Up (#3050) * More Scrutinizer Catch Up Continue the work of PR #3043 by attending to the 12 remaining 'new' issues. * Php 8.1 Problem One new null-instead-of-string problem. --- phpstan-baseline.neon | 15 ------- .../Chart/33_Chart_create_line_dateaxis.php | 22 +++++----- src/PhpSpreadsheet/Chart/Axis.php | 2 +- src/PhpSpreadsheet/Chart/Chart.php | 6 ++- src/PhpSpreadsheet/Reader/Xlsx.php | 13 +++--- src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 12 +++--- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 40 +++++++++---------- src/PhpSpreadsheet/Worksheet/PageSetup.php | 9 +++-- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- src/PhpSpreadsheet/Writer/Html.php | 29 +++++++------- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 4 +- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 8 ++-- .../Reader/Xlsx/AutoFilter2Test.php | 1 + tests/PhpSpreadsheetTests/RichTextTest.php | 3 -- 14 files changed, 78 insertions(+), 88 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cc2eaa3d..3e2ccfc1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2590,21 +2590,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - message: "#^Parameter \\#1 \\$value of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\PageSetup\\:\\:setFirstPageNumber\\(\\) expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\PageSetup\\:\\:\\$pageOrder has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Strict comparison using \\=\\=\\= between int\\ and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\SheetView\\:\\:\\$sheetViewTypes has no type specified\\.$#" count: 1 diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php index 1a47e5aa..413866e2 100644 --- a/samples/Chart/33_Chart_create_line_dateaxis.php +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -101,10 +101,10 @@ $dataSeriesValues = [ // marker details $dataSeriesValues[0] ->getMarkerFillColor() - ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0] ->getMarkerBorderColor() - ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); // line details - dashed, smooth line (Bezier) with arrows, 40% transparent $dataSeriesValues[0] @@ -129,18 +129,18 @@ $dataSeriesValues[1] // square marker border color ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $dataSeriesValues[1] // square marker fill color ->getMarkerFillColor() - ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1] ->setScatterLines(true) ->setSmoothLine(false) - ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[1]->setLineWidth(2.0); // series 3 - metric3, markers, no line $dataSeriesValues[2] // triangle? fill //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle ->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[2] // triangle border ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); @@ -239,7 +239,7 @@ $dataSeriesValues[0] ->setScatterlines(false); // disable connecting lines $dataSeriesValues[0] ->getMarkerFillColor() - ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_ARGB); + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); $dataSeriesValues[0] ->getMarkerBorderColor() ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); @@ -326,7 +326,7 @@ $chart = new Chart( // Set the position of the chart in the chart sheet below the first chart $chart->setTopLeftPosition('A13'); $chart->setBottomRightPosition('P25'); -$chart->setRoundedCorners('true'); // Rounded corners in Chart Outline +$chart->setRoundedCorners(true); // Rounded corners in Chart Outline // Add the chart to the worksheet $chartSheet $chartSheet->addChart($chart); @@ -350,8 +350,8 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj // get date of first day of the quarter of the start date - $startMonth = $startDate->format('n'); // suppress leading zero - $startYr = $startDate->format('Y'); + $startMonth = (int) $startDate->format('n'); // suppress leading zero + $startYr = (int) $startDate->format('Y'); $qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0); $qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3)); $qtrStartStr = "$startYr-$qtrStartMonth-01"; @@ -360,8 +360,8 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array // end the xaxis at the end of the quarter of the last date $lastDateStr = $dataSheet->getCellByColumnAndRow(2, $nrows + 1)->getValue(); $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); - $lastMonth = $lastDate->format('n'); - $lastYr = $lastDate->format('Y'); + $lastMonth = (int) $lastDate->format('n'); + $lastYr = (int) $lastDate->format('Y'); $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); $qtrEndMonth = 3 + (($qtr - 1) * 3); $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 3ac5c3cd..9ebb081f 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -219,7 +219,7 @@ class Axis extends Properties * @param ?int $alpha * @param ?string $AlphaType */ - public function setFillParameters($color, $alpha = null, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function setFillParameters($color, $alpha = null, $AlphaType = ChartColor::EXCEL_COLOR_TYPE_RGB): void { $this->fillColor->setColorProperties($color, $alpha, $AlphaType); } diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 036338c6..f41d7883 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -774,9 +774,11 @@ class Chart return $this->roundedCorners; } - public function setRoundedCorners(bool $roundedCorners): self + public function setRoundedCorners(?bool $roundedCorners): self { - $this->roundedCorners = $roundedCorners; + if ($roundedCorners !== null) { + $this->roundedCorners = $roundedCorners; + } return $this; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 244baddd..fc38375a 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -306,22 +306,25 @@ class Xlsx extends BaseReader return (bool) $c->v; } - private static function castToError(SimpleXMLElement $c): ?string + private static function castToError(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } - private static function castToString(SimpleXMLElement $c): ?string + private static function castToString(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } /** * @param mixed $value * @param mixed $calculatedValue */ - private function castToFormula(SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void + private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void { + if ($c === null) { + return; + } $attr = $c->f->attributes(); $cellDataType = 'f'; $value = "={$c->f}"; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 623c6691..39328adb 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -85,9 +85,9 @@ class AutoFilter } } - private function readCustomAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->customFilters) { + if (isset($filterColumn, $filterColumn->customFilters)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); $customFilters = $filterColumn->customFilters; // Custom filters can an AND or an OR join; @@ -104,9 +104,9 @@ class AutoFilter } } - private function readDynamicAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->dynamicFilter) { + if (isset($filterColumn, $filterColumn->dynamicFilter)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); // We should only ever have one dynamic filter foreach ($filterColumn->dynamicFilter as $filterRule) { @@ -126,9 +126,9 @@ class AutoFilter } } - private function readTopTenAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->top10) { + if (isset($filterColumn, $filterColumn->top10)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); // We should only ever have one top10 filter foreach ($filterColumn->top10 as $filterRule) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 507c8f5b..6bc6d7c5 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -11,7 +11,7 @@ use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; +use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -113,6 +113,7 @@ class Chart $plotSeries = $plotAttributes = []; $catAxRead = false; $plotNoFill = false; + /** @var SimpleXMLElement $chartDetail */ foreach ($chartDetails as $chartDetailKey => $chartDetail) { switch ($chartDetailKey) { case 'spPr': @@ -121,17 +122,18 @@ class Chart $plotNoFill = true; } if (isset($possibleNoFill->gradFill->gsLst)) { + /** @var SimpleXMLElement $gradient */ foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { /** @var float */ $pos = self::getAttribute($gradient, 'pos', 'float'); $gradientArray[] = [ - $pos / Properties::PERCENTAGE_MULTIPLIER, + $pos / ChartProperties::PERCENTAGE_MULTIPLIER, new ChartColor($this->readColor($gradient)), ]; } } if (isset($possibleNoFill->gradFill->lin)) { - $gradientLin = Properties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + $gradientLin = ChartProperties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); } break; @@ -464,12 +466,13 @@ class Chart $pointSize = null; $noFill = false; $bubble3D = false; - $dPtColors = []; + $dptColors = []; $markerFillColor = null; $markerBorderColor = null; $lineStyle = null; $labelLayout = null; $trendLines = []; + /** @var SimpleXMLElement $seriesDetail */ foreach ($seriesDetails as $seriesKey => $seriesDetail) { switch ($seriesKey) { case 'idx': @@ -487,7 +490,6 @@ class Chart break; case 'spPr': $children = $seriesDetail->children($this->aNamespace); - $ln = $children->ln; if (isset($children->ln)) { $ln = $children->ln; if (is_countable($ln->noFill) && count($ln->noFill) === 1) { @@ -1161,7 +1163,7 @@ class Chart } } - private function readEffects(SimpleXMLElement $chartDetail, ?Properties $chartObject): void + private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; @@ -1169,7 +1171,7 @@ class Chart $sppr = $chartDetail->spPr->children($this->aNamespace); if (isset($sppr->effectLst->glow)) { - $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / Properties::POINTS_WIDTH_MULTIPLIER; + $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / ChartProperties::POINTS_WIDTH_MULTIPLIER; if ($axisGlowSize != 0.0) { $colorArray = $this->readColor($sppr->effectLst->glow); $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']); @@ -1180,7 +1182,7 @@ class Chart /** @var string */ $softEdgeSize = self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string'); if (is_numeric($softEdgeSize)) { - $chartObject->setSoftEdges((float) Properties::xmlToPoints($softEdgeSize)); + $chartObject->setSoftEdges((float) ChartProperties::xmlToPoints($softEdgeSize)); } } @@ -1195,20 +1197,20 @@ class Chart if ($type !== '') { /** @var string */ $blur = self::getAttribute($sppr->effectLst->$type, 'blurRad', 'string'); - $blur = is_numeric($blur) ? Properties::xmlToPoints($blur) : null; + $blur = is_numeric($blur) ? ChartProperties::xmlToPoints($blur) : null; /** @var string */ $dist = self::getAttribute($sppr->effectLst->$type, 'dist', 'string'); - $dist = is_numeric($dist) ? Properties::xmlToPoints($dist) : null; + $dist = is_numeric($dist) ? ChartProperties::xmlToPoints($dist) : null; /** @var string */ $direction = self::getAttribute($sppr->effectLst->$type, 'dir', 'string'); - $direction = is_numeric($direction) ? Properties::xmlToAngle($direction) : null; + $direction = is_numeric($direction) ? ChartProperties::xmlToAngle($direction) : null; $algn = self::getAttribute($sppr->effectLst->$type, 'algn', 'string'); $rot = self::getAttribute($sppr->effectLst->$type, 'rotWithShape', 'string'); $size = []; foreach (['sx', 'sy'] as $sizeType) { $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); if (is_numeric($sizeValue)) { - $size[$sizeType] = Properties::xmlToTenthOfPercent((string) $sizeValue); + $size[$sizeType] = ChartProperties::xmlToTenthOfPercent((string) $sizeValue); } else { $size[$sizeType] = null; } @@ -1216,7 +1218,7 @@ class Chart foreach (['kx', 'ky'] as $sizeType) { $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); if (is_numeric($sizeValue)) { - $size[$sizeType] = Properties::xmlToAngle((string) $sizeValue); + $size[$sizeType] = ChartProperties::xmlToAngle((string) $sizeValue); } else { $size[$sizeType] = null; } @@ -1273,7 +1275,7 @@ class Chart return $result; } - private function readLineStyle(SimpleXMLElement $chartDetail, ?Properties $chartObject): void + private function readLineStyle(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; @@ -1287,7 +1289,7 @@ class Chart /** @var string */ $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string'); if (is_numeric($lineWidthTemp)) { - $lineWidth = Properties::xmlToPoints($lineWidthTemp); + $lineWidth = ChartProperties::xmlToPoints($lineWidthTemp); } /** @var string */ $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string'); @@ -1296,15 +1298,13 @@ class Chart /** @var string */ $capType = self::getAttribute($sppr->ln, 'cap', 'string'); if (isset($sppr->ln->miter)) { - $joinType = Properties::LINE_STYLE_JOIN_MITER; + $joinType = ChartProperties::LINE_STYLE_JOIN_MITER; } elseif (isset($sppr->ln->bevel)) { - $joinType = Properties::LINE_STYLE_JOIN_BEVEL; + $joinType = ChartProperties::LINE_STYLE_JOIN_BEVEL; } else { $joinType = ''; } - $headArrowType = ''; $headArrowSize = ''; - $endArrowType = ''; $endArrowSize = ''; /** @var string */ $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string'); @@ -1403,7 +1403,7 @@ class Chart /** @var string */ $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); if (is_numeric($textRotation)) { - $whichAxis->setAxisOption('textRotation', (string) Properties::xmlToAngle($textRotation)); + $whichAxis->setAxisOption('textRotation', (string) ChartProperties::xmlToAngle($textRotation)); } } } diff --git a/src/PhpSpreadsheet/Worksheet/PageSetup.php b/src/PhpSpreadsheet/Worksheet/PageSetup.php index c23bfc59..4bdc2d4c 100644 --- a/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -259,10 +259,11 @@ class PageSetup /** * First page number. * - * @var int + * @var ?int */ private $firstPageNumber; + /** @var string */ private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER; /** @@ -375,7 +376,7 @@ class PageSetup { // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, // but it is apparently still able to handle any scale >= 0, where 0 results in 100 - if (($scale >= 0) || $scale === null) { + if ($scale === null || $scale >= 0) { $this->scale = $scale; if ($update) { $this->fitToPage = false; @@ -845,7 +846,7 @@ class PageSetup /** * Get first page number. * - * @return int + * @return ?int */ public function getFirstPageNumber() { @@ -855,7 +856,7 @@ class PageSetup /** * Set first page number. * - * @param int $value + * @param ?int $value * * @return $this */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 413ec9ef..d13d4141 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1359,7 +1359,7 @@ class Worksheet implements IComparable if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) { // then there is a row dimension with explicit style, assign it to the cell - $cell->setXfIndex($rowDimension->getXfIndex()); + $cell->setXfIndex(/** @scrutinizer ignore-type */ $rowDimension->getXfIndex()); } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) { // then there is a column dimension, assign it to the cell $cell->setXfIndex($columnDimension->getXfIndex()); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index c115cabb..fca5ee89 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -24,6 +24,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Html extends BaseWriter @@ -1277,23 +1278,22 @@ class Html extends BaseWriter } } - private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, ?string &$cellData): void + private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void { if ($cell->getValue() instanceof RichText) { $this->generateRowCellDataValueRich($cell, $cellData); } else { $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); - if ($formatCode !== null) { - $cellData = NumberFormat::toFormattedString( - $origData, - $formatCode, - [$this, 'formatColor'] - ); - } + + $cellData = NumberFormat::toFormattedString( + $origData ?? '', + $formatCode ?? NumberFormat::FORMAT_GENERAL, + [$this, 'formatColor'] + ); if ($cellData === $origData) { - $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags()); + $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags()); } if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { $cellData = '' . $cellData . ''; @@ -1477,8 +1477,8 @@ class Html extends BaseWriter && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]); // Colspan and Rowspan - $colspan = 1; - $rowspan = 1; + $colSpan = 1; + $rowSpan = 1; if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum])) { $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]; $rowSpan = $spans['rowspan']; @@ -1791,7 +1791,8 @@ class Html extends BaseWriter public function getOrientation(): ?string { - return null; + // Expect Pdf classes to override this method. + return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null; } /** @@ -1830,9 +1831,9 @@ class Html extends BaseWriter $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; '; $htmlPage .= 'margin-bottom: ' . $bottom; $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation(); - if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) { + if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) { $htmlPage .= 'size: landscape; '; - } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { + } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) { $htmlPage .= 'size: portrait; '; } $htmlPage .= '}' . PHP_EOL; diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 8f09af69..847d772a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -465,7 +465,7 @@ class Worksheet extends BIFFwriter switch ($calctype) { case 'integer': case 'double': - $this->writeNumber($row, $column, $calculatedValue, $xfIndex); + $this->writeNumber($row, $column, (float) $calculatedValue, $xfIndex); break; case 'string': @@ -473,7 +473,7 @@ class Worksheet extends BIFFwriter break; case 'boolean': - $this->writeBoolErr($row, $column, $calculatedValue, 0, $xfIndex); + $this->writeBoolErr($row, $column, (int) $calculatedValue, 0, $xfIndex); break; default: diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 5680281f..1aa4f1ca 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -503,7 +503,7 @@ class Worksheet extends WriterPart private static function writeTimePeriodCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('timePeriod', $txt); if (empty($conditional->getConditions())) { if ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TODAY) { @@ -536,7 +536,7 @@ class Worksheet extends WriterPart private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('text', $txt); if (empty($conditional->getConditions())) { if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) { @@ -1034,7 +1034,7 @@ class Worksheet extends WriterPart } else { $objWriter->writeAttribute('fitToWidth', '0'); } - if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) { + if (!empty($worksheet->getPageSetup()->getFirstPageNumber())) { $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } @@ -1228,7 +1228,7 @@ class Worksheet extends WriterPart StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags())) ); $objWriter->endElement(); - } elseif ($cellValue instanceof RichText) { + } else { $objWriter->startElement('is'); $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue); $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 0bb9f130..57c09de5 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -70,6 +70,7 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index 49878529..e6f55514 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -28,9 +28,6 @@ class RichTextTest extends TestCase public function testTextElements(): void { $element1 = new TextElement('A'); - if ($element1->getFont() !== null) { - self::fail('Expected font to be null'); - } $element2 = new TextElement('B'); $element3 = new TextElement('C'); $richText = new RichText(); From 2fe66d097fc4f5a310d5e6e387f37011793007bb Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 12 Sep 2022 08:23:43 -0700 Subject: [PATCH 52/69] Upgrade mitoteam/jpgraph for Php8.2 Usage (#3058) They just released a Php8.2-compatible version. We should use that version going forward. Some tests had been disabled in 8.2 due to the problems which the new release fixes; these are now restored. --- composer.json | 2 +- composer.lock | 23 +++++++++++-------- .../PhpSpreadsheetTests/Helper/SampleTest.php | 6 ----- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 46ee56b9..6835b05b 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.1", + "mitoteam/jpgraph": "10.2.2", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 37e2dfa8..4097420d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "dd19bb54ddc39f5b24f564818cb46c7e", + "content-hash": "8512207f173cb137bc2085b7fdf698bc", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1342,20 +1342,23 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.1.3", + "version": "10.2.2", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "425a2a0f0c97a28fe0aca60a4384ce85880e438a" + "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/425a2a0f0c97a28fe0aca60a4384ce85880e438a", - "reference": "425a2a0f0c97a28fe0aca60a4384ce85880e438a", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", + "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=5.5 <=8.2" + }, + "replace": { + "jpgraph/jpgraph": "4.0.2" }, "type": "library", "autoload": { @@ -1372,16 +1375,16 @@ "name": "JpGraph team" } ], - "description": "Composer compatible version of JpGraph library with PHP 8.1 support", + "description": "Composer compatible version of JpGraph library with PHP 8.2 support", "homepage": "https://github.com/mitoteam/jpgraph", "keywords": [ "jpgraph" ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.1.3" + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.2" }, - "time": "2022-07-05T16:46:34+00:00" + "time": "2022-09-09T08:16:10+00:00" }, { "name": "mpdf/mpdf", @@ -5265,5 +5268,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index a104e8ff..2195155f 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -27,12 +27,6 @@ class SampleTest extends TestCase { $skipped = [ ]; - if (PHP_VERSION_ID >= 80200) { - // Hopefully temporary. Continue to try - // 32_chart_read_write_PDF/HTML - // so as not to lose track of the problem. - $skipped[] = 'Chart/35_Chart_render.php'; - } // Unfortunately some tests are too long to run with code-coverage // analysis on GitHub Actions, so we need to exclude them From 3e8d50547c0b7a8acf9352834b3e9155a86210b3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 12 Sep 2022 08:45:13 -0700 Subject: [PATCH 53/69] Minor Fix for Percentage Formatting (#3053) Fix #1929. This was already substantially fixed, but there was a lingering problem with an unexpected leading space. It turns out there was also a problem with leading zeros, also fixed. There are also problems involving commas; fixing those seems too complicated to delay these changes, but I will add it to my to-do list. --- .../Style/NumberFormat/PercentageFormatter.php | 12 +++++++----- tests/PhpSpreadsheetTests/Style/NumberFormatTest.php | 2 +- tests/data/Style/NumberFormat.php | 9 +++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index f4d3412b..07aaff1f 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -20,7 +20,8 @@ class PercentageFormatter extends BaseFormatter $format = str_replace('%', '%%', $format); $wholePartSize = strlen((string) floor($value)); - $decimalPartSize = $placeHolders = 0; + $decimalPartSize = 0; + $placeHolders = ''; // Number of decimals if (preg_match('/\.([?0]+)/u', $format, $matches)) { $decimalPartSize = strlen($matches[1]); @@ -29,12 +30,13 @@ class PercentageFormatter extends BaseFormatter $placeHolders = str_repeat(' ', strlen($matches[1]) - $decimalPartSize); } // Number of digits to display before the decimal - if (preg_match('/([#0,]+)\./u', $format, $matches)) { - $wholePartSize = max($wholePartSize, strlen($matches[1])); + if (preg_match('/([#0,]+)\.?/u', $format, $matches)) { + $firstZero = preg_replace('/^[#,]*/', '', $matches[1]); + $wholePartSize = max($wholePartSize, strlen($firstZero)); } - $wholePartSize += $decimalPartSize; - $replacement = "{$wholePartSize}.{$decimalPartSize}"; + $wholePartSize += $decimalPartSize + (int) ($decimalPartSize > 0); + $replacement = "0{$wholePartSize}.{$decimalPartSize}"; $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); /** @var float */ diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php index e386b292..f09c34d7 100644 --- a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php +++ b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php @@ -47,7 +47,7 @@ class NumberFormatTest extends TestCase public function testFormatValueWithMask($expectedResult, ...$args): void { $result = NumberFormat::toFormattedString(...$args); - self::assertEquals($expectedResult, $result); + self::assertSame($expectedResult, $result); } public function providerNumberFormat(): array diff --git a/tests/data/Style/NumberFormat.php b/tests/data/Style/NumberFormat.php index 80f7080b..b307e23d 100644 --- a/tests/data/Style/NumberFormat.php +++ b/tests/data/Style/NumberFormat.php @@ -146,13 +146,13 @@ return [ '#,###', ], [ - 12, + '12', 12000, '#,', ], // Scaling test [ - 12.199999999999999, + '12.2', 12200000, '0.0,,', ], @@ -1486,4 +1486,9 @@ return [ '-1111.119', NumberFormat::FORMAT_ACCOUNTING_EUR, ], + 'issue 1929' => ['(79.3%)', -0.793, '#,##0.0%;(#,##0.0%)'], + 'percent without leading 0' => ['6.2%', 0.062, '##.0%'], + 'percent with leading 0' => ['06.2%', 0.062, '00.0%'], + 'percent lead0 no decimal' => ['06%', 0.062, '00%'], + 'percent nolead0 no decimal' => ['6%', 0.062, '##%'], ]; From 441ae741d7eaca24e677f553456f9b0c6a9efcda Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 5 Sep 2022 06:36:42 +0200 Subject: [PATCH 54/69] Update Excel function samples for Date/Time and Engineering functions --- samples/Calculations/DateTime/DATEDIF.php | 1 + samples/Calculations/DateTime/DAYS.php | 1 + samples/Calculations/DateTime/NETWORKDAYS.php | 66 ++++++++++++++++ samples/Calculations/DateTime/WORKDAY.php | 67 ++++++++++++++++ samples/Calculations/DateTime/YEARFRAC.php | 76 +++++++++++++++++++ samples/Calculations/Engineering/BESSELI.php | 29 +++++++ samples/Calculations/Engineering/BESSELJ.php | 29 +++++++ samples/Calculations/Engineering/BESSELK.php | 29 +++++++ samples/Calculations/Engineering/BESSELY.php | 29 +++++++ samples/Calculations/Engineering/BIN2DEC.php | 46 +++++++++++ samples/Calculations/Engineering/BIN2HEX.php | 46 +++++++++++ samples/Calculations/Engineering/BIN2OCT.php | 46 +++++++++++ samples/Calculations/Engineering/BITAND.php | 49 ++++++++++++ .../Calculations/Engineering/BITLSHIFT.php | 65 ++++++++++++++++ samples/Calculations/Engineering/BITOR.php | 49 ++++++++++++ .../Calculations/Engineering/BITRSHIFT.php | 63 +++++++++++++++ samples/Calculations/Engineering/BITXOR.php | 49 ++++++++++++ samples/Calculations/Engineering/COMPLEX.php | 41 ++++++++++ samples/Calculations/Engineering/CONVERT.php | 58 ++++++++++++++ samples/Calculations/Engineering/DEC2BIN.php | 47 ++++++++++++ samples/Calculations/Engineering/DEC2HEX.php | 48 ++++++++++++ samples/Calculations/Engineering/DEC2OCT.php | 48 ++++++++++++ samples/Calculations/Engineering/DELTA.php | 46 +++++++++++ samples/Calculations/Engineering/ERF.php | 67 ++++++++++++++++ samples/Calculations/Engineering/ERFC.php | 41 ++++++++++ samples/Calculations/Engineering/GESTEP.php | 53 +++++++++++++ samples/Calculations/Engineering/HEX2BIN.php | 46 +++++++++++ samples/Calculations/Engineering/HEX2DEC.php | 48 ++++++++++++ samples/Calculations/Engineering/HEX2OCT.php | 46 +++++++++++ samples/Calculations/Engineering/IMABS.php | 48 ++++++++++++ .../Calculations/Engineering/IMAGINARY.php | 48 ++++++++++++ .../Calculations/Engineering/IMARGUMENT.php | 48 ++++++++++++ .../Calculations/Engineering/IMCONJUGATE.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCOS.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCOSH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCOT.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCSC.php | 48 ++++++++++++ samples/Calculations/Engineering/IMCSCH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMDIV.php | 42 ++++++++++ samples/Calculations/Engineering/IMEXP.php | 48 ++++++++++++ samples/Calculations/Engineering/IMLN.php | 48 ++++++++++++ samples/Calculations/Engineering/IMLOG10.php | 48 ++++++++++++ samples/Calculations/Engineering/IMLOG2.php | 48 ++++++++++++ samples/Calculations/Engineering/IMPOWER.php | 49 ++++++++++++ .../Calculations/Engineering/IMPRODUCT.php | 42 ++++++++++ samples/Calculations/Engineering/IMREAL.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSEC.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSECH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSIN.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSINH.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSQRT.php | 48 ++++++++++++ samples/Calculations/Engineering/IMSUB.php | 42 ++++++++++ samples/Calculations/Engineering/IMSUM.php | 42 ++++++++++ samples/Calculations/Engineering/IMTAN.php | 48 ++++++++++++ samples/Calculations/Engineering/OCT2BIN.php | 47 ++++++++++++ samples/Calculations/Engineering/OCT2DEC.php | 49 ++++++++++++ samples/Calculations/Engineering/OCT2HEX.php | 49 ++++++++++++ .../Calculation/Engineering/Compare.php | 4 +- tests/data/Calculation/DateTime/DATE.php | 5 +- .../data/Calculation/Engineering/BIN2DEC.php | 2 + 60 files changed, 2659 insertions(+), 3 deletions(-) create mode 100644 samples/Calculations/DateTime/NETWORKDAYS.php create mode 100644 samples/Calculations/DateTime/WORKDAY.php create mode 100644 samples/Calculations/DateTime/YEARFRAC.php create mode 100644 samples/Calculations/Engineering/BESSELI.php create mode 100644 samples/Calculations/Engineering/BESSELJ.php create mode 100644 samples/Calculations/Engineering/BESSELK.php create mode 100644 samples/Calculations/Engineering/BESSELY.php create mode 100644 samples/Calculations/Engineering/BIN2DEC.php create mode 100644 samples/Calculations/Engineering/BIN2HEX.php create mode 100644 samples/Calculations/Engineering/BIN2OCT.php create mode 100644 samples/Calculations/Engineering/BITAND.php create mode 100644 samples/Calculations/Engineering/BITLSHIFT.php create mode 100644 samples/Calculations/Engineering/BITOR.php create mode 100644 samples/Calculations/Engineering/BITRSHIFT.php create mode 100644 samples/Calculations/Engineering/BITXOR.php create mode 100644 samples/Calculations/Engineering/COMPLEX.php create mode 100644 samples/Calculations/Engineering/CONVERT.php create mode 100644 samples/Calculations/Engineering/DEC2BIN.php create mode 100644 samples/Calculations/Engineering/DEC2HEX.php create mode 100644 samples/Calculations/Engineering/DEC2OCT.php create mode 100644 samples/Calculations/Engineering/DELTA.php create mode 100644 samples/Calculations/Engineering/ERF.php create mode 100644 samples/Calculations/Engineering/ERFC.php create mode 100644 samples/Calculations/Engineering/GESTEP.php create mode 100644 samples/Calculations/Engineering/HEX2BIN.php create mode 100644 samples/Calculations/Engineering/HEX2DEC.php create mode 100644 samples/Calculations/Engineering/HEX2OCT.php create mode 100644 samples/Calculations/Engineering/IMABS.php create mode 100644 samples/Calculations/Engineering/IMAGINARY.php create mode 100644 samples/Calculations/Engineering/IMARGUMENT.php create mode 100644 samples/Calculations/Engineering/IMCONJUGATE.php create mode 100644 samples/Calculations/Engineering/IMCOS.php create mode 100644 samples/Calculations/Engineering/IMCOSH.php create mode 100644 samples/Calculations/Engineering/IMCOT.php create mode 100644 samples/Calculations/Engineering/IMCSC.php create mode 100644 samples/Calculations/Engineering/IMCSCH.php create mode 100644 samples/Calculations/Engineering/IMDIV.php create mode 100644 samples/Calculations/Engineering/IMEXP.php create mode 100644 samples/Calculations/Engineering/IMLN.php create mode 100644 samples/Calculations/Engineering/IMLOG10.php create mode 100644 samples/Calculations/Engineering/IMLOG2.php create mode 100644 samples/Calculations/Engineering/IMPOWER.php create mode 100644 samples/Calculations/Engineering/IMPRODUCT.php create mode 100644 samples/Calculations/Engineering/IMREAL.php create mode 100644 samples/Calculations/Engineering/IMSEC.php create mode 100644 samples/Calculations/Engineering/IMSECH.php create mode 100644 samples/Calculations/Engineering/IMSIN.php create mode 100644 samples/Calculations/Engineering/IMSINH.php create mode 100644 samples/Calculations/Engineering/IMSQRT.php create mode 100644 samples/Calculations/Engineering/IMSUB.php create mode 100644 samples/Calculations/Engineering/IMSUM.php create mode 100644 samples/Calculations/Engineering/IMTAN.php create mode 100644 samples/Calculations/Engineering/OCT2BIN.php create mode 100644 samples/Calculations/Engineering/OCT2DEC.php create mode 100644 samples/Calculations/Engineering/OCT2HEX.php diff --git a/samples/Calculations/DateTime/DATEDIF.php b/samples/Calculations/DateTime/DATEDIF.php index 7bc077c9..3e035f5a 100644 --- a/samples/Calculations/DateTime/DATEDIF.php +++ b/samples/Calculations/DateTime/DATEDIF.php @@ -24,6 +24,7 @@ $testDates = [ [2000, 1, 1], [2019, 2, 14], [2020, 7, 4], + [2020, 2, 29], ]; $testDateCount = count($testDates); diff --git a/samples/Calculations/DateTime/DAYS.php b/samples/Calculations/DateTime/DAYS.php index ddd47f37..15e9a58f 100644 --- a/samples/Calculations/DateTime/DAYS.php +++ b/samples/Calculations/DateTime/DAYS.php @@ -24,6 +24,7 @@ $testDates = [ [2000, 1, 1], [2019, 2, 14], [2020, 7, 4], + [2020, 2, 29], [2029, 12, 31], [2525, 1, 1], ]; diff --git a/samples/Calculations/DateTime/NETWORKDAYS.php b/samples/Calculations/DateTime/NETWORKDAYS.php new file mode 100644 index 00000000..585c0438 --- /dev/null +++ b/samples/Calculations/DateTime/NETWORKDAYS.php @@ -0,0 +1,66 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$publicHolidays = [ + [2022, 1, 3, '=DATE(G1, H1, I1)', 'New Year'], + [2022, 4, 15, '=DATE(G2, H2, I2)', 'Good Friday'], + [2022, 4, 18, '=DATE(G3, H3, I3)', 'Easter Monday'], + [2022, 5, 2, '=DATE(G4, H4, I4)', 'Early May Bank Holiday'], + [2022, 6, 2, '=DATE(G5, H5, I5)', 'Spring Bank Holiday'], + [2022, 6, 3, '=DATE(G6, H6, I6)', 'Platinum Jubilee Bank Holiday'], + [2022, 8, 29, '=DATE(G7, H7, I7)', 'Summer Bank Holiday'], + [2022, 12, 26, '=DATE(G8, H8, I8)', 'Boxing Day'], + [2022, 12, 27, '=DATE(G9, H9, I9)', 'Christmas Day'], +]; + +$holidayCount = count($publicHolidays); +$worksheet->fromArray($publicHolidays, null, 'G1', true); + +$worksheet->getStyle('J1:J' . $holidayCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->setCellValue('A1', '=DATE(2022,1,1)'); + +for ($numberOfMonths = 0; $numberOfMonths < 12; ++$numberOfMonths) { + $worksheet->setCellValue('B' . ($numberOfMonths + 1), '=EOMONTH(A1, ' . $numberOfMonths . ')'); + $worksheet->setCellValue('C' . ($numberOfMonths + 1), '=NETWORKDAYS(A1, B' . ($numberOfMonths + 1) . ')'); + $worksheet->setCellValue('D' . ($numberOfMonths + 1), '=NETWORKDAYS(A1, B' . ($numberOfMonths + 1) . ', J1:J' . $holidayCount . ')'); +} + +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('B1:B12') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log('UK Public Holidays'); +$holidayData = $worksheet->rangeToArray('J1:K' . $holidayCount, null, true, true, true); +$helper->displayGrid($holidayData); + +for ($row = 1; $row <= 12; ++$row) { + $helper->log(sprintf( + 'Between %s and %s is %d working days; %d with public holidays', + $worksheet->getCell('A1')->getFormattedValue(), + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/WORKDAY.php b/samples/Calculations/DateTime/WORKDAY.php new file mode 100644 index 00000000..d18e3463 --- /dev/null +++ b/samples/Calculations/DateTime/WORKDAY.php @@ -0,0 +1,67 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$publicHolidays = [ + [2022, 1, 3, '=DATE(G1, H1, I1)', 'New Year'], + [2022, 4, 15, '=DATE(G2, H2, I2)', 'Good Friday'], + [2022, 4, 18, '=DATE(G3, H3, I3)', 'Easter Monday'], + [2022, 5, 2, '=DATE(G4, H4, I4)', 'Early May Bank Holiday'], + [2022, 6, 2, '=DATE(G5, H5, I5)', 'Spring Bank Holiday'], + [2022, 6, 3, '=DATE(G6, H6, I6)', 'Platinum Jubilee Bank Holiday'], + [2022, 8, 29, '=DATE(G7, H7, I7)', 'Summer Bank Holiday'], + [2022, 12, 26, '=DATE(G8, H8, I8)', 'Boxing Day'], + [2022, 12, 27, '=DATE(G9, H9, I9)', 'Christmas Day'], +]; + +$holidayCount = count($publicHolidays); +$worksheet->fromArray($publicHolidays, null, 'G1', true); + +$worksheet->getStyle('J1:J' . $holidayCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->setCellValue('A1', '=DATE(2022,1,1)'); + +$workdayStep = 10; +for ($days = $workdayStep; $days <= 366; $days += $workdayStep) { + $worksheet->setCellValue('B' . ((int) $days / $workdayStep + 1), $days); + $worksheet->setCellValue('C' . ((int) $days / $workdayStep + 1), '=WORKDAY(A1, B' . ((int) $days / $workdayStep + 1) . ')'); + $worksheet->setCellValue('D' . ((int) $days / $workdayStep + 1), '=WORKDAY(A1, B' . ((int) $days / $workdayStep + 1) . ', J1:J' . $holidayCount . ')'); +} + +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('C1:D50') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log('UK Public Holidays'); +$holidayData = $worksheet->rangeToArray('J1:K' . $holidayCount, null, true, true, true); +$helper->displayGrid($holidayData); + +for ($days = $workdayStep; $days <= 366; $days += $workdayStep) { + $helper->log(sprintf( + '%d workdays from %s is %s; %s with public holidays', + $worksheet->getCell('B' . ((int) $days / $workdayStep + 1))->getFormattedValue(), + $worksheet->getCell('A1')->getFormattedValue(), + $worksheet->getCell('C' . ((int) $days / $workdayStep + 1))->getFormattedValue(), + $worksheet->getCell('D' . ((int) $days / $workdayStep + 1))->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/YEARFRAC.php b/samples/Calculations/DateTime/YEARFRAC.php new file mode 100644 index 00000000..81b36435 --- /dev/null +++ b/samples/Calculations/DateTime/YEARFRAC.php @@ -0,0 +1,76 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2020, 2, 29], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DATE(2022,12,31)'); + $worksheet->setCellValue('G' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ')'); + $worksheet->setCellValue('H' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 1)'); + $worksheet->setCellValue('I' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 2)'); + $worksheet->setCellValue('J' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 3)'); + $worksheet->setCellValue('K' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 4)'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log(sprintf( + 'Days: %f - US (NASD) 30/360', + $worksheet->getCell('G' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual', + $worksheet->getCell('H' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual/360', + $worksheet->getCell('I' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual/365', + $worksheet->getCell('J' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - European 30/360', + $worksheet->getCell('K' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/Engineering/BESSELI.php b/samples/Calculations/Engineering/BESSELI.php new file mode 100644 index 00000000..bd5d9b71 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELI.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELI({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELJ.php b/samples/Calculations/Engineering/BESSELJ.php new file mode 100644 index 00000000..3aa47886 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELJ.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELJ({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELK.php b/samples/Calculations/Engineering/BESSELK.php new file mode 100644 index 00000000..ee8698e9 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELK.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELK({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELY.php b/samples/Calculations/Engineering/BESSELY.php new file mode 100644 index 00000000..750b7204 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELY.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELY({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BIN2DEC.php b/samples/Calculations/Engineering/BIN2DEC.php new file mode 100644 index 00000000..0c4c4532 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2DEC.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BIN2HEX.php b/samples/Calculations/Engineering/BIN2HEX.php new file mode 100644 index 00000000..51a11199 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2HEX.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BIN2OCT.php b/samples/Calculations/Engineering/BIN2OCT.php new file mode 100644 index 00000000..c320d360 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2OCT.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITAND.php b/samples/Calculations/Engineering/BITAND.php new file mode 100644 index 00000000..2a8f7a3c --- /dev/null +++ b/samples/Calculations/Engineering/BITAND.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITAND(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise AND of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITLSHIFT.php b/samples/Calculations/Engineering/BITLSHIFT.php new file mode 100644 index 00000000..872c8098 --- /dev/null +++ b/samples/Calculations/Engineering/BITLSHIFT.php @@ -0,0 +1,65 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1], + [3], + [9], + [15], + [26], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); + $worksheet->setCellValue('C' . $row, '=BITLSHIFT(A' . $row . ',1)'); + $worksheet->setCellValue('D' . $row, '=DEC2BIN(C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=BITLSHIFT(A' . $row . ',2)'); + $worksheet->setCellValue('F' . $row, '=DEC2BIN(E' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=BITLSHIFT(A' . $row . ',3)'); + $worksheet->setCellValue('H' . $row, '=DEC2BIN(G' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 1 bit is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 2 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 3 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITOR.php b/samples/Calculations/Engineering/BITOR.php new file mode 100644 index 00000000..1bf7f71d --- /dev/null +++ b/samples/Calculations/Engineering/BITOR.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITOR(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise OR of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITRSHIFT.php b/samples/Calculations/Engineering/BITRSHIFT.php new file mode 100644 index 00000000..3e7f3a88 --- /dev/null +++ b/samples/Calculations/Engineering/BITRSHIFT.php @@ -0,0 +1,63 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [9], + [15], + [26], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); + $worksheet->setCellValue('C' . $row, '=BITRSHIFT(A' . $row . ',1)'); + $worksheet->setCellValue('D' . $row, '=DEC2BIN(C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=BITRSHIFT(A' . $row . ',2)'); + $worksheet->setCellValue('F' . $row, '=DEC2BIN(E' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=BITRSHIFT(A' . $row . ',3)'); + $worksheet->setCellValue('H' . $row, '=DEC2BIN(G' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 1 bit is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 2 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 3 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITXOR.php b/samples/Calculations/Engineering/BITXOR.php new file mode 100644 index 00000000..482662cd --- /dev/null +++ b/samples/Calculations/Engineering/BITXOR.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITXOR(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise XOR of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/COMPLEX.php b/samples/Calculations/Engineering/COMPLEX.php new file mode 100644 index 00000000..c58a1797 --- /dev/null +++ b/samples/Calculations/Engineering/COMPLEX.php @@ -0,0 +1,41 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3, 4], + [3, 4, '"j"'], + [3.5, 4.75], + [0, 1], + [1, 0], + [0, -1], + [0, 2], + [2, 0], +]; +$testDataCount = count($testData); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=COMPLEX(' . implode(',', $testData[$row - 1]) . ')'); +} + +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(A%d): Formula %s result is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/Engineering/CONVERT.php b/samples/Calculations/Engineering/CONVERT.php new file mode 100644 index 00000000..bce56ba5 --- /dev/null +++ b/samples/Calculations/Engineering/CONVERT.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$conversions = [ + [1, '"lbm"', '"kg"'], + [1, '"gal"', '"l"'], + [24, '"in"', '"ft"'], + [100, '"yd"', '"m"'], + [500, '"mi"', '"km"'], + [7.5, '"min"', '"sec"'], + [5, '"F"', '"C"'], + [32, '"C"', '"K"'], + [100, '"m2"', '"ft2"'], +]; +$testDataCount = count($conversions); + +$worksheet->fromArray($conversions, null, 'A1'); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=CONVERT(' . implode(',', $conversions[$row - 1]) . ')'); +} + +$worksheet->setCellValue('H1', '=CONVERT(CONVERT(100,"m","ft"),"m","ft")'); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(A%d): Unit of Measure Conversion Formula %s - %d %s is %f %s', + $row, + $worksheet->getCell('D' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + trim($worksheet->getCell('B' . $row)->getValue(), '"'), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + trim($worksheet->getCell('C' . $row)->getValue(), '"') + )); +} + +$helper->log('Old method for area conversions, before MS Excel introduced area Units of Measure'); + +$helper->log(sprintf( + '(A%d): Unit of Measure Conversion Formula %s result is %s', + $row, + $worksheet->getCell('H1')->getValue(), + $worksheet->getCell('H1')->getCalculatedValue() +)); diff --git a/samples/Calculations/Engineering/DEC2BIN.php b/samples/Calculations/Engineering/DEC2BIN.php new file mode 100644 index 00000000..2a064c06 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2BIN.php @@ -0,0 +1,47 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DEC2HEX.php b/samples/Calculations/Engineering/DEC2HEX.php new file mode 100644 index 00000000..0a19ae54 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2HEX.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], + [12345678], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DEC2OCT.php b/samples/Calculations/Engineering/DEC2OCT.php new file mode 100644 index 00000000..fc11a832 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2OCT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], + [12345678], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DELTA.php b/samples/Calculations/Engineering/DELTA.php new file mode 100644 index 00000000..cd51b161 --- /dev/null +++ b/samples/Calculations/Engineering/DELTA.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [4, 5], + [3, 3], + [0.5, 0], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=DELTA(A' . $row . ',B' . $row . ')'); +} + +$comparison = [ + 0 => 'The values are not equal', + 1 => 'The values are equal', +]; + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Compare values %d and %d - Result is %d - %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $comparison[$worksheet->getCell('C' . $row)->getCalculatedValue()] + )); +} diff --git a/samples/Calculations/Engineering/ERF.php b/samples/Calculations/Engineering/ERF.php new file mode 100644 index 00000000..e6505888 --- /dev/null +++ b/samples/Calculations/Engineering/ERF.php @@ -0,0 +1,67 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData1 = [ + [0.745], + [1], + [1.5], + [-2], +]; + +$testData2 = [ + [0, 1.5], + [1, 2], + [-2, 1], +]; +$testDataCount1 = count($testData1); +$testDataCount2 = count($testData2); +$testData2StartRow = $testDataCount1 + 1; + +$worksheet->fromArray($testData1, null, 'A1', true); +$worksheet->fromArray($testData2, null, "A{$testData2StartRow}", true); + +for ($row = 1; $row <= $testDataCount1; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERF(A' . $row . ')'); +} + +for ($row = $testDataCount1 + 1; $row <= $testDataCount2 + $testDataCount1; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERF(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +$helper->log('ERF() With a single argument'); +for ($row = 1; $row <= $testDataCount1; ++$row) { + $helper->log(sprintf( + '(C%d): %s The error function integrated between 0 and %f is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} + +$helper->log('ERF() With two arguments'); +for ($row = $testDataCount1 + 1; $row <= $testDataCount2 + $testDataCount1; ++$row) { + $helper->log(sprintf( + '(C%d): %s The error function integrated between %f and %f is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/ERFC.php b/samples/Calculations/Engineering/ERFC.php new file mode 100644 index 00000000..5e7bcc6d --- /dev/null +++ b/samples/Calculations/Engineering/ERFC.php @@ -0,0 +1,41 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [0], + [0.5], + [1], + [-1], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERFC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): %s The complementary error function integrated by %f and infinity is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/GESTEP.php b/samples/Calculations/Engineering/GESTEP.php new file mode 100644 index 00000000..73f0a31c --- /dev/null +++ b/samples/Calculations/Engineering/GESTEP.php @@ -0,0 +1,53 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [5, 4], + [5, 5], + [4, 5], + [-4, -5], + [-5, -4], + [1], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=GESTEP(A' . $row . ',B' . $row . ')'); +} + +$comparison = [ + 0 => 'Value %d is less than step %d', + 1 => 'Value %d is greater than or equal to step %d', +]; + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Compare value %d and step %d - Result is %d - %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + sprintf( + $comparison[$worksheet->getCell('C' . $row)->getCalculatedValue()], + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + ) + )); +} diff --git a/samples/Calculations/Engineering/HEX2BIN.php b/samples/Calculations/Engineering/HEX2BIN.php new file mode 100644 index 00000000..2ad08925 --- /dev/null +++ b/samples/Calculations/Engineering/HEX2BIN.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [8], + [42], + [99], + ['A2'], + ['F0'], + ['100'], + ['128'], + ['1AB'], + ['1FF'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/HEX2DEC.php b/samples/Calculations/Engineering/HEX2DEC.php new file mode 100644 index 00000000..745d4110 --- /dev/null +++ b/samples/Calculations/Engineering/HEX2DEC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['08'], + ['42'], + ['A2'], + ['400'], + ['1000'], + ['1234'], + ['ABCD'], + ['C3B0'], + ['FFFFFFFFF'], + ['FFFFFFFFFF'], + ['FFFFFFF800'], + ['FEDCBA9876'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/HEX2OCT.php b/samples/Calculations/Engineering/HEX2OCT.php new file mode 100644 index 00000000..3608c1bb --- /dev/null +++ b/samples/Calculations/Engineering/HEX2OCT.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['08'], + ['42'], + ['A2'], + ['400'], + ['100'], + ['1234'], + ['ABCD'], + ['C3B0'], + ['FFFFFFFFFF'], + ['FFFFFFF800'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMABS.php b/samples/Calculations/Engineering/IMABS.php new file mode 100644 index 00000000..9c6b843c --- /dev/null +++ b/samples/Calculations/Engineering/IMABS.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMABS(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The absolute value of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMAGINARY.php b/samples/Calculations/Engineering/IMAGINARY.php new file mode 100644 index 00000000..99138938 --- /dev/null +++ b/samples/Calculations/Engineering/IMAGINARY.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMAGINARY(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The imaginary component of %s is %f', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMARGUMENT.php b/samples/Calculations/Engineering/IMARGUMENT.php new file mode 100644 index 00000000..e559e951 --- /dev/null +++ b/samples/Calculations/Engineering/IMARGUMENT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMARGUMENT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Theta Argument of %s is %f radians', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCONJUGATE.php b/samples/Calculations/Engineering/IMCONJUGATE.php new file mode 100644 index 00000000..3b4429ed --- /dev/null +++ b/samples/Calculations/Engineering/IMCONJUGATE.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCONJUGATE(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Conjugate of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOS.php b/samples/Calculations/Engineering/IMCOS.php new file mode 100644 index 00000000..5b8f81ea --- /dev/null +++ b/samples/Calculations/Engineering/IMCOS.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOS(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cosine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOSH.php b/samples/Calculations/Engineering/IMCOSH.php new file mode 100644 index 00000000..9a393766 --- /dev/null +++ b/samples/Calculations/Engineering/IMCOSH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOSH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Cosine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOT.php b/samples/Calculations/Engineering/IMCOT.php new file mode 100644 index 00000000..e3d980cd --- /dev/null +++ b/samples/Calculations/Engineering/IMCOT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cotangent of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCSC.php b/samples/Calculations/Engineering/IMCSC.php new file mode 100644 index 00000000..ab6695d0 --- /dev/null +++ b/samples/Calculations/Engineering/IMCSC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCSC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cosecant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCSCH.php b/samples/Calculations/Engineering/IMCSCH.php new file mode 100644 index 00000000..4513d9e9 --- /dev/null +++ b/samples/Calculations/Engineering/IMCSCH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCSCH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Cosecant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMDIV.php b/samples/Calculations/Engineering/IMDIV.php new file mode 100644 index 00000000..9512be57 --- /dev/null +++ b/samples/Calculations/Engineering/IMDIV.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMDIV(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Quotient of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMEXP.php b/samples/Calculations/Engineering/IMEXP.php new file mode 100644 index 00000000..7f5837b2 --- /dev/null +++ b/samples/Calculations/Engineering/IMEXP.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMEXP(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Exponential of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLN.php b/samples/Calculations/Engineering/IMLN.php new file mode 100644 index 00000000..95618257 --- /dev/null +++ b/samples/Calculations/Engineering/IMLN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Natural Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLOG10.php b/samples/Calculations/Engineering/IMLOG10.php new file mode 100644 index 00000000..d501c3de --- /dev/null +++ b/samples/Calculations/Engineering/IMLOG10.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLOG10(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Base-10 Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLOG2.php b/samples/Calculations/Engineering/IMLOG2.php new file mode 100644 index 00000000..25986b39 --- /dev/null +++ b/samples/Calculations/Engineering/IMLOG2.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLOG2(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Base-2 Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMPOWER.php b/samples/Calculations/Engineering/IMPOWER.php new file mode 100644 index 00000000..c6674fbe --- /dev/null +++ b/samples/Calculations/Engineering/IMPOWER.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', 2], + ['5-12i', 2], + ['3.25+7.5i', 3], + ['3.25-12.5i', 2], + ['-3.25+7.5i', 3], + ['-3.25-7.5i', 4], + ['0-j', 5], + ['0-2.5j', 3], + ['0+j', 2.5], + ['0+1.25j', 2], + [4, 3], + [-2.5, 2], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMPOWER(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): %s raised to the power of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMPRODUCT.php b/samples/Calculations/Engineering/IMPRODUCT.php new file mode 100644 index 00000000..f81bc666 --- /dev/null +++ b/samples/Calculations/Engineering/IMPRODUCT.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMPRODUCT(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Product of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMREAL.php b/samples/Calculations/Engineering/IMREAL.php new file mode 100644 index 00000000..4e537c0f --- /dev/null +++ b/samples/Calculations/Engineering/IMREAL.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMREAL(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The real component of %s is %f radians', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSEC.php b/samples/Calculations/Engineering/IMSEC.php new file mode 100644 index 00000000..e6c524b3 --- /dev/null +++ b/samples/Calculations/Engineering/IMSEC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Secant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSECH.php b/samples/Calculations/Engineering/IMSECH.php new file mode 100644 index 00000000..e07b6e08 --- /dev/null +++ b/samples/Calculations/Engineering/IMSECH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSECH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Secant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSIN.php b/samples/Calculations/Engineering/IMSIN.php new file mode 100644 index 00000000..d3b8c281 --- /dev/null +++ b/samples/Calculations/Engineering/IMSIN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Sine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSINH.php b/samples/Calculations/Engineering/IMSINH.php new file mode 100644 index 00000000..ac0a9039 --- /dev/null +++ b/samples/Calculations/Engineering/IMSINH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSINH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Sine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSQRT.php b/samples/Calculations/Engineering/IMSQRT.php new file mode 100644 index 00000000..c2573c91 --- /dev/null +++ b/samples/Calculations/Engineering/IMSQRT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSQRT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Square Root of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSUB.php b/samples/Calculations/Engineering/IMSUB.php new file mode 100644 index 00000000..90bd27a4 --- /dev/null +++ b/samples/Calculations/Engineering/IMSUB.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMSUB(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Difference between %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSUM.php b/samples/Calculations/Engineering/IMSUM.php new file mode 100644 index 00000000..2a8be320 --- /dev/null +++ b/samples/Calculations/Engineering/IMSUM.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMSUM(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Sum of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMTAN.php b/samples/Calculations/Engineering/IMTAN.php new file mode 100644 index 00000000..ffaa53b2 --- /dev/null +++ b/samples/Calculations/Engineering/IMTAN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMTAN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Tangent of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2BIN.php b/samples/Calculations/Engineering/OCT2BIN.php new file mode 100644 index 00000000..9c4bbf86 --- /dev/null +++ b/samples/Calculations/Engineering/OCT2BIN.php @@ -0,0 +1,47 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [7], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [567], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2DEC.php b/samples/Calculations/Engineering/OCT2DEC.php new file mode 100644 index 00000000..ea6afb2f --- /dev/null +++ b/samples/Calculations/Engineering/OCT2DEC.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [7], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [4567], + [7777700001], + [7777776543], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2HEX.php b/samples/Calculations/Engineering/OCT2HEX.php new file mode 100644 index 00000000..47e9b6e1 --- /dev/null +++ b/samples/Calculations/Engineering/OCT2HEX.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [12], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [4567], + [7777700001], + [7777776543], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php index 4d4bc07e..6aaf1faa 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php @@ -57,7 +57,7 @@ class Compare * * @param array|float $number the value to test against step * Or can be an array of values - * @param array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. + * @param null|array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. * Or can be an array of values * * @return array|int|string (string in the event of an error) @@ -72,7 +72,7 @@ class Compare try { $number = EngineeringValidations::validateFloat($number); - $step = EngineeringValidations::validateFloat($step); + $step = EngineeringValidations::validateFloat($step ?? 0.0); } catch (Exception $e) { return $e->getMessage(); } diff --git a/tests/data/Calculation/DateTime/DATE.php b/tests/data/Calculation/DateTime/DATE.php index 72816b76..d8b4303a 100644 --- a/tests/data/Calculation/DateTime/DATE.php +++ b/tests/data/Calculation/DateTime/DATE.php @@ -1,7 +1,9 @@ Date: Wed, 14 Sep 2022 07:11:20 -0700 Subject: [PATCH 55/69] Scrutinizer Clean Up Tests (#3061) * Scrutinizer Clean Up Tests No source code involved. * Scrutinizer Whack-a-mole Fixed 17, added 10. Trying again. * Simplify Some Tests Eliminate some null assertions. * Dead Code Remove 2 statements. --- infra/DocumentGenerator.php | 2 +- ...dling_loader_exceptions_using_TryCatch.php | 2 +- src/PhpSpreadsheet/Spreadsheet.php | 13 ++ .../Engineering/ParseComplexTest.php | 8 +- .../Functions/Financial/IrrTest.php | 45 +++-- .../Functions/LookupRef/ColumnsTest.php | 8 +- .../Functions/LookupRef/IndexTest.php | 8 +- .../Functions/LookupRef/RowsTest.php | 8 +- .../Functions/MathTrig/RandBetweenTest.php | 2 +- tests/PhpSpreadsheetTests/DefinedNameTest.php | 36 ++-- .../Functional/PrintAreaTest.php | 3 +- .../PhpSpreadsheetTests/NamedFormulaTest.php | 12 +- tests/PhpSpreadsheetTests/NamedRangeTest.php | 12 +- .../Reader/Csv/CsvContiguousTest.php | 10 +- .../Reader/Xls/NumberFormatGeneralTest.php | 24 +-- .../Reader/Xls/RichTextSizeTest.php | 3 +- .../Reader/Xlsx/AutoFilter2Test.php | 9 +- .../Reader/Xlsx/ChartSheetTest.php | 7 +- .../Reader/Xlsx/Issue2501Test.php | 16 +- .../Reader/Xlsx/NamespaceNonStdTest.php | 10 +- .../Reader/Xlsx/NamespaceOpenpyxl35Test.php | 10 +- .../Reader/Xlsx/NamespaceStdTest.php | 10 +- .../Reader/Xlsx/PageSetup2Test.php | 3 +- .../Shared/PasswordHasherTest.php | 23 ++- tests/PhpSpreadsheetTests/SpreadsheetTest.php | 171 ++++++++++-------- .../ConditionalFormatting/CellMatcherTest.php | 92 ++++++---- .../Wizard/WizardFactoryTest.php | 9 +- .../Writer/Xls/VisibilityTest.php | 2 +- .../Writer/Xlsx/UnparsedDataCloneTest.php | 12 +- .../Writer/Xlsx/VisibilityTest.php | 2 +- tests/data/Calculation/Financial/IRR.php | 1 + 31 files changed, 292 insertions(+), 281 deletions(-) diff --git a/infra/DocumentGenerator.php b/infra/DocumentGenerator.php index e2c3c86c..8a6be076 100644 --- a/infra/DocumentGenerator.php +++ b/infra/DocumentGenerator.php @@ -41,7 +41,7 @@ class DocumentGenerator private static function tableRow(array $lengths, ?array $values = null): string { $result = ''; - foreach (array_map(null, $lengths, $values ?? []) as $i => [$length, $value]) { + foreach (array_map(/** @scrutinizer ignore-type */ null, $lengths, $values ?? []) as $i => [$length, $value]) { $pad = $value === null ? '-' : ' '; if ($i > 0) { $result .= '|' . $pad; diff --git a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php index 603f6cb8..d984b7b6 100644 --- a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php +++ b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php @@ -11,5 +11,5 @@ $helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFi try { $spreadsheet = IOFactory::load($inputFileName); } catch (ReaderException $e) { - $helper->log('Error loading file "' . pathinfo($inputFileName, PATHINFO_BASENAME) . '": ' . $e->getMessage()); + $helper->log('Error loading file "' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . '": ' . $e->getMessage()); } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 4bb93987..364700e2 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -724,6 +724,19 @@ class Spreadsheet return null; } + /** + * Get sheet by name, throwing exception if not found. + */ + public function getSheetByNameOrThrow(string $worksheetName): Worksheet + { + $worksheet = $this->getSheetByName($worksheetName); + if ($worksheet === null) { + throw new Exception("Sheet $worksheetName does not exist."); + } + + return $worksheet; + } + /** * Get index for sheet. * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php index 1022052e..a32d946f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php @@ -3,21 +3,15 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ParseComplexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - public function testParseComplex(): void { [$real, $imaginary, $suffix] = [1.23e-4, 5.67e+8, 'j']; - $result = Engineering::parseComplex('1.23e-4+5.67e+8j'); + $result = /** @scrutinizer ignore-deprecated */ Engineering::parseComplex('1.23e-4+5.67e+8j'); self::assertArrayHasKey('real', $result); self::assertEquals($real, $result['real']); self::assertArrayHasKey('imaginary', $result); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php index 3c4bdb5a..2565f6c2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php @@ -2,26 +2,45 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PHPUnit\Framework\TestCase; - -class IrrTest extends TestCase +class IrrTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerIRR * * @param mixed $expectedResult + * @param mixed $values */ - public function testIRR($expectedResult, ...$args): void + public function testIRR($expectedResult, $values = null): void { - $result = Financial::IRR(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $formula = '=IRR('; + if ($values !== null) { + if (is_array($values)) { + $row = 0; + foreach ($values as $value) { + if (is_array($value)) { + foreach ($value as $arrayValue) { + ++$row; + $sheet->getCell("A$row")->setValue($arrayValue); + } + } else { + ++$row; + $sheet->getCell("A$row")->setValue($value); + } + } + $formula .= "A1:A$row"; + } else { + $sheet->getCell('A1')->setValue($values); + $formula .= 'A1'; + } + } + $formula .= ')'; + $sheet->getCell('D1')->setValue($formula); + $result = $sheet->getCell('D1')->getCalculatedValue(); + $this->adjustResult($result, $expectedResult); + + self::assertEqualsWithDelta($expectedResult, $result, 0.1E-8); } public function providerIRR(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php index f4f9bb9e..e8790cbf 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class ColumnsTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOLUMNS * @@ -21,7 +15,7 @@ class ColumnsTest extends TestCase */ public function testCOLUMNS($expectedResult, ...$args): void { - $result = LookupRef::COLUMNS(...$args); + $result = LookupRef::COLUMNS(/** @scrutinizer ignore-type */ ...$args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php index 03c87b50..fa3f4848 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class IndexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerINDEX * @@ -21,7 +15,7 @@ class IndexTest extends TestCase */ public function testINDEX($expectedResult, ...$args): void { - $result = LookupRef::INDEX(...$args); + $result = LookupRef::INDEX(/** @scrutinizer ignore-type */ ...$args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php index 2155bdf1..fd9902f4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class RowsTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerROWS * @@ -21,7 +15,7 @@ class RowsTest extends TestCase */ public function testROWS($expectedResult, ...$args): void { - $result = LookupRef::ROWS(...$args); + $result = LookupRef::ROWS(/** @scrutinizer ignore-type */ ...$args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php index 99cc1fa8..50b7117c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php @@ -60,7 +60,7 @@ class RandBetweenTest extends AllSetupTeardown $formula = "=RandBetween({$argument1}, {$argument2})"; $result = $calculation->_calculateFormulaValue($formula); self::assertIsArray($result); - self::assertCount($expectedRows, $result); + self::assertCount($expectedRows, /** @scrutinizer ignore-type */ $result); self::assertIsArray($result[0]); self::assertCount($expectedColumns, /** @scrutinizer ignore-type */ $result[0]); } diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 82950880..c001f204 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -58,7 +58,7 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); self::assertCount(2, $this->spreadsheet->getDefinedNames()); @@ -66,7 +66,7 @@ class DefinedNameTest extends TestCase self::assertNotNull($definedName1); self::assertSame('=A1', $definedName1->getValue()); - $definedName2 = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $definedName2 = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($definedName2); self::assertSame('=B1', $definedName2->getValue()); } @@ -103,13 +103,13 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getDefinedNames()); - $definedName = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $definedName = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($definedName); self::assertSame('=B1', $definedName->getValue()); } @@ -120,10 +120,10 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); - $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getDefinedNames()); $definedName = $this->spreadsheet->getDefinedName('foo'); @@ -154,10 +154,8 @@ class DefinedNameTest extends TestCase public function testChangeWorksheet(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -172,10 +170,8 @@ class DefinedNameTest extends TestCase public function testLocalOnly(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -190,10 +186,8 @@ class DefinedNameTest extends TestCase public function testScope(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -208,10 +202,8 @@ class DefinedNameTest extends TestCase public function testClone(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); diff --git a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php index 700c7c7f..921c59f5 100644 --- a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php +++ b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php @@ -58,8 +58,7 @@ class PrintAreaTest extends AbstractFunctional private static function getPrintArea(Spreadsheet $spreadsheet, string $name): string { - $sheet = $spreadsheet->getSheetByName($name); - self::assertNotNull($sheet, "Unable to get sheet $name"); + $sheet = $spreadsheet->getSheetByNameOrThrow($name); return $sheet->getPageSetup()->getPrintArea(); } diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 02e9d818..28857abe 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -62,7 +62,7 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); self::assertCount(2, $this->spreadsheet->getNamedFormulae()); @@ -72,7 +72,7 @@ class NamedFormulaTest extends TestCase '=19%', $formula->getValue() ); - $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($formula); self::assertSame( '=16%', @@ -100,13 +100,13 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getNamedFormulae()); - $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($formula); self::assertSame( '=16%', @@ -120,10 +120,10 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); - $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getNamedFormulae()); $formula = $this->spreadsheet->getNamedFormula('foo'); diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index 402e7eba..9440ef21 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -62,7 +62,7 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); self::assertCount(2, $this->spreadsheet->getNamedRanges()); @@ -72,7 +72,7 @@ class NamedRangeTest extends TestCase '=A1', $range->getValue() ); - $range = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $range = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($range); self::assertSame( '=B1', @@ -100,13 +100,13 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getNamedRanges()); - $sheet = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $sheet = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($sheet); self::assertSame( '=B1', @@ -120,10 +120,10 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); - $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getNamedRanges()); $range = $this->spreadsheet->getNamedRange('foo'); diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php index 291a29d0..8c28c1e7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php @@ -56,13 +56,11 @@ class CsvContiguousTest extends TestCase private static function getCellValue(Spreadsheet $spreadsheet, string $sheetName, string $cellAddress): string { - $sheet = $spreadsheet->getSheetByName($sheetName); + $sheet = $spreadsheet->getSheetByNameOrThrow($sheetName); $result = ''; - if ($sheet !== null) { - $value = $sheet->getCell($cellAddress)->getValue(); - if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { - $result = (string) $value; - } + $value = $sheet->getCell($cellAddress)->getValue(); + if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { + $result = (string) $value; } return $result; diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php index a67fbb34..80867892 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php @@ -15,20 +15,16 @@ class NumberFormatGeneralTest extends AbstractFunctional $reader = new Xls(); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('Blad1'); - if ($sheet === null) { - self::fail('Expected to find sheet Blad1'); - } else { - $array = $sheet->toArray(); - self::assertSame('€ 2.95', $array[1][3]); - self::assertSame(2.95, $sheet->getCell('D2')->getValue()); - self::assertSame(2.95, $sheet->getCell('D2')->getCalculatedValue()); - self::assertSame('€ 2.95', $sheet->getCell('D2')->getFormattedValue()); - self::assertSame(21, $array[1][4]); - self::assertSame(21, $sheet->getCell('E2')->getValue()); - self::assertSame(21, $sheet->getCell('E2')->getCalculatedValue()); - self::assertSame('21', $sheet->getCell('E2')->getFormattedValue()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('Blad1'); + $array = $sheet->toArray(); + self::assertSame('€ 2.95', $array[1][3]); + self::assertSame(2.95, $sheet->getCell('D2')->getValue()); + self::assertSame(2.95, $sheet->getCell('D2')->getCalculatedValue()); + self::assertSame('€ 2.95', $sheet->getCell('D2')->getFormattedValue()); + self::assertSame(21, $array[1][4]); + self::assertSame(21, $sheet->getCell('E2')->getValue()); + self::assertSame(21, $sheet->getCell('E2')->getCalculatedValue()); + self::assertSame('21', $sheet->getCell('E2')->getFormattedValue()); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php index 54274e8c..cf45dec1 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php @@ -12,8 +12,7 @@ class RichTextSizeTest extends AbstractFunctional $filename = 'tests/data/Reader/XLS/RichTextFontSize.xls'; $reader = new Xls(); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('橱柜门板'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('橱柜门板'); $text = $sheet->getCell('L15')->getValue(); $elements = $text->getRichTextElements(); self::assertEquals(10, $elements[2]->getFont()->getSize()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 57c09de5..06d6b562 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -28,8 +28,7 @@ class AutoFilter2Test extends TestCase public function testReadDateRange(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('daterange'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('daterange'); $filter = $sheet->getAutoFilter(); $maxRow = 30; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -61,8 +60,7 @@ class AutoFilter2Test extends TestCase public function testReadTopTen(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('top10'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('top10'); $filter = $sheet->getAutoFilter(); $maxRow = 65; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -87,8 +85,7 @@ class AutoFilter2Test extends TestCase public function testReadDynamic(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('dynamic'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('dynamic'); $filter = $sheet->getAutoFilter(); $maxRow = 30; self::assertSame("A1:A$maxRow", $filter->getRange()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php index 0f1605ff..e7862697 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class ChartSheetTest extends TestCase @@ -16,8 +15,7 @@ class ChartSheetTest extends TestCase $spreadsheet = $reader->load($filename); self::assertCount(2, $spreadsheet->getAllSheets()); - $chartSheet = $spreadsheet->getSheetByName('Chart1'); - self::assertInstanceOf(Worksheet::class, $chartSheet); + $chartSheet = $spreadsheet->getSheetByNameOrThrow('Chart1'); self::assertSame(1, $chartSheet->getChartCount()); } @@ -29,7 +27,6 @@ class ChartSheetTest extends TestCase $spreadsheet = $reader->load($filename); self::assertCount(1, $spreadsheet->getAllSheets()); - $chartSheet = $spreadsheet->getSheetByName('Chart1'); - self::assertNull($chartSheet); + self::assertNull($spreadsheet->getSheetByName('Chart1')); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php index 6b090fe8..57958245 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php @@ -42,28 +42,20 @@ class Issue2501Test extends TestCase $filename = self::$testbook; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('Columns'); + $sheet = $spreadsheet->getSheetByNameOrThrow('Columns'); $expected = [ 'A1:A1048576', 'B1:D1048576', 'E2:E4', ]; - if ($sheet === null) { - self::fail('Unable to find sheet Columns'); - } else { - self::assertSame($expected, array_values($sheet->getMergeCells())); - } - $sheet = $spreadsheet->getSheetByName('Rows'); + self::assertSame($expected, array_values($sheet->getMergeCells())); + $sheet = $spreadsheet->getSheetByNameOrThrow('Rows'); $expected = [ 'A1:XFD1', 'A2:XFD4', 'B5:D5', ]; - if ($sheet === null) { - self::fail('Unable to find sheet Rows'); - } else { - self::assertSame($expected, array_values($sheet->getMergeCells())); - } + self::assertSame($expected, array_values($sheet->getMergeCells())); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php index 1849c5bd..cc2c03dc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceNonStdTest.php @@ -62,13 +62,9 @@ class NamespaceNonStdTest extends \PHPUnit\Framework\TestCase self::assertSame('A2', $sheet->getFreezePane()); self::assertSame('A2', $sheet->getTopLeftCell()); self::assertSame('B3', $sheet->getSelectedCells()); - $sheet = $spreadsheet->getSheetByName('SylkTest'); - if ($sheet === null) { - self::fail('Unable to load expected sheet'); - } else { - self::assertNull($sheet->getFreezePane()); - self::assertNull($sheet->getTopLeftCell()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('SylkTest'); + self::assertNull($sheet->getFreezePane()); + self::assertNull($sheet->getTopLeftCell()); } public function testLoadXlsx(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php index 1b1743c9..afb36862 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php @@ -100,13 +100,9 @@ class NamespaceOpenpyxl35Test extends \PHPUnit\Framework\TestCase ], ]; foreach ($expectedArray as $sheetName => $array1) { - $sheet = $spreadsheet->getSheetByName($sheetName); - if ($sheet === null) { - self::fail("Unable to find sheet $sheetName"); - } else { - foreach ($array1 as $key => $value) { - self::assertSame($value, self::getCellValue($sheet, $key), "error in sheet $sheetName cell $key"); - } + $sheet = $spreadsheet->getSheetByNameOrThrow($sheetName); + foreach ($array1 as $key => $value) { + self::assertSame($value, self::getCellValue($sheet, $key), "error in sheet $sheetName cell $key"); } } $spreadsheet->disconnectWorksheets(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php index 8b47d141..5f8c8bc3 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceStdTest.php @@ -62,13 +62,9 @@ class NamespaceStdTest extends \PHPUnit\Framework\TestCase self::assertSame('A2', $sheet->getFreezePane()); self::assertSame('A2', $sheet->getTopLeftCell()); self::assertSame('B3', $sheet->getSelectedCells()); - $sheet = $spreadsheet->getSheetByName('SylkTest'); - if ($sheet === null) { - self::fail('Unable to load expected sheet'); - } else { - self::assertNull($sheet->getFreezePane()); - self::assertNull($sheet->getTopLeftCell()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('SylkTest'); + self::assertNull($sheet->getFreezePane()); + self::assertNull($sheet->getTopLeftCell()); } public function testLoadXlsx(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php index 1bbc88ac..12675b41 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php @@ -28,8 +28,7 @@ class PageSetup2Test extends TestCase public function testColumnBreak(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('colbreak'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('colbreak'); $breaks = $sheet->getBreaks(); self::assertCount(1, $breaks); $break = $breaks['D1'] ?? null; diff --git a/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php b/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php index 4b7923d8..c9912b8c 100644 --- a/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php +++ b/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php @@ -10,16 +10,27 @@ class PasswordHasherTest extends TestCase { /** * @dataProvider providerHashPassword - * - * @param mixed $expectedResult */ - public function testHashPassword($expectedResult, ...$args): void - { + public function testHashPassword( + string $expectedResult, + string $password, + ?string $algorithm = null, + ?string $salt = null, + ?int $spinCount = null + ): void { if ($expectedResult === 'exception') { $this->expectException(SpException::class); } - $result = PasswordHasher::hashPassword(...$args); - self::assertEquals($expectedResult, $result); + if ($algorithm === null) { + $result = PasswordHasher::hashPassword($password); + } elseif ($salt === null) { + $result = PasswordHasher::hashPassword($password, $algorithm); + } elseif ($spinCount === null) { + $result = PasswordHasher::hashPassword($password, $algorithm, $salt); + } else { + $result = PasswordHasher::hashPassword($password, $algorithm, $salt, $spinCount); + } + self::assertSame($expectedResult, $result); } public function providerHashPassword(): array diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index 11fb56e4..76e28b3d 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -2,28 +2,38 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Exception as ssException; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class SpreadsheetTest extends TestCase { - /** @var Spreadsheet */ - private $object; + /** @var ?Spreadsheet */ + private $spreadsheet; - protected function setUp(): void + protected function tearDown(): void { - parent::setUp(); - $this->object = new Spreadsheet(); - $sheet = $this->object->getActiveSheet(); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + private function getSpreadsheet(): Spreadsheet + { + $this->spreadsheet = $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('someSheet1'); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); $sheet = new Worksheet(); $sheet->setTitle('someSheet 3'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); + + return $spreadsheet; } public function dataProviderForSheetNames(): array @@ -35,6 +45,7 @@ class SpreadsheetTest extends TestCase [1, "'someSheet2'"], [2, 'someSheet 3'], [2, "'someSheet 3'"], + [null, 'someSheet 33'], ]; return $array; @@ -43,135 +54,153 @@ class SpreadsheetTest extends TestCase /** * @dataProvider dataProviderForSheetNames */ - public function testGetSheetByName(int $index, string $sheetName): void + public function testGetSheetByName(?int $index, string $sheetName): void { - self::assertSame($this->object->getSheet($index), $this->object->getSheetByName($sheetName)); + $spreadsheet = $this->getSpreadsheet(); + if ($index === null) { + self::assertNull($spreadsheet->getSheetByName($sheetName)); + } else { + self::assertSame($spreadsheet->getSheet($index), $spreadsheet->getSheetByName($sheetName)); + } } public function testAddSheetDuplicateTitle(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); } public function testAddSheetNoAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->addSheet($sheet); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); } public function testAddSheetAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet0'); - $this->object->addSheet($sheet, 0); - self::assertEquals(3, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet, 0); + self::assertEquals(3, $spreadsheet->getActiveSheetIndex()); } public function testRemoveSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->removeSheetByIndex(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->removeSheetByIndex(4); } public function testRemoveSheetNoAdjustActive(): void { - $this->object->setActiveSheetIndex(1); - self::assertEquals(1, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(2); - self::assertEquals(1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(1); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(2); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); } public function testRemoveSheetAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(1); - self::assertEquals(1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(1); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); } public function testGetSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->getSheet(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->getSheet(4); } public function testGetIndexNonExistent(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->getIndex($sheet); + $spreadsheet->getIndex($sheet); } public function testSetIndexByName(): void { - $this->object->setIndexByName('someSheet1', 1); - self::assertEquals('someSheet2', $this->object->getSheet(0)->getTitle()); - self::assertEquals('someSheet1', $this->object->getSheet(1)->getTitle()); - self::assertEquals('someSheet 3', $this->object->getSheet(2)->getTitle()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setIndexByName('someSheet1', 1); + self::assertEquals('someSheet2', $spreadsheet->getSheet(0)->getTitle()); + self::assertEquals('someSheet1', $spreadsheet->getSheet(1)->getTitle()); + self::assertEquals('someSheet 3', $spreadsheet->getSheet(2)->getTitle()); } public function testRemoveAllSheets(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(1, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(0, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(-1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(-1, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->addSheet($sheet); - self::assertEquals(0, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); } public function testBug1735(): void { - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $spreadsheet->createSheet()->setTitle('addedsheet'); - $spreadsheet->setActiveSheetIndex(1); - $spreadsheet->removeSheetByIndex(0); - $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet1 = new Spreadsheet(); + $spreadsheet1->createSheet()->setTitle('addedsheet'); + $spreadsheet1->setActiveSheetIndex(1); + $spreadsheet1->removeSheetByIndex(0); + $sheet = $spreadsheet1->getActiveSheet(); self::assertEquals('addedsheet', $sheet->getTitle()); } public function testSetActiveSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->setActiveSheetIndex(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->setActiveSheetIndex(4); } public function testSetActiveSheetNoSuchName(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->setActiveSheetIndexByName('unknown'); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->setActiveSheetIndexByName('unknown'); } public function testAddExternal(): void { - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $sheet = $spreadsheet->createSheet()->setTitle('someSheet19'); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet1 = new Spreadsheet(); + $sheet = $spreadsheet1->createSheet()->setTitle('someSheet19'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); $sheet->getCell('B1')->getStyle()->getFont()->setSuperscript(true); $sheet->getCell('C1')->getStyle()->getFont()->setSubscript(true); - self::assertCount(4, $spreadsheet->getCellXfCollection()); + self::assertCount(4, $spreadsheet1->getCellXfCollection()); self::assertEquals(1, $sheet->getCell('A1')->getXfIndex()); - $this->object->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); - self::assertCount(2, $this->object->getCellXfCollection()); - $sheet3 = $this->object->addExternalSheet($sheet); - self::assertCount(6, $this->object->getCellXfCollection()); + $spreadsheet->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); + self::assertCount(2, $spreadsheet->getCellXfCollection()); + $sheet3 = $spreadsheet->addExternalSheet($sheet); + self::assertCount(6, $spreadsheet->getCellXfCollection()); self::assertEquals('someSheet19', $sheet3->getTitle()); self::assertEquals(1, $sheet3->getCell('A1')->getValue()); self::assertTrue($sheet3->getCell('A1')->getStyle()->getFont()->getBold()); @@ -181,17 +210,17 @@ class SpreadsheetTest extends TestCase public function testAddExternalDuplicateName(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $this->expectException(ssException::class); + $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->createSheet()->setTitle('someSheet1'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); - $this->object->addExternalSheet($sheet); + $spreadsheet->addExternalSheet($sheet); } public function testAddExternalColumnDimensionStyles(): void { - $spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet1 = new Spreadsheet(); $sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension'); $sheet1->getCell('A1')->setValue(1); $sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true); @@ -200,7 +229,7 @@ class SpreadsheetTest extends TestCase self::assertEquals(1, $index); self::assertCount(2, $spreadsheet1->getCellXfCollection()); - $spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet2 = new Spreadsheet(); $sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles'); $sheet2->getCell('A1')->setValue(1); $sheet2->getCell('A1')->getStyle()->getFont()->setBold(true); @@ -220,7 +249,7 @@ class SpreadsheetTest extends TestCase public function testAddExternalRowDimensionStyles(): void { - $spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet1 = new Spreadsheet(); $sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension'); $sheet1->getCell('A1')->setValue(1); $sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true); @@ -229,7 +258,7 @@ class SpreadsheetTest extends TestCase self::assertEquals(1, $index); self::assertCount(2, $spreadsheet1->getCellXfCollection()); - $spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet2 = new Spreadsheet(); $sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles'); $sheet2->getCell('A1')->setValue(1); $sheet2->getCell('A1')->getStyle()->getFont()->setBold(true); diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 2c6d0da8..decbad5b 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -2,9 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Style\ConditionalFormatting; +use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Exception as ssException; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellMatcher; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase @@ -30,18 +33,26 @@ class CellMatcherTest extends TestCase } } + private function confirmString(Worksheet $worksheet, Cell $cell, string $cellAddress): string + { + $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()) ?? ''; + if ($cfRange === '') { + self::fail("{$cellAddress} is not in a Conditional Format range"); + } + + return $cfRange; + } + /** * @dataProvider basicCellIsComparisonDataProvider */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "{$cellAddress} is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -83,18 +94,35 @@ class CellMatcherTest extends TestCase ]; } + public function testNotInRange(): void + { + $this->spreadsheet = $this->loadSpreadsheet(); + $sheetname = 'cellIs Comparison'; + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); + $cell = $worksheet->getCell('J20'); + + $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); + self::assertNull($cfRange); + } + + public function testUnknownSheet(): void + { + $this->expectException(ssException::class); + $this->spreadsheet = $this->loadSpreadsheet(); + $sheetname = 'cellIs Comparisonxxx'; + $this->spreadsheet->getSheetByNameOrThrow($sheetname); + } + /** * @dataProvider rangeCellIsComparisonDataProvider */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -132,12 +160,10 @@ class CellMatcherTest extends TestCase public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -168,12 +194,10 @@ class CellMatcherTest extends TestCase public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -214,12 +238,10 @@ class CellMatcherTest extends TestCase public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -324,12 +346,10 @@ class CellMatcherTest extends TestCase public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -357,12 +377,10 @@ class CellMatcherTest extends TestCase public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -389,12 +407,10 @@ class CellMatcherTest extends TestCase public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -433,12 +449,10 @@ class CellMatcherTest extends TestCase public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::AssertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -469,12 +483,10 @@ class CellMatcherTest extends TestCase public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { $this->spreadsheet = $this->loadSpreadsheet(); - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - self::assertNotNull($worksheet, "$sheetname not found in test workbook"); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - self::assertNotNull($cfRange, "$cellAddress is not in a Conditional Format range"); + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php index c35c626e..2d4991a4 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/WizardFactoryTest.php @@ -24,9 +24,9 @@ class WizardFactoryTest extends TestCase /** * @dataProvider basicWizardFactoryProvider * - * @param class-string $expectedWizard + * @psalm-param class-string $expectedWizard */ - public function testBasicWizardFactory(string $ruleType, $expectedWizard): void + public function testBasicWizardFactory(string $ruleType, string $expectedWizard): void { $wizard = $this->wizardFactory->newRule($ruleType); self::assertInstanceOf($expectedWizard, $wizard); @@ -54,10 +54,7 @@ class WizardFactoryTest extends TestCase $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); - $worksheet = $spreadsheet->getSheetByName($sheetName); - if ($worksheet === null) { - self::markTestSkipped("{$sheetName} not found in test workbook"); - } + $worksheet = $spreadsheet->getSheetByNameOrThrow($sheetName); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php index 7de39328..8c4aa1b9 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php @@ -79,7 +79,7 @@ class VisibilityTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); foreach ($visibleSheets as $sheetName => $visibility) { - $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByNameOrThrow($sheetName); self::assertSame($visibility, $reloadedWorksheet->getSheetState()); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php index f9535714..eba7288f 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php @@ -77,19 +77,15 @@ class UnparsedDataCloneTest extends TestCase $reader1 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet1 = $reader1->load($resultFilename1); unlink($resultFilename1); - $sheet1c = $spreadsheet1->getSheetByName('Clone'); - self::assertNotNull($sheet1c); - $sheet1o = $spreadsheet1->getSheetByName('Original'); - self::assertNotNull($sheet1o); + $sheet1c = $spreadsheet1->getSheetByNameOrThrow('Clone'); + $sheet1o = $spreadsheet1->getSheetByNameOrThrow('Original'); $writer->save($resultFilename2); $reader2 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet2 = $reader2->load($resultFilename2); unlink($resultFilename2); - $sheet2c = $spreadsheet2->getSheetByName('Clone'); - self::assertNotNull($sheet2c); - $sheet2o = $spreadsheet2->getSheetByName('Original'); - self::assertNotNull($sheet2o); + $sheet2c = $spreadsheet2->getSheetByNameOrThrow('Clone'); + $sheet2o = $spreadsheet2->getSheetByNameOrThrow('Original'); self::assertEquals($spreadsheet1->getSheetCount(), $spreadsheet2->getSheetCount()); self::assertCount(1, $sheet1c->getDrawingCollection()); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php index 7e1ca967..ec2534fd 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php @@ -79,7 +79,7 @@ class VisibilityTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); foreach ($visibleSheets as $sheetName => $visibility) { - $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByNameOrThrow($sheetName); self::assertSame($visibility, $reloadedWorksheet->getSheetState()); } } diff --git a/tests/data/Calculation/Financial/IRR.php b/tests/data/Calculation/Financial/IRR.php index f6c24c13..5051182a 100644 --- a/tests/data/Calculation/Financial/IRR.php +++ b/tests/data/Calculation/Financial/IRR.php @@ -83,4 +83,5 @@ return [ -21000, ], ], + 'no arguments' => ['exception'], ]; From 6c1651e9955982186be45f578ce9c94cb61bfce4 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 14 Sep 2022 09:05:01 -0700 Subject: [PATCH 56/69] Floating-point Equality in Two Tests (#3064) * Floating-point Equality in Two Tests Merging a change today, Git reported failures that did not occur during "normal" unit testing. The merge still succeeded, but ... The problem was an error comparing float values for equal, and the inequality occurred beyond the 14th decimal digit. Change the tests in question, which incidentally were not part of the merged changed, to use assertEqualsWithDelta. * Egad - 112 More Precision-related Problems Spread across 9 test members. --- .../Functions/Engineering/ConvertUoMTest.php | 10 +++------- .../Calculation/Functions/Engineering/ErfCTest.php | 9 +-------- .../Functions/Engineering/ErfPreciseTest.php | 9 +-------- .../Calculation/Functions/Engineering/ErfTest.php | 9 +-------- .../Calculation/Functions/MathTrig/FactTest.php | 6 ++++-- .../Calculation/Functions/MathTrig/MUnitTest.php | 4 +++- .../Calculation/Functions/TextData/NumberValueTest.php | 6 ++++-- .../Cell/AdvancedValueBinderTest.php | 4 +++- tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php | 10 ++++++---- tests/PhpSpreadsheetTests/Shared/FontTest.php | 6 ++++-- .../Shared/Trend/LinearBestFitTest.php | 10 ++++++---- 11 files changed, 36 insertions(+), 47 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php index 9a448824..f15725cb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php @@ -4,15 +4,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ConvertUoMTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } + const UOM_PRECISION = 1E-12; public function testGetConversionGroups(): void { @@ -52,7 +48,7 @@ class ConvertUoMTest extends TestCase public function testCONVERTUOM($expectedResult, ...$args): void { $result = Engineering::CONVERTUOM(...$args); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::UOM_PRECISION); } public function providerCONVERTUOM(): array @@ -69,7 +65,7 @@ class ConvertUoMTest extends TestCase $formula = "=CONVERT({$value}, {$fromUoM}, {$toUoM})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::UOM_PRECISION); } public function providerConvertUoMArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php index f0a721c7..88e808ab 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfCTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERFC * @@ -25,7 +19,6 @@ class ErfCTest extends TestCase public function testERFC($expectedResult, $lower): void { $result = Engineering::ERFC($lower); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -43,7 +36,7 @@ class ErfCTest extends TestCase $formula = "=ERFC({$lower})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfCArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php index dc3ee84c..8a069ff1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfPreciseTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERFPRECISE * @@ -25,7 +19,6 @@ class ErfPreciseTest extends TestCase public function testERFPRECISE($expectedResult, $limit): void { $result = Engineering::ERFPRECISE($limit); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -43,7 +36,7 @@ class ErfPreciseTest extends TestCase $formula = "=ERF.PRECISE({$limit})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfPreciseArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php index 4d13d47d..26e0381c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERF * @@ -26,7 +20,6 @@ class ErfTest extends TestCase public function testERF($expectedResult, $lower, $upper = null): void { $result = Engineering::ERF($lower, $upper); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -44,7 +37,7 @@ class ErfTest extends TestCase $formula = "=ERF({$lower}, {$upper})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php index 328773e0..6bdfa570 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class FactTest extends AllSetupTeardown { + const FACT_PRECISION = 1E-12; + /** * @dataProvider providerFACT * @@ -53,7 +55,7 @@ class FactTest extends AllSetupTeardown $sheet->getCell('B1')->setValue('=FACT(A1)'); } $result = $sheet->getCell('B1')->getCalculatedValue(); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FACT_PRECISION); } public function providerFACTGnumeric(): array @@ -70,7 +72,7 @@ class FactTest extends AllSetupTeardown $formula = "=FACT({$array})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::FACT_PRECISION); } public function providerFactArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php index 96ebc45b..9ac68ee5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\MatrixFunctions; class MUnitTest extends AllSetupTeardown { + const MU_PRECISION = 1.0E-12; + public function testMUNIT(): void { $identity = MatrixFunctions::identity(3); @@ -15,7 +17,7 @@ class MUnitTest extends AllSetupTeardown self::assertEquals($startArray, $resultArray); $inverseArray = MatrixFunctions::inverse($startArray); $resultArray = MatrixFunctions::multiply($startArray, $inverseArray); - self::assertEquals($identity, $resultArray); + self::assertEqualsWithDelta($identity, $resultArray, self::MU_PRECISION); self::assertEquals('#VALUE!', MatrixFunctions::identity(0)); self::assertEquals('#VALUE!', MatrixFunctions::identity(-1)); self::assertEquals('#VALUE!', MatrixFunctions::identity('X')); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php index a100695f..1a909932 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class NumberValueTest extends AllSetupTeardown { + const NV_PRECISION = 1.0E-8; + /** * @dataProvider providerNUMBERVALUE * @@ -34,7 +36,7 @@ class NumberValueTest extends AllSetupTeardown $sheet->getCell('B1')->setValue('=NUMBERVALUE(A1, A2, A3)'); } $result = $sheet->getCell('B1')->getCalculatedValue(); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::NV_PRECISION); } public function providerNUMBERVALUE(): array @@ -51,7 +53,7 @@ class NumberValueTest extends AllSetupTeardown $formula = "=NumberValue({$argument1}, {$argument2}, {$argument3})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::NV_PRECISION); } public function providerNumberValueArray(): array diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index 34ff2121..54f3e0cd 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -11,6 +11,8 @@ use PHPUnit\Framework\TestCase; class AdvancedValueBinderTest extends TestCase { + const AVB_PRECISION = 1.0E-8; + /** * @var string */ @@ -161,7 +163,7 @@ class AdvancedValueBinderTest extends TestCase $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->getCell('A1')->setValue($value); - self::assertEquals($valueBinded, $sheet->getCell('A1')->getValue()); + self::assertEqualsWithDelta($valueBinded, $sheet->getCell('A1')->getValue(), self::AVB_PRECISION); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php index e1271b9a..6b0ebf67 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php @@ -15,6 +15,8 @@ use PHPUnit\Framework\TestCase; class XlsxTest extends TestCase { + const XLSX_PRECISION = 1.0E-8; + public function testLoadXlsxRowColumnAttributes(): void { $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; @@ -133,10 +135,10 @@ class XlsxTest extends TestCase $pageMargins = $worksheet->getPageMargins(); // Convert from inches to cm for testing - self::assertEquals(2.5, $pageMargins->getTop() * 2.54); - self::assertEquals(3.3, $pageMargins->getLeft() * 2.54); - self::assertEquals(3.3, $pageMargins->getRight() * 2.54); - self::assertEquals(1.3, $pageMargins->getHeader() * 2.54); + self::assertEqualsWithDelta(2.5, $pageMargins->getTop() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(3.3, $pageMargins->getLeft() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(3.3, $pageMargins->getRight() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(1.3, $pageMargins->getHeader() * 2.54, self::XLSX_PRECISION); self::assertEquals(PageSetup::PAPERSIZE_A4, $worksheet->getPageSetup()->getPaperSize()); self::assertEquals(['A10', 'A20', 'A30', 'A40', 'A50'], array_keys($worksheet->getBreaks())); diff --git a/tests/PhpSpreadsheetTests/Shared/FontTest.php b/tests/PhpSpreadsheetTests/Shared/FontTest.php index c733f8ee..2d5feb6e 100644 --- a/tests/PhpSpreadsheetTests/Shared/FontTest.php +++ b/tests/PhpSpreadsheetTests/Shared/FontTest.php @@ -8,6 +8,8 @@ use PHPUnit\Framework\TestCase; class FontTest extends TestCase { + const FONT_PRECISION = 1.0E-12; + public function testGetAutoSizeMethod(): void { $expectedResult = Font::AUTOSIZE_METHOD_APPROX; @@ -63,7 +65,7 @@ class FontTest extends TestCase public function testInchSizeToPixels($expectedResult, $size): void { $result = Font::inchSizeToPixels($size); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FONT_PRECISION); } public function providerInchSizeToPixels(): array @@ -80,7 +82,7 @@ class FontTest extends TestCase public function testCentimeterSizeToPixels($expectedResult, $size): void { $result = Font::centimeterSizeToPixels($size); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FONT_PRECISION); } public function providerCentimeterSizeToPixels(): array diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php index 9ada87a5..34321227 100644 --- a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php +++ b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php @@ -7,6 +7,8 @@ use PHPUnit\Framework\TestCase; class LinearBestFitTest extends TestCase { + const LBF_PRECISION = 1.0E-8; + /** * @dataProvider providerLinearBestFit * @@ -27,13 +29,13 @@ class LinearBestFitTest extends TestCase ): void { $bestFit = new LinearBestFit($yValues, $xValues); $slope = $bestFit->getSlope(1); - self::assertEquals($expectedSlope[0], $slope); + self::assertEqualsWithDelta($expectedSlope[0], $slope, self::LBF_PRECISION); $slope = $bestFit->getSlope(); - self::assertEquals($expectedSlope[1], $slope); + self::assertEqualsWithDelta($expectedSlope[1], $slope, self::LBF_PRECISION); $intersect = $bestFit->getIntersect(1); - self::assertEquals($expectedIntersect[0], $intersect); + self::assertEqualsWithDelta($expectedIntersect[0], $intersect, self::LBF_PRECISION); $intersect = $bestFit->getIntersect(); - self::assertEquals($expectedIntersect[1], $intersect); + self::assertEqualsWithDelta($expectedIntersect[1], $intersect, self::LBF_PRECISION); $equation = $bestFit->getEquation(2); self::assertEquals($expectedEquation, $equation); From 8513c6418c61b41eebda73c4b9faaad85dd273bf Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 14 Sep 2022 10:12:53 -0700 Subject: [PATCH 57/69] Memory Leak in Sample35 (#3062) * Memory Leak in Sample35 All but 6 chart samples can be rendered by Sample35. Of those 6, 3 of the problems are because the script runs out of memory processing them. Adopting a suggestion from @MAKS-dev in issue #2092, adding a call to gc_collect_cycles after the charts from each spreadsheet is rendered appears to make it possible to include those 3 spreadsheets in Sample35 after all. Also take advantage of this opportunity to correct a number (hopefully all) of Scrutinizer problems with JpgraphRendererBases. * Minor Fix Problem running 8.1 unit tests. * Resolve Problems with Pie 3D Charts Minor fix, leaving only one spreadsheet unusable in Sample35. The reasons for its unusability are now documented in the code. * Mitoteam Made Changes Discussing this problem with them, they decided they should make a change for Pie3D rather than forcing us to use the workaround pushed earlier. Change to require mitoteam 10.2.3, revert workaround. --- composer.json | 2 +- composer.lock | 16 ++--- samples/Chart/35_Chart_render.php | 13 ++-- .../Chart/Renderer/JpGraphRendererBase.php | 61 +++++++------------ 4 files changed, 38 insertions(+), 54 deletions(-) diff --git a/composer.json b/composer.json index 6835b05b..a4b033cd 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "10.2.2", + "mitoteam/jpgraph": "10.2.3", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 4097420d..508733ee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8512207f173cb137bc2085b7fdf698bc", + "content-hash": "b5bdb9f96d18ce59557436521053fdd9", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1342,16 +1342,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.2.2", + "version": "10.2.3", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf" + "reference": "21121535537e05c32e7964327b80746462a6057d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", - "reference": "6d87fc342afaf8a9a898a3122b5f0f34fc82c4cf", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/21121535537e05c32e7964327b80746462a6057d", + "reference": "21121535537e05c32e7964327b80746462a6057d", "shasum": "" }, "require": { @@ -1375,16 +1375,16 @@ "name": "JpGraph team" } ], - "description": "Composer compatible version of JpGraph library with PHP 8.2 support", + "description": "JpGraph library composer package with PHP 8.2 support", "homepage": "https://github.com/mitoteam/jpgraph", "keywords": [ "jpgraph" ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.2.2" + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.3" }, - "time": "2022-09-09T08:16:10+00:00" + "time": "2022-09-14T04:02:09+00:00" }, { "name": "mpdf/mpdf", diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 891cd27c..f6dfeb46 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -25,12 +25,10 @@ if (count($inputFileNames) === 1) { $unresolvedErrors = []; } else { $unresolvedErrors = [ + // The following spreadsheet was created by 3rd party software, + // and doesn't include the data that usually accompanies a chart. + // That is good enough for Excel, but not for JpGraph. '32readwriteBubbleChart2.xlsx', - '32readwritePieChart3.xlsx', - '32readwritePieChart4.xlsx', - '32readwritePieChart3D1.xlsx', - '32readwritePieChartExploded1.xlsx', - '32readwritePieChartExploded3D1.xlsx', ]; } foreach ($inputFileNames as $inputFileName) { @@ -42,7 +40,9 @@ foreach ($inputFileNames as $inputFileName) { continue; } if (in_array($inputFileNameShort, $unresolvedErrors, true)) { - $helper->log('File ' . $inputFileNameShort . ' does not yet work with this script'); + $helper->log('*****'); + $helper->log('***** File ' . $inputFileNameShort . ' does not yet work with this script'); + $helper->log('*****'); continue; } @@ -92,6 +92,7 @@ foreach ($inputFileNames as $inputFileName) { $spreadsheet->disconnectWorksheets(); unset($spreadsheet); + gc_collect_cycles(); } $helper->log('Done rendering charts as images'); diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php index d31a55b3..cb9b544b 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -102,13 +102,11 @@ abstract class JpGraphRendererBase implements IRenderer return $seriesPlot; } - private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') + private function formatDataSetLabels($groupID, $datasetLabels, $rotation = '') { - $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); - if ($datasetLabelFormatCode !== null) { - // Retrieve any label formatting code - $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); - } + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? ''; + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); $testCurrentIndex = 0; foreach ($datasetLabels as $i => $datasetLabel) { @@ -273,7 +271,7 @@ abstract class JpGraphRendererBase implements IRenderer $this->renderTitle(); } - private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void + private function renderPlotLine($groupID, $filled = false, $combination = false): void { $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); @@ -281,7 +279,7 @@ abstract class JpGraphRendererBase implements IRenderer $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); $this->graph->xaxis->SetTickLabels($datasetLabels); } @@ -353,7 +351,7 @@ abstract class JpGraphRendererBase implements IRenderer $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation); // Rotate for bar rather than column chart if ($rotation == 'bar') { $datasetLabels = array_reverse($datasetLabels); @@ -430,11 +428,9 @@ abstract class JpGraphRendererBase implements IRenderer private function renderPlotScatter($groupID, $bubble): void { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { @@ -478,7 +474,6 @@ abstract class JpGraphRendererBase implements IRenderer $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { @@ -513,15 +508,11 @@ abstract class JpGraphRendererBase implements IRenderer private function renderPlotContour($groupID): void { - $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; $dataValues = []; // Loop through each data series in turn for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); $dataValues[$i] = $dataValuesX; @@ -565,7 +556,7 @@ abstract class JpGraphRendererBase implements IRenderer $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); $this->graph->xaxis->SetTickLabels($datasetLabels); } @@ -575,21 +566,21 @@ abstract class JpGraphRendererBase implements IRenderer $this->graph->Add($seriesPlot); } - private function renderAreaChart($groupCount, $dimensions = '2d'): void + private function renderAreaChart($groupCount): void { $this->renderCartesianPlotArea(); for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, true, false, $dimensions); + $this->renderPlotLine($i, true, false); } } - private function renderLineChart($groupCount, $dimensions = '2d'): void + private function renderLineChart($groupCount): void { $this->renderCartesianPlotArea(); for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, false, false, $dimensions); + $this->renderPlotLine($i, false, false); } } @@ -626,19 +617,17 @@ abstract class JpGraphRendererBase implements IRenderer $iLimit = ($multiplePlots) ? $groupCount : 1; for ($groupID = 0; $groupID < $iLimit; ++$groupID) { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); $datasetLabels = []; if ($groupID == 0) { $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); if ($labelCount > 0) { $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); } } $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; // For pie charts, we only display the first series: doughnut charts generally display all series $jLimit = ($multiplePlots) ? $seriesCount : 1; // Loop through each data series in turn @@ -669,7 +658,7 @@ abstract class JpGraphRendererBase implements IRenderer $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); } - if ($doughnut) { + if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) { $seriesPlot->SetMidColor('white'); } @@ -710,7 +699,7 @@ abstract class JpGraphRendererBase implements IRenderer } } - private function renderContourChart($groupCount, $dimensions): void + private function renderContourChart($groupCount): void { $this->renderCartesianPlotArea('intint'); @@ -719,7 +708,7 @@ abstract class JpGraphRendererBase implements IRenderer } } - private function renderCombinationChart($groupCount, $dimensions, $outputDestination) + private function renderCombinationChart($groupCount, $outputDestination) { $this->renderCartesianPlotArea(); @@ -728,10 +717,8 @@ abstract class JpGraphRendererBase implements IRenderer $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); switch ($chartType) { case 'area3DChart': - $dimensions = '3d'; - // no break case 'areaChart': - $this->renderPlotLine($i, true, true, $dimensions); + $this->renderPlotLine($i, true, true); break; case 'bar3DChart': @@ -742,10 +729,8 @@ abstract class JpGraphRendererBase implements IRenderer break; case 'line3DChart': - $dimensions = '3d'; - // no break case 'lineChart': - $this->renderPlotLine($i, false, true, $dimensions); + $this->renderPlotLine($i, false, true); break; case 'scatterChart': @@ -792,7 +777,7 @@ abstract class JpGraphRendererBase implements IRenderer return false; } else { - return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); + return $this->renderCombinationChart($groupCount, $outputDestination); } } @@ -801,7 +786,7 @@ abstract class JpGraphRendererBase implements IRenderer $dimensions = '3d'; // no break case 'areaChart': - $this->renderAreaChart($groupCount, $dimensions); + $this->renderAreaChart($groupCount); break; case 'bar3DChart': @@ -815,7 +800,7 @@ abstract class JpGraphRendererBase implements IRenderer $dimensions = '3d'; // no break case 'lineChart': - $this->renderLineChart($groupCount, $dimensions); + $this->renderLineChart($groupCount); break; case 'pie3DChart': @@ -845,10 +830,8 @@ abstract class JpGraphRendererBase implements IRenderer break; case 'surface3DChart': - $dimensions = '3d'; - // no break case 'surfaceChart': - $this->renderContourChart($groupCount, $dimensions); + $this->renderContourChart($groupCount); break; case 'stockChart': From 8ecf69a5c4469111ed5adb58421e442f5eadfdbe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 15 Sep 2022 21:07:31 +0200 Subject: [PATCH 58/69] Handle additional merge options like those provide in OpenOffice or LibreOffice to hide cell values in a merge range rather than empty them, or to merge the values as well as the cells This includes reading hidden values in merge ranges, so that Unmerging can restore their visibility --- CHANGELOG.md | 6 +- .../images/12-01-MergeCells-Options-2.png | Bin 0 -> 3313 bytes .../images/12-01-MergeCells-Options-3.png | Bin 0 -> 5029 bytes .../images/12-01-MergeCells-Options.png | Bin 0 -> 22477 bytes docs/topics/recipes.md | 60 ++++++- src/PhpSpreadsheet/Reader/Gnumeric.php | 2 +- src/PhpSpreadsheet/Reader/Ods.php | 3 +- src/PhpSpreadsheet/Reader/Xls.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 3 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 76 ++++++-- .../Worksheet/MergeBehaviourTest.php | 166 ++++++++++++++++++ 12 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 docs/topics/images/12-01-MergeCells-Options-2.png create mode 100644 docs/topics/images/12-01-MergeCells-Options-3.png create mode 100644 docs/topics/images/12-01-MergeCells-Options.png create mode 100644 tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc17d852..f15e9d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed -- Nothing +- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) + - Merge methods now allow an additional `$behaviour` argument. Permitted values are: + - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) + - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell ### Deprecated diff --git a/docs/topics/images/12-01-MergeCells-Options-2.png b/docs/topics/images/12-01-MergeCells-Options-2.png new file mode 100644 index 0000000000000000000000000000000000000000..5e745fc934dc88c114b39dc1a45a48ddd88ad0aa GIT binary patch literal 3313 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 literal 0 HcmV?d00001 diff --git a/docs/topics/images/12-01-MergeCells-Options-3.png b/docs/topics/images/12-01-MergeCells-Options-3.png new file mode 100644 index 0000000000000000000000000000000000000000..30ad346eac43344b9200a9c49928209a71663e35 GIT binary patch literal 5029 zcmZWt2Qb{~?@06>HI+ll6#I0FEH7No7N zY8+s*F&E-V_bY^KOX+H_dH7XzY333VDWf)}Q?|Hlno&75y_g$Fe&Qf`X${iZ#N~39 zkFMLSYkBGF9gV$1;(izJC!wq`3rT!nygl0lv+RfBi>sYQ@08kTbdwYB20O03EMNPe{0fZ}fa9l6~ z?-_)Y^87=0Jc6%reVI}w+=gEfga+b+H3HLR3llCI6&01Pp5AtgD?L5EudgpIF=9+2 z(r_HijaZ3txz`c6zt;JuBfeFSm6i2$tB<;v z=1^w%<6i1e&!x%oG*k+*p`oGdm&kmUArn@JE)Zmp70)QP!$*5k^Qvp!ziWBOw{uEF zX+B74#4M-50i9V{QDJE{JCc9Sc%TL=?K~~$?1286Jsig292ITGHdM=1@bB5_UdGQ{l+X=(QM}&`+x$x*1fP7Hs!eYtw0aX+T<9dbZVrQ6sa3!zmMm z6u< zL{w_nQCe^icNyt_vTV@Y!wTa)6zgAbQTLs7=ZYb~c@0K%7>{`;x(iW0mB3cxNyhCu zfn?2)``|1Oh9u$p++Z0gU+b(5!t$DVA2k_inAGusm`CvbUTy7M6V1)tmCvUuvKJ>f zjI77^whv}}>(hNV@R@Ehu$oo{~(p;&0sb04@J6G!z z@C}o$gR~&rpOUFMT)Ki$gDI8ZV!s0oadj%qnr;v{maOavKlzACTZZEL*;f))#q&Mv z`T032MP({^A7#h}GwC`Sqi7gwInGb(yx(enYh%;A?h=R)j_IA19BY(Pe3W91jD&8g zMkWbK*F^?{{+P(BA3@gp9{1wG%)DOCy@Y%9P&Z=6r9Rbh<9Da~?P@XKmD;6J&IUy=HR;^V3-A062A><5-vhgXJb$QI zS>@dKAC$CEo7(jo50ws%KkIHSO_dgz?L6E*TnO9zlp%}?L9^WtR%dM*IZ>1Ok@H5? z&-a{j4m@zik!mq_9hLp!t4lFCj7+7!kcM*NX@+4BhM7EozFl}1_J+UdaS@W8lR+YP zf={?=S?#Uf*nU~=^;9Fn+o(r2HDiXBXAObQOHZ{EFnOdYnw<4P#Ge_cPv$EnO;%~l`EbXW?TNrTON9#Z8e+D%nc^oRIxJ^LIVtO~)wss?Oa4~f7 z^|#*{L?0ea8o|RhnwL@5nh6UR#T1lrb04#RQjM{cS;2}#IG!++Jhj|Wca=!V!_#G$ zzt+;Jti!A6lvhvA>s5B#ev4bv&wkB=NFPb=4YF`6eiX0WoIs&%w}TUkbyC2v_)v@P zci75`_>bf%$Zo`{?ZFG${4Mx$>Q4rKe6B4xKU!@p2;K?1=K?+ZwFM^}EyEWIt+}N> zqS3WBU7uK2g1-ViZC7yt7nRW_S$}log>JRPq*`YruH$IAyCSHB=ozZN2K0*~jLqD% z%5NGaqg6uV|6FMpltZLOv#APK4KJm=iojM#TXCS%@9OIdeD(bxde*YCV9tA z?`_M;^Ne_#3oIx`O^%tpojqxRsQ|P!<}4FBHL11L{P?R)$o{wPkkPFc`6?(*yJ+qA z@0^cI^;+%o7u-q0yc#|P2`3}18W|01PZ9?m1II6>(CHwP4hmr&`@z8R*<>C=ia6Gf z;s_UR^d}AaXMtdy=o0AFiS}~$+BZ1}pLCnPTcx>QNrF|fH^(uqn`q zJk*LFU~z5w>oNY_Qx)Oq!O$n)v&B2Mbv2PiQ>Jbi@2&-9*xFywukpvs3yKIe!XVX6 ziCA2_XgW!%oYD5trWky1_D*dZ!ZA10scZYgPj@{VkFNbb0)Zex@Vz@Erx@^H z!g|N>-oUW8Ops3x9H$~9Z+HxgE}X)t1cQ@|#@UcT_A3W73G_mq-hP_zl9idT0}vJ) zyYU&FnY{0stY3cO3#)6SkYX=Ti)?+`u2f)1$V*qA4;Z4dtcBXq9^M%#dooGwIXi8C zm0R??L6%zn7<`9;DXHdaK%$R^_uATuWp?ta?OpPbQ=tr$wtHHhq_fn0038odH^X3a;3I*$qk;T;ofD`8~0O`qa1axO4- zW535;>M?4@EJu&?4m-rHLQZBeBM*2S(zfA=x~aj(>-M1mBh}?c3n-~0fkRWJPS=4j z>z@7Hh=qoI`AdyvR|>>Y!UJCZs6gu@pWhcSE4C1Y|kT5$Ql;z*XZ zUET+XXBA3K;##}W>LAHTvE->;uEUIP<=W9M-Tp-}`q`t`ut!3>m)bfXc5&`5j}_j$ z*i_!LzR>QwbJ3}mv&P`c8M4)NsywcbhKBt-ArW55dWdRe71K0xqZBg7f?Qp5vWP{{>J0%4=X39vhNxW zk9Se)EBGDb-OQOP7i`qVGlBLmy%h-Z>7~loFI!&>21SGQ$pH80F(Sa~Z`u+U7gt{| zF)w(vouOUS1JET)SRKB`!^4xJZFQQCI-U|tkIcBgMj@w2}$wq-6>*L zk@+`(ZxqOX-@ge0Y=xu#9yOXxxBXt~%p8kGKCUTJP>1lNbGvDqJF4_O)OraJi`>Hgn$Td8K`K5WzPV~*>=S?~O^t83y$E(ygDl#)} zML|gqVSz2bk%prRML77lbqEAHE$2LV>qIj0+Ip?Er5@@rqN)XUo9EMy ztLLMd(I#9l*uZSw_5nyci<~3z8O259xqRGtGc7-^dxDiD$CNzp9;)T-#kLxcDBQc=)5gs(5o(fSBY`~sV?Im!4(9&=8}j9!Oga)IO^ zw7$htt^Kg-7D$L@*YrVelZvNi>${N4i?4Y#eLjVWz2T_JEg)8U9QBHW6EkG6I%~Po znB}kXD7?qzCtOUI+GeDT-MNkz)zdNznjvS;>({c`63nUAog$Bo=j!QKX^=Tp=wBJ( zF1QwkmXko%&7>GHWFl2n2B>yiJ9vVUc{8AbK=DPl8~U0q$Js|(MF0jK!2j*lrl%R{e+ zaqC~|9nb;96bQoE@#h@bT~a#oS{sSDuzdp?qVVef!UReDQ`gl?o_rkcRKT0{CGJ|@ zP=s>TG`5cwDB)WXc(&|Dt1bX1;_J1?KMZv>()(P>!@0$;f{7Ib``qJ5BHhxSqj3;I z1Rl79npzx=dVL-rQ-r53Ux!$EeMG|?g%EyzRMh_KKduAeS8TbM{}ULdkb(8=vd41uDD|t*22Rue<_tGWbOQuoq+aXoaPU%>-KrH_ra^eX557PgF-4^J(X&%)4IXWKQ@!%_BFn@M)oWn@RaNCpko1@E%|*05M89 z6ZLK#$og*trL*=KrNk(%Nwg=Rhf~_u3vB|`Yb6>ITOuBA?qKKrcU)}QLsK+Ea@|Xo z7%`;NoT*Qx;^rItjOz)^tN9)?CWVvc6nQfxP98rRej-zzK4n(H_&z~Wk-mLqRS@R@@$Q27 zo%DVbkkO~}Qq?%U%V%Nwn|+y(Lg&H|ovduNn09`fMGXoM7lTbWovid7_2h%BW@mHNVJxE(IJMniZNHc<5Wt=6e2 zorTi?^zC6vbXyK0kB+Z5Cvg3C`-1|OCNcg%RvK!xRVpD=_RsD3;&;aOGhgNT5?`Yz|`@RQ<{gB&lOUL^jmtY4N@n$9m~Yr44(WHsY0aB zznq#Y9MbR;b!n$dQ+O7waVmY{Xx!mAAS7LQA$e*+#qRa{O~7q0HtQmHzH|_1Whtd= zvCZB9lr30=pyMkgNwnOo8(?#>a8&G6k4ek0jR!%qB;3><2*|RxVWOY>klEIvkg20*oiR1Qiy6*cb`^E>RB*DsE2u0qkQ$H7me7`WK>DYI|+Fp1%vSS#M?(@07 zGCD(MAV2oH0G3&}o$np~MKUH1=P0l0!1u)cC2t`Y=u49q>D%qP#k8nwCvL>+t2D9C zE8M1D9$ULD;RvO3dyN}H_Zyl!B%g4uVC_x}as3V3^BlWDGj#Rx$vihE7adc@T-63o zFy0xBs@|p0dUT++negaK>hu@AYUr-?F4y9@neB)Xys2(>rsP0RydT?e;$)V_!LxHg z?|kaH+?TYGo*=^KHbZb`mhotAlSKavX8-($tdZD+{WpZb( z{3(8r8AVJM)w;mI5n?PCVBpz)w+>zL#hnOhMgf?bi#9l|_pc{{kG`xq2VAl* z>rk1q|33wgpv%uZyMkYWY@y1p(SilkGbs<1h^;k?G*g(xpH*Ake{r6X*4Gzj$rW)T zTg}mGf7SjUT7H1UHkR?*VIO23>s%Ijs-i}+Igk!ntL8y2*nEy|4uDHc7}>b2j(*i| ze-2Gmbq=5exC&gEuokU;BJeA~U|HMPG1pD}^y0;=FX|eQoYXuf-v>$Mq$RkT3pWG6 zP9pnRwX;s}VlvXG_{0mGL8>U^Mx+;f_b#poiZl|+(4CS)m(;$fD9lZDJ>vU5<_=LBc*iT z7w^yK-tYRx`rY6CV-|}S>%7j{`<%1)ex7F!Q5tIUxL6>p2M-?LDk{ioK6vo36ZjZn zq5)6(h{)1`9}nF$<)t4~3{h+Yzo6SlsY*R~@FgDm+8hJ;{i%zBzT1Nb&)n}n4;QVe zy&pW#wNR9m((*RhYxSwsn#evg9yS<08m?KiIWZV+&rWl<+WPEuVz9mKPZayb!-F89 zOktF%0MZ>7`6$FhwdVmy#_L)4@&hACBUU7@kX!mb2|8ip+aSO66e%+FC$C&yee2m5 zTWSq#uNjuO%6jclx|7wU?Gcz;_dH9aD`#jiceZ(`e75m5RZMEzwM{-%$Dnb0fWQ5p zWj4DPO+lE1U_n8*m)PQI?;(r|kvQ|Rp#&pHnsX2dx)g!ZD=-^oum))XREi+9dW#Ak zMXs^}hq-s}oPKrf zqe?OP3!oZsrg9TFWFj91emDYK@)P)jJu;uCWp~i`0)-@6G%W=kG#WQa4S2J;nqheJ zLnh8QCB0gy#eiAr?8aLH4a=SGzH_2r=0@<~(;sal6v@$$kf2XmFVNC(H$&|(>kz0S z>|M7~daV*j?$v_)9f@_{yUkiQOnTjBzfzia z4JV%~s?;OG@$@?yT^9)Llo9?Qc0P)7Nw~<%$~5k~*UhYM1Gih#UVCn(v|0#EJ%p&? z87e0~d*=-;m%BVt3GVB7uzSE1hNrFlqVdw|gTzbpXnxG};K*RrK zGbz}h)dZ;qR}-m!J(X%2G(lzuK{yMXM!A&X4zS>s>LcrVoZ@y64t;v>#fN0-HGdx@5&Grp%qVSUP`UZ)BQR3oQt6-u{!eCO)Jf0I5!WD0%~ zE%V1N1k@c(_~&k7=Nqy-r1gN;@7>hO17jvd{p=Wp1S<@sC+upi^gf1}nf^$K21dnV zTYJAKMvXX@Fq^p0@lv zSq9}+Uk?l>vMOwgQjMF`3RE*TRSXde7*|}5)7i9oS@US8lG0-6=fXnRx6R|vhkwJoO&d?&1PuB z?cz)_i*S}2Z!$fZv>cbBG<$je>)Wm!NvSx=EV6%|!~ATT$vr(mYkydO^IU+~PFZ946uy8?jL3?i zQ#qJYL3Hyq^nw;)HxF~!RYMb!`k!`KLrL;+;7)RSjlaF)@wsjfGXry*H@Zh|T`;qR zw`ce#^QoR&-^_gw9)G;3r9&m?$ie$lz{bE?>w@2aOfO=)gX+XA#6XDmjh6v&5mY(V z9Nu(VxQwYKP6d^+2Rtc#vOR1YW*hd$Ag<?_Bo%epSiAdD?}2Tbd;)&}<6!{L{%k_bGKeJ28tgsFD@`+Y4@lyo{g|0(tBy z(*DRVgkA|NDxTsj?(XBOT@6wHz*mMnJo`CsY)JSM)gMMlt*ZWB!{&1Orrc4VEEz0AK^S{@tFo|MfFr%fa6z((Lu52DwX-C#ngji z=?^q$IeYv2$RMb^V8rzVKdF^m|K6aLj*QPjyS*u9AMB4>%#Jwc zqc)n}*xe?S((T1iJssNQjP%Xa|6-9OF}8ToB0M$;+o@t=$MDnYqME3Z=ii0m`e|7X zfTJKXXhq9_LHR3E$ftr_%FP6v3hjK2lKmE}&uK_^=+{T5R5572(I5KbdNdTZepK6qW+n6ETF@uU@0lmt?64+I}BA++?RB&EfDtLH{ZNz(-Jwn(> zzIo72JJl3uy?yp6HkdJTz^>877o^LD)hVEgPij+Ir79k)u3wa+GK0uB zXY~WaMup(vQdJIHNT=c$^42S&O;1vkl1|=uOK*q>Zdf!DLe|ZF zUY~<}uI&j+&o{tW)(g0VR2@)Enx`}Shfrz~LQVV86zZmfIppQ5fm;|yk5)0W^Y|cp0gc%?Xm+(c2fg6~ zZ!g?;0k1_6v*Ai}B|z`l{9Q~CgG}ecHR@5q621KGfYMQY`LZ`f;c;9l%_Z5aX)&;1(w=o zqg`tvCWsVW=@lYHR}`w$8I_O+OMa!8`q-vH3z?=LV)r=>)8Tvw!>9eReznz8XOAaF zu2Ns4+3^HREPC%qhs5)>zEXcgKf6P(dM&0P#EpL~Lr2G&nAgcv_r^{IR0ee%dSH-) zQ>5zRX%Swa)$TqwVu9pYa352xmr-3~l8);Qp;JL)%s}tgi`F2+mKJAzs-dDqV2$$%9R0_IPE|w=CcMrP+fZta3zr7Srv4f%5V_O&-5Z{7qd}s*$l{hQVQRUtsnul^=kMPP(ZpSZQmqG^$(s654Tkt`&z z$%{Z$$tWU#q=^av!BQ+_;sl)+W1&3H<$ys~?_qbdg7g7yU7E_0#kmmLysq3z>{Qb>C{1CGE063;xJ;;fK|AlOXLwJ z0COq7DOa}NicrXnhCEh8&^TzCA_@)vetIM)TO}1wt-CrRjfb(OVMd_@YW?$r{A_P- z3We%uBt@4(GI7@hZf1M+7Fu;GB#*ua%cEhq+r4hQx9QHVHaND zD=;jBy16=&LG&`>xqifuA}DoZhMgDK7bFtY;+K|DYUW!`(pnoevvN(U~rnDw@y z7EfiSh(OC2Adg6b*s?^svIrB8`M_$+%lGCR-Ll>QjU)NZ{G+NXyDJUcl>t`4+5JL`@ z@(azEVP|NoFW;KQ4y=S%7g6AMy4!er`CGTbs4pF05|9$hEHK=+^vM|yS-)f2BtiNL zl@L}x6}EWu!V9SCA#mVioNA@kgY=0d7;9MsIZTy+7ON-yQV|O%l2AqIG5wqoP@u->&raM`Ss~qucLZ@&jI>HS1E$gP;`Sh87UDEKOV0AxGNHcy#MTJjBd?2 zx8KB=Ueo169*Jn;X(s)>eNzAVz3{UH!!7VhlLijyqw2KOE>{m`{fu zHcEn?uR!7P(Q*`rU*zoV5g)>hc3g_DW{O4ezm~K+=w8c zLgO%EJPa4)h1|2%5lwKZ$;FT>PNCcn@6=GnB2}gN`6QZfZXFz&G~tjL$IYds`d^Xi z3Iw4A!UrwR>x7JWH|wSWA*KwE;(|wd3pKLJp;dFlLe58E$GHrFwGjm0)fI|KH0VyZtl(xrXen*17Z9= zX=x+oAGt{!uDq>sgJ)~GIyZ_&z z2H73+oC}p!xA(Ktv$me{r2{kKp1{QH&mLdJF+?zEyuz#Fk46q2F^(c#S!~^N&u)sU zAc-wNIQ4i|0-eSOg_0;HRWTB?-Ls&P|A_=eMnY^#mMAH~ukWp-)cgq9=B9~2`?&SR zvQ6gXAc+M&^y>OElz}Wj#Tjfz(PTfx33H#Rev_pStYGJUH|hAX$fRQv7((@%AX7^- z%v0R3gnKWEFj~S(ms;B$h9cbVeK5|RZl|TX$(0#~`A__NNoMDMW1Kc58MO7!pLtmS zwRf(6bHH-M4%V0(Z-RV?=kRc{ zqZrjIe_N^w7Z6;#)M`;`W`s(yd>G|d!oy(D2&UM99Y2D*B7IcwcYBUqkT?7z#?79I z5L2IZk|BpQfIe*mnA+>oCPFw95aY**AprIumIoDWoT49tj}Eygp`?&@#^Bbe%ddH~`(0?FB+1E0>{Wg{&3KL&-?GE1o`nF`b9kYK zlQ7gRWA9II6Wh71;yWH}oytryqV{B@uPt-A9sQ{ZrgVeGZ_xU!GkK84!tgRta|l^5 z6cP-ZzQ;(F43>s&HT4T}|9t%ueS-WaPAj;nmPq^JxV7XNZ}C%U4~-*`w)ZN3Aj|Wd z-7E3eCwitt9eg(?J8`~w@6AG^k=b9K&u$yc?O$BH|9}SKl~zodTV}j0D*a-H%R&*8 z?wBdt?M~mouw})B9C%d_DQsqV$B7NRHWac7T!r=D!H1fj85YlL9@VB%{yATbce6f1 zT;@;l?MWz}FG$P}-|*c8sC~h36?G9JA@Ix3u4u-+<_KHn$!1~;w`%m@!+7@3+cJkGasF!umSL|8zz!h7{H5sm#9;w#Q9}6AQ7WP#O_R8KH;D zt-*+M>g5qnK061GTKe*Vit@Kk_vR{FPiH$3(2$*I-(8}8=KwC%$>9%7*EcBH)17JT zJ$IRBC`(Sh=k+63`r`Yr@pxKArv=&8)QcUqPq8#mp4Bozqeni^x@m7l+1hBd2wjM^ z9v7ovDO8hVFk(<4isk)FdntsqUCE`DhGM|5%H(htPiT}F^QpAxLV_&#;~TMH>8J@< z(tJ~tFIU&pY?KIukAaOT`=bit;#qf}!oWKxAe70$?TK5*|HkbUnVh}mlWJ#g%B^JF z+El|q1tx-odIG)^y8^zdTbMVqUfPkCN7R?oq<&TOFPIleJ?9FUn)Q&2x;(K=&P&4p z+ritg8KSYTPQT=4VhleIbt;;aN0f?QoG+;4Up7WetUo(-du5y*^}AMw4T6>U&vvst zqKcE@XH#B;7LpJ?dd=N1#2A#ygyrdNMB$h0BZWos4i2ZE*3yl^@AyjCPht-xiLD78R z7r%&Mxs}b9xOF*9>ySkW;p+n8*LogmM`H;W_?d`863A)7{}$u^(#VhQ(?DEK9sCn$ zg#RV?u*e~-)F0iawTH%y`)XIhoOk=;w!~41kS>5K1ma6F*msbBs^$9cz`uhVs5>$N z9L)m=JmK+HntFucp)X@5St_3RGL!~5g9W`m{*w$4yr7%qEu2Vq8nOHMnm>e0ftZpn z{1cJH>Hdk-)hq^xLN)TY_o1X#W-8wHMa|HagL~e|=9xkiI6AZosNcPOmeBWhcZuOZ z5?=oX)gb#3RtVMO{0#NgGi`yC7wtSzldrulG`SCS;y^MY0}8D`@INzh5(0y0Qo?}c z03}oO2nz+M7@B2n52QjGI$0ZVG*|(lFbbq^4=`4Qr-ug+!hE+Ghdu-O;pCxwha~|} z9)iMtn~9CGyeH8|v;-StntUJXH0i^;zmH$LcOR5n$;ZqXmj%gmSjLP0_}^)%I5)s| zr88_;?C2+;)hT1oJv5jYGMQF`0MQS$cVE~tX)<_KiK_fm4*mWf%FND(6{`t=y`MII-#HJu>?v(ZP#fXj zX7|_Y8r?CiO1%49<_X$q0(?K{^w)XhqpVUq*TZ_ap=4wCIgAzIR zbG@QOzRP}{7E$6k^yj_WyIzZruBw?@Zq=9F5auk`_(%*@WNtH>cx+j1MU*b?SIno3 z$p}>|>NXy@I;roBx9JsgcM`QSr`KQ9K=bM4MQBC8?jrKn zdFbpbq2bbDI;9g=qpGUJ^ajOz-y<~nM92zwltmUusKqK(bn}>s01a?r>3c@jswVzv zSPjt?fp25_BgF080beV(lyY+~#-!9oe5$1M%^muQBJNa4Q&@oL^7Pib<{P;-!NNbBB| zveV2gbl#F7#dk=gZ%t(D2Oh%Nvy=wx@fJUA_m5SM+6H-5Lxa^%jY!CSR9u5Y3$Qk3 zxXfhDL;f)X8@Q>tFH*?CHVd$WslUGQ2FYy@o>AGjnN%oEr|asEKQmNFDPz&njPrm@ zM+Edrhw#>4Q?9iJz5v0d)Z!|$*kiN8d13}Hs&W?|NxA9Ax ztXvG+T1>O!C;!SmR9;=W7|6~isGfcBY=?Vj&~rcej8{=>WjE+hO>&sW_Mxcq7e(e~ zqk|`t>7*Cco8O5TQeu_@lNYB+~$)JDq^HqCv5XGJw*>2W2( z@)*A_agbk?e(kl4Az~0Hjf4f&^)yb>84(a<=88pa(7%SyCSTSeeAj*BefW*xPKzxU z^>a>^Zy-S{Y?>=I;@6W0t=JcS4sa1K#?s-d9}++iltH=%*tm&rY15z&=`G2iM?4ZOV4_RX&bdyI-OWuGlQn?F5B zo?URAef2Jf9-I7Uiz?0bXlVn%48W*&ymlIKC`a+T*i}N0H_uA!iD}M&w6gjGPA>88 z+}LXY;|q{=4!;l}rOG@t^e3;9ilBKrvt5odv+W((rHi;I$Du65eydyVF5v3wn$735 zOv6t{f3-bNcds7U7LRiA<0UdI3gTQd+vJu~14BQMdO3a%KogZIFFK@6$Xi5{UFZ)c zG?>G%lh|Mj^XrYHJ+;QDs#GFqWD=G!M29;qH1T7@|1N(e zcn2^$(zFVQDze(gjZ&_KP;dOxGMW1nTNne=a$S_6JfZxW^`W~@<_3eacx07T`aYEt z0I58Lw1z(GWY=jF=gft)263`mF_w0+yR31zg@}J^&G*RoR~eS(#TqcUsI&VscLB8H zf&5^uWMD@yhPvbpxZJJ*hN3f=cG?~-I_>oIkN~X@K?!9M9`9jV+$sDe4ed{{et~^u zD?4R@N~(>#9cv8lMs>t-u09b`*>-7%f~7}7Y5>^zVwpt&hb?4`Mvw)sAVhIR+*eOs zi5T8Q69Q16TT=&DU$eXzKOxsN%!!yKT)IhcC9lof!^1m>m||sG|9r6;Pk_wfXJ0O+ zs2O?xjp1uyCqJdW`!)2@>#qINkhA*Ed4#QTLu>8bUcAJnW#3kX0ZAzV3Ry6Xw=_BF z`KSANN4~y&ZCP1a;9VeujbJO1P!OL=40A5k=q*oa#IV9EFOPEzdykp@RevVuZf(qT zJNKg%^}D5>dRh`p_pRWDJ14v6^>>$vXCcifOjBvZ>zU+^sIi_fp`Pc-naXDzozXceA23tl%7=)0dA zN5tP*byS!S?=E^B3msr3R;Bewh#tj{iSS6zd+lQ155D{nlv@vDN9<%HqPQgY&$OYnHg?@h4;_`qNWr|K7c}AerS29YNtAqG0E~Oikqr%?=wHB zFV!@_2POBMZb4n*A{1~A0x$6II4h3AmFj_S<8RCZtfZDSd9|D$-U;td`Xn$P=G}8$MQ&z@))_O8&c=ruF*M3uY7QQWDQR5fsg^}5(3%y!`8%K|95%4Sa- zWeBm&&$OZDB*Oi}=R;b#k3LbgNwy{vjhNwY=DlG13{}~zG;DdoD)DvSfAIs(zYe?$ zF%AU8ub!@eIC#?Dt5pD*irda6$BBuC_$w*w@6OZ!w^jb_*@EE1xfVe(eJ^jIuy7Lc zzIZL#K#BZ<+M01QUAP?$VDRbzs>CG;5~)dG;|TQa-S@Uk8iU3rYJ5@1{OPr!Y*9hi zUyl@i?>Ce4Fs-*n_0@JcOS(I3U1ztOwB6~I`wL(>E|%OGxu1`UA3rISOSA>HMeYa4 zA8vD80L?5&3X!3HcNnh#8%K(^olD;+Y5)ObWn1wEgRjYCCLqNNgSA|T9RbxlTKw=N zVWi6D?A`4fSA-uO!hpms(Sg6o)8W9V5@j!okcw8?|ISBOA8zZZ%|63$-2C05WfX*im^l!XSFEpb4|R-WkS7U4mR3Zoq{ zNGDLxKprHQQ>@~X)rs-^`axW#*oFEr?*)v<{O!@^u*@~4;JnLC0&OaQ2IEy<2)TTV zCKDKA_*lI0_g(JdiD0}l z--_RBk=!}g*kN{AuZdEPut*3=pXl{$oxOtN%KPdg#W01m`I9NrO$PHPI9mVb^#e=L zCAWAS>MXX)M1z3G;DfhGRgWH*zB8e9;Z1$L^#^#%*0XtGHtF@`)wenyv7Xm-MnW(` zU|O`GK{OO{LD2%jq?Pb5D4l$a{i} z1)Fr8MsS=p+pV<@=HG8Gl%v|uW7-n}*O!{^bdlWR%7BJ*7OBXt&I-iwU@?HW2*HmV zdLb(*pp<;Mh%n?Sk>9O{3|jh*B9BxxV8@%1cRoA+P#DBP2s3Wv{n}2qb1A|g_`XX| z!XWwIlfb^m$V=~(7h=gd<2HpTT>)y4u-BZAS4E$3`!DB!8mH;nxC$@HUzWim$q8Si z*Zb5EmFAjDejIn!M`f12o4yM{D=<<}>w1)9m5oqr7P%hsnF2NuI;{tg>Y9K+7Gg~; zt^^8GIN16?&du)+e@t;1v)*XRabyZfC;myE^xJQkDZq7S{FC41##?{#xr;Za4tBdl zy|3`)rzBx%J)(51#iPF}vWsu2oE6-w)#$(Uxv}u=(h(!E$k?@V-tzZ9o~V*KiM^ zH+wXibFEeQ_wDw)%N;&7(90tSBiP>@XcUeXqm-*fz@T2uKPf(qQWJr(uU?ncYF3mR z!?8X*i3{#n=E1}@2wCFRtmXIpens~>5p-jQCuT=#Vb?Vc7R*zW{G<*=bba!u;;i() zm_z8acVU+tl$kFY2KN0aC7t$2k1RP+b3h-k=aFHt;fyciSS53BuaRgA)q zqskW77@h<_vZfG=^d6hoTAFXp;TPEze#`P_vB%WRDx*0-7%yAU6E}AcXjshx$m+QB zWu&}W3UOU7d2JwzT{z5Gf%&WMj4>Uj2e~upXYPvzF9SkIF-d?v|Wh~`W+&ytgSFziU*S)H7kc{TRd2J0*7eET}EC)N7@562_yOdyC4 z#r6EriJzV^y8BE7pGjP1;YOpjmaABcij$(hg zVGu}9mkeX*!z0mmmT+z06_8dNAM|`Ti2b1e2CtUl|E!MxmW3PCee&R5(n0S86;*P3 zcz8%5j!8Z-fGC9Dj{vJE?IhhmYTnHIb+d9B$nu*|N4V@z#)fF z#CqW-9Fm{BgWAVrH$A0W{ewvn-KXd<z=3k72HHKKh>-({~y z3%A##B~4yhhOu+hP9ur_kI+whj=xN6anKDvJvdH|QN*6sS2FmPHt=%1$LCa(s5iBB z+MD>E4N%my{ZdqQ+WaTQvzH*XKyIxriY_)9*RM4r6$M)lLS$rS4wiailX65FPE>&8 z5ux^)T%nX83b>tC*NN#bjlAD@D|=}-rBa572aM|@v6_$Q&k?cH5~Qh3s>@mD@+u|* z7Gqe}tfmjKO&90Ql*)g(f-|`M;@Cq9b!=UyX0Imd`x*nzlh-usgq zT*Wu7@Qf1e>CVenuf5Fmhq?K9K?R zbr__gOW9DuppFT@__OWD7m z>3j=hV&9P~2^KIG4`~TBkd~<&CByf4&39pu4Wu0j`TYQeH0xDC48gQ)wQd}0V8s9N zOnxeEzes|d5kB-)Mck&88BSOHpMfqOl^8cr~s%E1l1#qG_jC!D8bl+4PT`v0`=gtaH)b#{DMy zttvncm^B!yyTjw) zzTS8+hJyXxT5KHn)NZ&65(ne}M7%`stpIG~{m_u3h}oLZs$s})5ZfgR~+~=+I4#3sn38W zq-=3N9WWb1F&)Se^xpPn&(Y3fJ!5h!iinvLfUiGzUzDxBnM6Rfy1qx@J1p!BEk)o_ z2D^LhP7hULC(1=Ed7PiClkS2}fA0Is#~)P?JdN?$t>L>9DnF91sXq`s#W1lJ2f;sU z*XpSpakZV(9oa^~v=r3B0VyY77jKC9O>Yk`qiO)nPf#i z<%Zzd(a~U8=hhR_fb91wqStXf%3C%@OPBqbhJK4vLT`!LQTo_M!PeRtT2&BdW# z?Y%c!_YNR-h#yhZ`CoZ@;{ZC$1L6kfB?&{sA@Cig;C_2z`VscZ7PM`r#cR?7rB{=b zx5>$NdLl?4`KxB?BTiM@?$2oul!nb01xD07?tUe{!~zZAl<;pouH=>1yJ<|C#DHfdf^@BqbFBRYHzuayvfc_{7c(tVDDX zK^=gpdH{5k!7N=^WC?l%)y`0adAkg&CHP`Aw?{<2v>K-*8Z$#K=G!;XdU5Wf@xg}t{0V^KefBtt-7fJR`% zGSDjab9SHOw44A&eEP7miYBZ+H)}pbEuJYEp!xv&FV6tB8>cjVmw(HE)Z^VbJTANq zCNr*dknC>`PSGiwu}xw63)2N3$|{G*OqClx7jAkE$LhN;?^Q|w2s6eF3R{AKqxFR# zh!GkHqN)HkUc?LXzy8yc&>u?*mJ@ z&g&@_)inT%${h*j!)#r)$|~#EFG6L3`zR0eI4!Ou9kbJ+&q<%}(sv=>a zk~V*RaYP{5i7RL%Y1O-)YI12w^cat#%A)(NL?yw(#7gqi&OW~6-5Gxb%R6PsS!*R!uOk0BE#8m9d(8L{_l=j9%b4)a06Bm@nO?L~ zHbm?^&lzg>|3C#-BagIvEr%dM6x*ic^mc{3c^t?27|2x$h6p~M4GQoaC4A^b!f$`S zGQkp4ANxw9N<-a92)nHM80-xJ94q`-9$}|;0IS`LTlCe}IH*C9H00#|zSHxt{VyPx z1hrNBu+hZpbA`n}I<(3LNZfrZVRY-FK?V`|{MkaYuI=&loPh(Jj%Wnwqp6O~<2e5n zdM{)H-8@pJfW!|p*iA_x4z?>Nxx-8hNOoU+0MZ2wCco+%u&>MI8{wb7b?xA4l$ga8uay4j<$A9Pv~Xfz z@B#^p8 zwt*htE2q_k9QSqo+6X=vf;f(RiidFsqGOEMe_CPFV0@=U1y=e!lU~WqIKBnM9Ovde zfvoV|0HBXPvc9|=S1C;I0te```uYA{9v2W?=hpobpBB&q`wc7#3GQc~);|>Sq^ixK zT*laIVcf&2u<7(KcY6iiU)7%f&+U z3nmKxkKVnkN+U)?0^dNW;D7XKq*WIy(ar!uZN%*4*7=`R!l&$f(IHRpExz^xGCSDI zF6`*A@{_Y=^ZplEj^l}rlg0VhxxY=)illb$9k1V_q2bP_Y&i3JpBQojA|N@ z6mBEdG3emvX3XAwdor}LcLs>)x#EK4578p}OKQK`PXr_0F=b|(GOz{@0k zmGjp#KjZl*#yxrPh4L~+r5>^(K8?THxHG{5$vAeGz+>QXnirColc`gPiPXW|4OhTT zCI7Pj)Fea)Ike30>u|N8?Gq##+txoU+N$|c*qzQv?d!=k0v$u>C z)sC2X$#c%}^G_CTBRQUPhxlk)Ey__YOHKD7FN;$X$SM01W@EaR8e%wv3Gn}88D;{I z`TBoXgO~dOZL<8|K)CBm_jpT!fUUh2m#rYX7${qWCbik!U={&=|Bw|pu8nS+e|<<< z?Z=(YoT$I`!M;Psz;rIa2{_YSFVs+v6k~1~Q|DxQX=cMJ07%e=dL&zkTz+5vu%Ja) zyiF<`wNN(S^IB!yOQ7YHi|Mr|l6yfyej0x7;A*g48&3Z-52h2%eXOs z&LKsFVS^JRe6(^+3gO)H2Oq~|At9r z5m@d~m23+CiOrUQfV5ennn2pSbvguuaGeJz80mi|xfM(ir}6MvAnUAY7~c1AxdY-& zAHO*)(g#ct)eSj!lcU5T$nGuOWnM4I~y1AQ@|bcSiq1FD2$5 z`yWuGLe~Ezm)3r=2P8_M0j<*Vd8ls9*qZ(@-uH4FdF5G@ujgr!Cy=PX8G(w;S}dhq zT!3C0&i((Wm!bfXck8NmvOS{~41g(-^GlrFqh7w2~d8bXdXMayO{ zlxP3w)yl{HZ;>>w9Tx=7t8tr)4?vH%MxsuBPto5RI+1l$^5+IzvKIMmjYutOM{`BT$+{>wePL$%neDd`fNRFc( zLfBjmWpDt%mQD>sJxj>-Vm*ZCqg2uIJz#7pHcP4XC=(r{5OK#GUHD=tdM{W~228E2 zO6phHPxCdOQHl>WOEYk6d>J0PUacLX9-M7En11e^egXqm=v21+#TsV=(f^0cX^?_| zz5@B{-s$AP#-Ct(?+yD$rZfdO0=ehLk?xWgYeaxGu_D)fuZy{9_5@phuUF8wuq*tv zpc3}E9N(^~&8~ycm*fCO5CR%GxY8pAVrXEHumuPmos~oh0DN&h2jEL{003Wln3pFW z6p_>zf(;x2wGQ|P8uyUuFQ{AZshfIXv(J%X3;RQTuXA;^QuFP+4a&WPfS=7^V8CBc z`4BKeWGuMJGv$^SS&5pQ^I>G-c8P|iG(9HanGyr z9Pz7}?@RtO)v=~O-mAP74x>^Bng~Peeu0}NIczs=kG};?&J9y@!*z9aOv@zi>Pcbl zx<92=APC^=P))EDq5lgG1OQh=&ey|W^+3P72>E!m+{N#8zNZ|g$Qy14lj;iX{BRn$ zv;_olNN!8~+64hs8A-Ms2Pnz^)y9>_L-qY{k$sIRxn(RFrWF}2ghpI@j4dQJU#r=VRa}3-Xld;{sgkR*+g>4=x~zyc9w;Ck!;jbzUPt8tY!AhN*igqlHl2O zuCe(#b?(9(*VN;z`KK9cFy=dmoZXiK%O;zOiA8?LYku9TIhm%~U==NCyGtf0G~7D6 zIko&)wcPj#ylH8F2N6~EZs^eb2XVbwi}1z3fW){|a4QUJ@rVuIrvRR!7c^Z3ir(+d z)-vjUWqlGaOU9&#HU9kmBmZZ}aLunN9=Y4^PbjQ?(d<A{`+l6Yy6H)rDTvu_mMyvK{AC&SgSETfewqDkj1EP?AUsWjsRD@P4 zBK#s<3p9Nt^XbK}73cqfFl$R&RVw<&S+| zd#*!x<~avL+{h&M<~dhr+bceIdQIH%Z_SsYyzH#I{7n}PYBr$K7(BzeRu`Df-mT2Q zp0!)gAS&gqOh4+l-mQUQU6x9lxxE>*F7rL0poJgX3K&8P5cy+grq|@zZ>_l(TWIs6 zDx)5f={5y$dwz^)i8>VN%9F_LEH1%_(t!-092SQi5#s~aYdHi)(`hue)6wzPp+495 zfcSw%2=uN2tn%phk%(><`GLXLqnxmZ*~U?mV1+rgkiH`-pWyOzk3hvKX{R2sH)ijK z>0`W8ac1vMl;&fm0QYD|H7y7HUwadw(!1xjhIhA=hb3;$}#8Ah_7D$o&k`|L&ehT^Rx3+*zRQg5ExnBt-KG634lr&nt(;fhjWB-0=7pB z;_hq$<_yZs{j>@~B`v3nQG{RaRf?E1|B~Cuj<#JBm|Urby(chJKoQ_;t><<4siGpM z#3WHct<1<+%!6BnccpM@R6+-H!U)PFQsHx$@@Q85yfG3nKRt|O7PD3lNHQS9bmQxQ z1;bF_5XL`Y>4XDn8_D~Afd1Cm(?5Q{H{tx4td%+$q?R>-H`Zw8d4EB}m7jCQxApX_ z{8Hy^deGkmX!0xKug<(QaI!{eTLpa{eqrJ*Zo^rW<+N1gb;h=QkLQxfROjx2~1Jv_-MMKk>YxdoA}KJhmU0 zfa*qaxqV7s#4A^AFB;(^X=!fi#Uw{rLQg{e1)dFjjR~(F{6~`9WztV=Ny&qr=}I}SQ8Mz zhgCIwIWDdcSt8FaBBZuQv_uq%cBELJUr2b+eCUx%>XFk{F4XK!IsHXj{G4z?gW`ik~4E!OSH^8Yal&1pfh3b2C|lM?vA8-+A#3xH*em| z4WHZ&?4-UVClxN0W)PE;U|E{wMp_bsPJBq(KA9=J@b@FchiL47e;Ti0?+3`-plrY@ z=JstA{2B**G>Yk@!>BfIw*byKI9elTO#9RPya-E?xzOr z?k~1x=rm?^$)CPMg}I3qEfJg5#&QO2-7RJ8gtz}NC{dJ?Mc zy>H#M*Yv1LOxN-9Na?Uaz1@8Y9Y(lpMHyezlnOI7s&2;VMEd@3eJ@}nEG2J5>Cp1g zu{r5Fo=JCY&w|~3>&$5D$$zCULppxZDVH%&F}R{EQs8)5eZb0{3Es@FFfzF*e0Nj$ zhak<8X*JgZ*P_$b;M7i*62{Y-RPyYHji2pmVd zJyNrzp5c5P>FZ#gR$6E`Kz<|ysd8+EQZPDwLHBGK8q%ivSj$0 z#EDsjV@Ry=OoL$-V6GVMeNF5dex#W!is==KD!G0Q{<$y64yIiH{a1Zz>J9?`uB(&1 z6Sh@*SCS)_CJD&ZG6S4b>&a5a3&ORQQC5{!e}MJsvPzmct|UABBp91vR1|1$bbc{q zUNUm`a#1>+C)0SI0|vayH;Mx_M0g#$cV3qV4Q{9Vab!%cU4P&3LaXrc^3@IzAvutXC6wd*-+s=w@yR7LqCEfh(R5waY#W>#)m5G zOW88Pf=~05;Jxx19md(Ne2dvDNyUfxcWq!0NvJ;Tuv+}1(T0?VW#}i8UL(WC;Qpl7 z!y8LhxA=8lbjr!}Q-1keS%bWTJAqv^X1nK^WtM|su*l|6pJ$#UK|;YzlA!R~xP6Eb zmjv6;0LHlwfML6>MIo|-!P8!B^c@#qg5WU0RZfD1nWrJcy)a^ozAo1h5AQ!S^kxbg z57R$%!DSto0S6aB<3&>9xyQ~uc!ibTE~JYjZ!fWwA)gs-$N^9mK2U8W%A%*ADEh}+ z0l`xIY%%!Vuj_z2Q>BuLeR$x~*2*Vu_X=;L?{UEdP{fde!*#SBnR`zB=Nc+$vSr@$ zYq9fy*}L5?PaI&RUl_PO=?)k-DGyv{q2*+IeqJ6t_R5%}ZIUu6TJo|oU;`r7d5c+* zH3zCneYyJUWq0rAaC<5JFal{$w7?e|-dMyK`D9Yhz&_z$!pB$92nJ~v0=)iv1cZr~ zK6=g91~m@<`%LkEvZ7~_gyWPTD|an&{Bh_5tb=on(RIS7z6g3+Q#W7?|L+6bCk3y9 z{!qD7eQg8JTKsShP{0h-|NN1Vq5XS_UiEm;wTuQMl-*Nfq zW9sU({rTTzoxnGNoC0Iwikvd3YYLgnXW(ZmUnUp8h@lP$`gpn;coPspyNRgE^Hk%Fx-;nub&R>WU}7MLP`=Oa zib=h^fSs7d zY^>)`;K)hOK?#%%IcOk=-SSd2bcje*2HA**wKxDuOA5`h6cxHFz?*2U8*L`wkUI=I z!k{4pPW}M8knzX!l-ZOLxieAbz9gm863cGFL1{qOR`fayC#0d=PY-*F zyZ1$HD@mE%0JFhvg(CzdOOQNa)qaK(1BYvafZpoQjD>=#Dt6Ut7Y#^LBfO^j!anE~ z*6s2pgu%n_baqSNpy|rniII961;)>_CoWNqky&bcDx7o2{1tX~k!hwnwzazoyl4=Y zYCV9d{i!z?gHRy@VCzyIu}rIZ3e)mt{hBKf@hlu6i60vb5O? zPFaqkpEY|AEyjzJPk?_Mr!HSY4`jeTI8$ilZBG<@>Dljr0s9)cw>n`nN^2uO{+Scv&Mm6Vu%dz)1_UO3 z<$@rpjNp9u81sqlQDpW9HXz3DtsI3J(?_I=q#OZ;6$%(-_T|o(&r66uXvRpb$G?z# z!`B@f_Fm3`GkS%H1+|b2kST&06F}NV*DhA!OhajE4L|E*2?gn*c|Uz15c|}3!Nr@6 z8)unjK)2&m<3AS1D@=SV^;hvzTvqcC_I8rT(RW=(yY4zxPB*SbE#J(&;iG}L)UXQZ>y=~f+B3NL(9 zH!>W6A`g9fi;m`Kd38|RWI81Z#Gw5_0-jhP5|pj#L(yFW2{X6>X{45}03= z_e+-lMrD6>|Fe8~%!@XeWM=Bf1vS#CS+KR)j8X=@j9?W9zidOULYQOed#;*weKkS4 zskved26=?Lz)}#d^DK3BZ0l0u?5j;gG~Ou7l)Lvt#0L1_@O&j$|o=} zGs#sgM4?(>jn7)^LTAa!x{O}u}KNQ-D~)yDIr5mS-*8}dVS zarcyW*Ld@dxyW8zispW^Jsc~vX0b6Bx~xzx0hbEqa+%@CJ7kvPn;6kItl5$_sfXX>A Qf|tw0@Tft#zFXY?0K=D0BLDyZ literal 0 HcmV?d00001 diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index afa3dcc8..04a4e6aa 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1332,22 +1332,72 @@ rows (default), or above. The following code adds the summary above: $spreadsheet->getActiveSheet()->setShowSummaryBelow(false); ``` -## Merge/unmerge cells +## Merge/Unmerge cells -If you have a big piece of data you want to display in a worksheet, you -can merge two or more cells together, to become one cell. This can be -done using the following code: +If you have a big piece of data you want to display in a worksheet, or a +heading that needs to span multiple sub-heading columns, you can merge +two or more cells together, to become one cell. This can be done using +the following code: ```php $spreadsheet->getActiveSheet()->mergeCells('A18:E22'); ``` -Removing a merge can be done using the unmergeCells method: +Removing a merge can be done using the `unmergeCells()` method: ```php $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` +MS Excel itself doesn't yet offer the functionality to simply hide the merged cells, or to merge the content of cells into a single cell, but it is available in Open/Libre Office. + +### Merge with MERGE_CELL_CONTENT_EMPTY + +The default behaviour is to empty all cells except for the top-left corner cell in the merge range; and this is also the default behaviour for the `mergeCells()` method in PhpSpreadsheet. +When this behaviour is applied, those cell values will be set to null; and if they are subsequently Unmerged, they will be empty cells. + +Passing an extra flag value to the `mergeCells()` method in PhpSpreadsheet can change this behaviour. + +![12-01-MergeCells-Options.png](./images/12-01-MergeCells-Options.png) + +Possible flag values are: +- Worksheet::MERGE_CELL_CONTENT_EMPTY (the default) +- Worksheet::MERGE_CELL_CONTENT_HIDE +- Worksheet::MERGE_CELL_CONTENT_MERGE + +### Merge with MERGE_CELL_CONTENT_HIDE + +The first alternative, available only in OpenOffice, is to hide those cells, but to leave their content intact. +When a file saved as `Xlsx` in those applications is opened in MS Excel, and those cells are unmerged, the original content will still be present. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_HIDE); +``` + +Will replicate that behaviour. + +### Merge with MERGE_CELL_CONTENT_MERGE + +The second alternative, available in both OpenOffice and LibreOffice is to merge the content of every cell in the merge range into the top-left cell, while setting those hidden cells to empty. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_MERGE); +``` + +Particularly when the merged cells contain formulae, the logic for this merge seems strange: +walking through the merge range, each cell is calculated in turn, and appended to the "master" cell, then it is emptied, so any subsequent calculations that reference the cell see an empty cell, not the pre-merge value. +For example, suppose our spreadsheet contains + +![12-01-MergeCells-Options-2.png](./images/12-01-MergeCells-Options-2.png) + +where `B2` is the formula `=5-B1` and `C2` is the formula `=A2/B2`, +and we want to merge cells `A2` to `C2` with all the cell values merged. +The result is: + +![12-01-MergeCells-Options-3.png](./images/12-01-MergeCells-Options-3.png) + +The cell value `12` from cell `A2` is fixed; the value from `B2` is the result of the formula `=5-B1` (`4`, which is appended to our merged value), and cell `B2` is then emptied, so when we evaluate cell `C2` with the formula `=A2/B2` it gives us `12 / 0` which results in a `#DIV/0!` error (so the error `#DIV/0!` is appended to our merged value rather than the original calculation result of `3`). + ## Inserting or Removing rows/columns You can insert/remove rows/columns at a specific position. The following diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ca087e61..1dcb0a12 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -363,7 +363,7 @@ class Gnumeric extends BaseReader if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos((string) $mergeCells, ':') !== false) { - $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 7e776ab7..e3de4731 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -20,6 +20,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; use XMLReader; use ZipArchive; @@ -759,7 +760,7 @@ class Ods extends BaseReader } $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 71496ece..a8de5228 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4585,7 +4585,7 @@ class Xls extends BaseReader (strpos($cellRangeAddress, ':') !== false) && ($this->includeCellRangeFiltered($cellRangeAddress)) ) { - $this->phpSheet->mergeCells($cellRangeAddress); + $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index fc38375a..26cd1af3 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -914,7 +914,7 @@ class Xlsx extends BaseReader foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { $mergeRef = (string) $mergeCell['ref']; if (strpos($mergeRef, ':') !== false) { - $docSheet->mergeCells((string) $mergeCell['ref']); + $docSheet->mergeCells((string) $mergeCell['ref'], Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 0b5e0966..d8f0d9dc 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; /** @@ -364,7 +365,7 @@ class Xml extends BaseReader $rowTo = $rowTo + $cell_ss['MergeDown']; } $cellRange .= ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } $hasCalculatedValue = false; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d13d4141..55327932 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -41,6 +41,10 @@ class Worksheet implements IComparable public const SHEETSTATE_HIDDEN = 'hidden'; public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const MERGE_CELL_CONTENT_EMPTY = 'empty'; + public const MERGE_CELL_CONTENT_HIDE = 'hide'; + public const MERGE_CELL_CONTENT_MERGE = 'merge'; + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** @@ -1758,10 +1762,15 @@ class Worksheet implements IComparable * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCells($range) + public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); @@ -1793,18 +1802,22 @@ class Worksheet implements IComparable $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); } - // Blank out the rest of the cells in the range (if they exist) - if ($numberRows > $numberColumns) { - $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); - } else { - $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); + if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) { + // Blank out the rest of the cells in the range (if they exist) + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour); + } else { + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour); + } } return $this; } - private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) { $iterator = $column->getCellIterator($firstRow); $iterator->setIterateOnlyExistingCells(true); @@ -1814,17 +1827,21 @@ class Worksheet implements IComparable if ($row > $lastRow) { break; } - $thisCell = $cell->getColumn() . $row; - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } } - private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getRowIterator($firstRow, $lastRow) as $row) { $iterator = $row->getCellIterator($firstColumn); $iterator->setIterateOnlyExistingCells(true); @@ -1835,13 +1852,31 @@ class Worksheet implements IComparable if ($columnIndex > $lastColumnIndex) { break; } - $thisCell = $column . $cell->getRow(); - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } + } + + public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array + { + if ($cell->getCoordinate() !== $upperLeft) { + Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance(); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $cellValue = $cell->getFormattedValue(); + if ($cellValue !== '') { + $leftCellValue[] = $cellValue; + } + } + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + + return $leftCellValue; } /** @@ -1856,17 +1891,22 @@ class Worksheet implements IComparable * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $row2 Numeric row coordinate of the last cell + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $cellRange = new CellRange( CellAddress::fromColumnAndRow($columnIndex1, $row1), CellAddress::fromColumnAndRow($columnIndex2, $row2) ); - return $this->mergeCells($cellRange); + return $this->mergeCells($cellRange, $behaviour); } /** diff --git a/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php new file mode 100644 index 00000000..68c9d87a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php @@ -0,0 +1,166 @@ +getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsDefaultBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviour(): void + { + $expectedResult = [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 9.9], + [5.5, 7.7, 13.2], + ]; + + $mergeRange = 'A1:C3'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', '2022-09-15'], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + /** + * @dataProvider mergeCellsMergeBehaviourProvider + */ + public function testMergeCellsMergeBehaviour(array $testData, string $mergeRange, array $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($testData, null, 'A1', true); + // Force a precalculation to populate the calculation cache, so that we can verify that it is being cleared + $worksheet->toArray(); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function mergeCellsMergeBehaviourProvider(): array + { + return [ + 'With Calculated Values' => [ + $this->testDataRaw, + 'A1:C3', + [ + ['1.1 2.2 1.1 4.4 5.5 0 1.1 0 0', null, null], + [null, null, null], + [null, null, null], + ], + ], + 'With Empty Cells' => [ + [ + [1, '', 2], + [null, 3, null], + [4, null, 5], + ], + 'A1:C3', + [ + ['1 2 3 4 5', null, null], + [null, null, null], + [null, null, null], + ], + ], + [ + [ + [12, '=5+1', '=A1/A2'], + ], + 'A1:C1', + [ + ['12 6 #DIV/0!', null, null], + ], + ], + ]; + } + + public function testMergeCellsMergeBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19 2022-09-15', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } +} From 84d6d983482adfc094cca24087cac454244c2d11 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 16 Sep 2022 01:55:36 +0200 Subject: [PATCH 59/69] Unit tests for correctly handling hidden merged cells in Readers --- .../Reader/Ods/HiddenMergeCellsTest.php | 48 ++++++++++++++++++ .../Reader/Xls/HiddenMergeCellsTest.php | 48 ++++++++++++++++++ .../Reader/Xlsx/HiddenMergeCellsTest.php | 48 ++++++++++++++++++ .../data/Reader/Ods/HiddenMergeCellsTest.ods | Bin 0 -> 8045 bytes .../data/Reader/XLS/HiddenMergeCellsTest.xls | Bin 0 -> 5632 bytes .../Reader/XLSX/HiddenMergeCellsTest.xlsx | Bin 0 -> 4576 bytes 6 files changed, 144 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php create mode 100644 tests/data/Reader/Ods/HiddenMergeCellsTest.ods create mode 100644 tests/data/Reader/XLS/HiddenMergeCellsTest.xls create mode 100644 tests/data/Reader/XLSX/HiddenMergeCellsTest.xlsx diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php new file mode 100644 index 00000000..710b292a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php new file mode 100644 index 00000000..c7085281 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php new file mode 100644 index 00000000..4919cd27 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/data/Reader/Ods/HiddenMergeCellsTest.ods b/tests/data/Reader/Ods/HiddenMergeCellsTest.ods new file mode 100644 index 0000000000000000000000000000000000000000..b8beb972c568a6702fa7e36c61d245776fa8c798 GIT binary patch literal 8045 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLS/HiddenMergeCellsTest.xls b/tests/data/Reader/XLS/HiddenMergeCellsTest.xls new file mode 100644 index 0000000000000000000000000000000000000000..fefbcecc802d7aafa01a4321bd6667e1a9adcfc5 GIT binary patch literal 5632 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 Date: Fri, 16 Sep 2022 08:25:26 -0700 Subject: [PATCH 60/69] R1C1 Format and Internationalization, plus Relative Offsets (#3052) * R1C1 Format and Internationalization, plus Relative Offsets Fix #1704, albeit imperfectly. Excel's implementation of this feature makes it impossible to fix perfectly. I don't know why it was necessary to internationalize R1C1 in the first place - the benefits are so minimal,and the result is worksheets that break when opened in different locales. Ugh. I can't even find complete documentation about the format in different languages; I am using https://answers.microsoft.com/en-us/officeinsider/forum/all/indirect-function-is-broken-at-least-for-excel-in/1fcbcf20-a103-4172-abf1-2c0dfe848e60 as my definitive reference. This fix concentrates on the original report, using the INDIRECT function; there may be other areas similarly affected. As with ambiguous date formats, PhpSpreadsheet will do a little better than Excel itself when reading spreadsheets with internationalized R1C1 by trying all possibilities before giving up. When it does give up, it will now return `#REF!`, as Excel does, rather than throwing an exception, which is certainly friendlier. Although read now works better, when writing it will use whatever the user specified, so spreadsheets breaking in the wrong locale will still happen. There were some bugs that turned up as I added test cases, all of them concerning relative addressing in R1C1 format, e.g. `R[+1]C[-1]`. The regexp for validating the format allowed for minus signs, but not plus signs. Also, the relevant functions did not allow for passing the current cell address, which made relative addressing impossible. The code now allows these, and suitable test cases are added. * Use Locale for Formats, but Not for XML Implementing a suggestion from @MarkBaker to use the system locale for determining R1C1 format rather than looping through a set of regexes and accepting any that work. This is closer to how Excel itself operates. The assumption we are making is to use the first character of the translated ROW and COLUMN functions. This will not work for Russian or Bulgarian, where each starts with the same letter, but it appears that Russian, at least, still uses R1C1. So our algorithm will not use non-ASCII characters, nor characters where ROW and COLUMN start with the same letter, falling back to R/C in those cases. Turkish falls into that category. Czech uses an accented character for one of the functions, and I'm guessing to use the unaccented character in that case. Polish COLUMN function is NR.KOLUMNY, and I'm guessing to use K in that case. The function that converts R1C1 references is also used by the XML reader *where the format is always R1C1*, not locale-based (confirmed by successfully opening in Excel an XML spreadsheet when my language is set to French). The conversion code now handles that distinction through the use of an extra parameter. Xml Reader Load Test is duplicated to confirm that spreadsheet is loaded properly whether the locale is English or French. (No, I did not add an INDIRECT function to the Xml spreadsheet.) Tests CsvIssue2232Test and TranslationTest both changed locale without resetting it when done. That omission was exposed by the new code, and both are now corrected. * OpenOffice and Gnumeric OpenOffice and Gnumeric make it much easier to test with other languages - they can be handled with an environment variable. Sensibly, they require R and C as the characters for R1C1 notation regardless of the language. Change code to recognize this difference from Excel. * Handle Output of ADDRESS Function One other function has to deal with R1C1 format as a string. Unlike INDIRECT, which receives the string on input, ADDRESS generates the string on output. Ensure that the ADDRESS output is consistent with the INDIRECT input. ADDRESS expects its 4th arg to be bool, but it can also accept int, and many examples on the net supply it as an int. This had not been handled properly, but is now corrected. * More Structured Test I earlier introduced a new test for relative R1C1 addressing. Rewrite it to be clearer. * Add Row for This to Locale Spreadsheet It took a while for me to figure out how it all works. I have added a new row (with English value `*RC`) to Translations.xlsx, in the "Lookup and Reference" section of sheet "Excel Functions". By starting the "function name" with an asterisk, it will not be confused with a "real" function (confirmed by a new test). This approach also gives us the flexibility to do something similar if another surprise case occurs in future; in particular, I think this is more flexible than adding this as another option on the "Excel Localisation" sheet. It also means that any errors or omissions in the list below will be handled as with any other translation problem, by updating the spreadsheet without needing to touch any code. The spreadsheet has the following entries in the *RC row: - first letter of ROW/COLUMN functions for da, de, es, fi, fr, hu, nl, nb, pt, pt_br, sv - no value for locales where ROW/COLUMN functions start with same letter - bg, ru, tr - no value for locales with a multi-part name for ROW and/or COLUMN - it, pl (I had not previously noted Italian as an exception) - no value for locales where ROW and/or COLUMN starts with a non-ASCII character - cs (this would also apply to bg and ru which are already included under "same letter") - it does nothing for locales which are defined on the "Excel Localisation" sheet but have no entries yet on the "Excel Functions" sheet (e.g. eu) Note that all but the first bullet item will continue to use R/C, which leaves them no worse off than they were before this change. --- infra/LocaleGenerator.php | 2 +- .../Calculation/Calculation.php | 2 +- .../Calculation/LookupRef/Address.php | 7 +- .../Calculation/LookupRef/Helpers.php | 10 +- .../Calculation/LookupRef/Indirect.php | 9 +- .../Calculation/locale/Translations.xlsx | Bin 110548 -> 111436 bytes .../Calculation/locale/da/functions | 1 + .../Calculation/locale/de/functions | 1 + .../Calculation/locale/es/functions | 1 + .../Calculation/locale/fi/functions | 1 + .../Calculation/locale/fr/functions | 1 + .../Calculation/locale/hu/functions | 1 + .../Calculation/locale/nb/functions | 1 + .../Calculation/locale/nl/functions | 1 + .../Calculation/locale/pt/br/functions | 1 + .../Calculation/locale/pt/functions | 1 + .../Calculation/locale/sv/functions | 1 + src/PhpSpreadsheet/Cell/AddressHelper.php | 29 +++- .../LookupRef/AddressInternationalTest.php | 79 +++++++++++ .../Functions/LookupRef/AddressTest.php | 6 - .../LookupRef/IndirectInternationalTest.php | 132 ++++++++++++++++++ .../Functions/LookupRef/IndirectTest.php | 44 ++++++ .../Calculation/TranslationTest.php | 5 + .../LocaleGeneratorTest.php | 42 +++++- .../Reader/Csv/CsvIssue2232Test.php | 9 +- .../Reader/Xml/PageSetupTest.php | 18 ++- .../Reader/Xml/XmlLoadTest.php | 43 +++++- tests/data/Calculation/LookupRef/ADDRESS.php | 16 +++ tests/data/Calculation/Translations.php | 10 ++ 29 files changed, 436 insertions(+), 38 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index bb97754d..992bde17 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -146,7 +146,7 @@ class LocaleGenerator $translationValue = $translationCell->getValue(); if ($this->isFunctionCategoryEntry($translationCell)) { $this->writeFileSectionHeader($functionFile, "{$translationValue} ({$functionName})"); - } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions)) { + } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions) && substr($functionName, 0, 1) !== '*') { $this->log("Function {$functionName} is not defined in PhpSpreadsheet"); } elseif (!empty($translationValue)) { $functionTranslation = "{$functionName} = {$translationValue}" . self::EOL; diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index b5a26f62..94eadd85 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3110,7 +3110,7 @@ class Calculation [$localeFunction] = explode('##', $localeFunction); // Strip out comments if (strpos($localeFunction, '=') !== false) { [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); - if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { + if ((substr($fName, 0, 1) === '*' || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { self::$localeFunctions[$fName] = $lfName; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index c8cdf2dd..0d2db8b2 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; class Address @@ -72,6 +73,9 @@ class Address $sheetName = self::sheetName($sheetName); + if (is_int($referenceStyle)) { + $referenceStyle = (bool) $referenceStyle; + } if ((!is_bool($referenceStyle)) || $referenceStyle === self::REFERENCE_STYLE_A1) { return self::formatAsA1($row, $column, $relativity, $sheetName); } @@ -113,7 +117,8 @@ class Address if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { $row = "[{$row}]"; } + [$rowChar, $colChar] = AddressHelper::getRowAndColumnChars(); - return "{$sheetName}R{$row}C{$column}"; + return "{$sheetName}$rowChar{$row}$colChar{$column}"; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 7408a66e..76a194b3 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -13,12 +13,12 @@ class Helpers public const CELLADDRESS_USE_R1C1 = false; - private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1): string + private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1, ?int $baseRow = null, ?int $baseCol = null): string { if ($a1 === self::CELLADDRESS_USE_R1C1) { - $cellAddress1 = AddressHelper::convertToA1($cellAddress1); + $cellAddress1 = AddressHelper::convertToA1($cellAddress1, $baseRow ?? 1, $baseCol ?? 1); if ($cellAddress2) { - $cellAddress2 = AddressHelper::convertToA1($cellAddress2); + $cellAddress2 = AddressHelper::convertToA1($cellAddress2, $baseRow ?? 1, $baseCol ?? 1); } } @@ -35,7 +35,7 @@ class Helpers } } - public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = ''): array + public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = '', ?int $baseRow = null, ?int $baseCol = null): array { $cellAddress1 = $cellAddress; $cellAddress2 = null; @@ -52,7 +52,7 @@ class Helpers if (strpos($cellAddress, ':') !== false) { [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); } - $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol); return [$cellAddress1, $cellAddress2, $cellAddress]; } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index 417a1f79..91a14491 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Indirect @@ -63,6 +64,8 @@ class Indirect */ public static function INDIRECT($cellAddress, $a1fmt, Cell $cell) { + [$baseCol, $baseRow] = Coordinate::indexesFromString($cell->getCoordinate()); + try { $a1 = self::a1Format($a1fmt); $cellAddress = self::validateAddress($cellAddress); @@ -78,7 +81,11 @@ class Indirect $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); } - [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); + try { + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol); + } catch (Exception $e) { + return ExcelError::REF(); + } if ( (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || diff --git a/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx index d013aee0f1a2dea934a0a215e5aaa1e3202bbecb..7b9fb0dddfff921e9831507af3b0dfd53a8ee10a 100644 GIT binary patch delta 97055 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 z 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 aY;M+8O0usI{{4An delta 96406 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

kRS+FJs>|j1s|SB3^yySfvWerv?)OThZcTsFcT| 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 = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale, string $r, string $c): void + { + if ($locale !== '') { + Settings::setLocale($locale); + } + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerInternational(): array + { + return [ + 'Default' => ['', 'R', 'C'], + 'English' => ['en', 'R', 'C'], + 'French' => ['fr', 'L', 'C'], + 'German' => ['de', 'Z', 'S'], + 'Made-up' => ['xx', 'R', 'C'], + 'Spanish' => ['es', 'F', 'C'], + 'Bulgarian' => ['bg', 'R', 'C'], + 'Czech' => ['cs', 'R', 'C'], // maybe should be R/S + 'Polish' => ['pl', 'R', 'C'], // maybe should be W/K + 'Turkish' => ['tr', 'R', 'C'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode, string $r, string $c): void + { + Functions::setCompatibilityMode($compatibilityMode); + Settings::setLocale('de'); + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL, 'Z', 'S'], + [Functions::COMPATIBILITY_OPENOFFICE, 'R', 'C'], + [Functions::COMPATIBILITY_GNUMERIC, 'R', 'C'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php index 2b92030e..2a6ae883 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class AddressTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerADDRESS * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php new file mode 100644 index 00000000..7d6d0a65 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php @@ -0,0 +1,132 @@ +locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale): void + { + Settings::setLocale($locale); + $sameAsEnglish = ['en', 'xx', 'ru', 'tr', 'cs', 'pl']; + $sheet = $this->getSheet(); + $sheet->getCell('C1')->setValue('text'); + $sheet->getCell('A2')->setValue('en'); + $sheet->getCell('B2')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A3')->setValue('fr'); + $sheet->getCell('B3')->setValue('=INDIRECT("L1C3", false)'); + $sheet->getCell('A4')->setValue('de'); + $sheet->getCell('B4')->setValue('=INDIRECT("Z1S3", false)'); + $sheet->getCell('A5')->setValue('es'); + $sheet->getCell('B5')->setValue('=INDIRECT("F1C3", false)'); + $sheet->getCell('A6')->setValue('xx'); + $sheet->getCell('B6')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A7')->setValue('ru'); + $sheet->getCell('B7')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A8')->setValue('cs'); + $sheet->getCell('B8')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A9')->setValue('tr'); + $sheet->getCell('B9')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A10')->setValue('pl'); + $sheet->getCell('B10')->setValue('=INDIRECT("R1C3", false)'); + $maxRow = $sheet->getHighestRow(); + for ($row = 2; $row <= $maxRow; ++$row) { + $rowLocale = $sheet->getCell("A$row")->getValue(); + if (in_array($rowLocale, $sameAsEnglish, true) && in_array($locale, $sameAsEnglish, true)) { + $expectedResult = 'text'; + } else { + $expectedResult = ($locale === $sheet->getCell("A$row")->getValue()) ? 'text' : '#REF!'; + } + self::assertSame($expectedResult, $sheet->getCell("B$row")->getCalculatedValue(), "Locale $locale error in cell B$row $rowLocale"); + } + } + + public function providerInternational(): array + { + return [ + 'English' => ['en'], + 'French' => ['fr'], + 'German' => ['de'], + 'Made-up' => ['xx'], + 'Spanish' => ['es'], + 'Russian' => ['ru'], + 'Czech' => ['cs'], + 'Polish' => ['pl'], + 'Turkish' => ['tr'], + ]; + } + + /** + * @dataProvider providerRelativeInternational + */ + public function testRelativeInternational(string $locale, string $cell, string $relative): void + { + Settings::setLocale($locale); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell($cell)->setValue("=INDIRECT(\"$relative\", false)"); + self::assertSame('text', $sheet->getCell($cell)->getCalculatedValue()); + } + + public function providerRelativeInternational(): array + { + return [ + 'English A3' => ['en', 'A3', 'R[]C[+2]'], + 'French B4' => ['fr', 'B4', 'L[-1]C[+1]'], + 'German C5' => ['de', 'C5', 'Z[-2]S[]'], + 'Spanish E1' => ['es', 'E1', 'F[+2]C[-2]'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode): void + { + Functions::setCompatibilityMode($compatibilityMode); + if ($compatibilityMode === Functions::COMPATIBILITY_EXCEL) { + $expected1 = '#REF!'; + $expected2 = 'text'; + } else { + $expected2 = '#REF!'; + $expected1 = 'text'; + } + Settings::setLocale('fr'); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell('A1')->setValue('=INDIRECT("R3C3", false)'); + $sheet->getCell('A2')->setValue('=INDIRECT("L3C3", false)'); + self::assertSame($expected1, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($expected2, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL], + [Functions::COMPATIBILITY_OPENOFFICE], + [Functions::COMPATIBILITY_GNUMERIC], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php index 7601e336..accfc058 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php @@ -132,4 +132,48 @@ class IndirectTest extends AllSetupTeardown $result = \PhpOffice\PhpSpreadsheet\Calculation\Functions::flattenSingleValue($result); self::assertSame('This is it', $result); } + + /** + * @param null|int|string $expectedResult + * + * @dataProvider providerRelative + */ + public function testR1C1Relative($expectedResult, string $address): void + { + $sheet = $this->getSheet(); + $sheet->fromArray([ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ['a3', 'b3', 'c3'], + ['a4', 'b4', 'c4'], + ]); + $sheet->getCell('B2')->setValue('=INDIRECT("' . $address . '", false)'); + self::assertSame($expectedResult, $sheet->getCell('B2')->getCalculatedValue()); + } + + public function providerRelative(): array + { + return [ + 'same row with bracket next column' => ['c2', 'R[]C[+1]'], + 'same row without bracket next column' => ['c2', 'RC[+1]'], + 'same row without bracket next column no plus sign' => ['c2', 'RC[1]'], + 'same row previous column' => ['a2', 'RC[-1]'], + 'previous row previous column' => ['a1', 'R[-1]C[-1]'], + 'previous row same column with bracket' => ['b1', 'R[-1]C[]'], + 'previous row same column without bracket' => ['b1', 'R[-1]C'], + 'previous row next column' => ['c1', 'R[-1]C[+1]'], + 'next row no plus sign previous column' => ['a3', 'R[1]C[-1]'], + 'next row previous column' => ['a3', 'R[+1]C[-1]'], + 'next row same column' => ['b3', 'R[+1]C'], + 'next row next column' => ['c3', 'R[+1]C[+1]'], + 'two rows down same column' => ['b4', 'R[+2]C'], + 'invalid row' => ['#REF!', 'R[-2]C'], + 'invalid column' => ['#REF!', 'RC[-2]'], + 'circular reference' => [0, 'RC'], // matches Excel's treatment + 'absolute row absolute column' => ['c2', 'R2C3'], + 'absolute row relative column' => ['a2', 'R2C[-1]'], + 'relative row absolute column lowercase' => ['a2', 'rc1'], + 'uninitialized cell' => [null, 'RC[+2]'], // Excel result is 0 + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php index e2384460..ac1f15a1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php @@ -19,18 +19,23 @@ class TranslationTest extends TestCase */ private $returnDate; + /** @var string */ + private $locale; + protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); $this->returnDate = Functions::getReturnDateType(); Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); Functions::setReturnDateType($this->returnDate); + Settings::setLocale($this->locale); } /** diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php index e7429a25..ca9d5183 100644 --- a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -11,31 +11,59 @@ class LocaleGeneratorTest extends TestCase { public function testLocaleGenerator(): void { + $directory = realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/') ?: ''; + self::assertNotEquals('', $directory); $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) ->getProperty('phpSpreadsheetFunctions'); $phpSpreadsheetFunctionsProperty->setAccessible(true); $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); $localeGenerator = new LocaleGenerator( - (string) realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), + $directory . DIRECTORY_SEPARATOR, 'Translations.xlsx', $phpSpreadsheetFunctions ); $localeGenerator->generateLocales(); $testLocales = [ + 'bg', + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fi', 'fr', + 'hu', + 'it', + 'nb', 'nl', + 'pl', 'pt', - 'pt_br', 'ru', + 'sv', + 'tr', ]; - foreach ($testLocales as $locale) { - $locale = str_replace('_', '/', $locale); - $path = realpath(__DIR__ . "/../../src/PhpSpreadsheet/Calculation/locale/{$locale}"); - self::assertFileExists("{$path}/config"); - self::assertFileExists("{$path}/functions"); + $count = count(glob($directory . DIRECTORY_SEPARATOR . '*') ?: []) - 1; // exclude Translations.xlsx + self::assertCount($count, $testLocales); + $testLocales[] = 'pt_br'; + $testLocales[] = 'en_uk'; + $noconfig = ['en']; + $nofunctions = ['en', 'en_uk']; + foreach ($testLocales as $originalLocale) { + $locale = str_replace('_', DIRECTORY_SEPARATOR, $originalLocale); + $path = $directory . DIRECTORY_SEPARATOR . $locale; + if (in_array($originalLocale, $noconfig, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'config'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'config'); + } + if (in_array($originalLocale, $nofunctions, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'functions'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'functions'); + } } } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php index f9321102..429874dc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php @@ -2,11 +2,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; use PhpOffice\PhpSpreadsheet\Reader\Csv; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class CsvIssue2232Test extends TestCase @@ -16,14 +16,19 @@ class CsvIssue2232Test extends TestCase */ private $valueBinder; + /** @var string */ + private $locale; + protected function setUp(): void { $this->valueBinder = Cell::getValueBinder(); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Cell::setValueBinder($this->valueBinder); + Settings::setLocale($this->locale); } /** @@ -78,7 +83,7 @@ class CsvIssue2232Test extends TestCase Cell::setValueBinder($binder); } - Calculation::getInstance()->setLocale('fr'); + Settings::setLocale('fr'); $reader = new Csv(); $filename = 'tests/data/Reader/CSV/issue.2232.csv'; diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php index 97476ed5..ae79dd0e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php @@ -14,19 +14,25 @@ class PageSetupTest extends TestCase private const MARGIN_UNIT_CONVERSION = 2.54; // Inches to cm /** - * @var Spreadsheet + * @var ?Spreadsheet */ private $spreadsheet; - protected function setup(): void + /** @var string */ + private $filename = 'tests/data/Reader/Xml/PageSetup.xml'; + + protected function tearDown(): void { - $filename = 'tests/data/Reader/Xml/PageSetup.xml'; - $reader = new Xml(); - $this->spreadsheet = $reader->load($filename); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } public function testPageSetup(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageSetupAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { @@ -49,6 +55,8 @@ class PageSetupTest extends TestCase public function testPageMargins(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageMarginAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php index 29d81299..9846b861 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php @@ -4,18 +4,51 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xml; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Reader\Xml; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class XmlLoadTest extends TestCase { - public function testLoad(): void + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var string */ + private $locale; + + protected function setUp(): void + { + $this->locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + Settings::setLocale($this->locale); + } + + public function testLoadEnglish(): void + { + $this->xtestLoad(); + } + + public function testLoadFrench(): void + { + Settings::setLocale('fr'); + $this->xtestLoad(); + } + + public function xtestLoad(): void { $filename = __DIR__ . '/../../../..' . '/samples/templates/excel2003.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); @@ -71,7 +104,7 @@ class XmlLoadTest extends TestCase $reader = new Xml(); $filter = new XmlFilter(); $reader->setReadFilter($filter); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); self::assertEquals('Report Data', $sheet->getTitle()); @@ -87,7 +120,7 @@ class XmlLoadTest extends TestCase . '/samples/templates/excel2003.xml'; $reader = new Xml(); $reader->setLoadSheetsOnly(['Unknown Sheet', 'Report Data']); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Report Data', $sheet->getTitle()); @@ -102,7 +135,7 @@ class XmlLoadTest extends TestCase . '/../../../..' . '/samples/templates/excel2003.short.bad.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Sample Data', $sheet->getTitle()); diff --git a/tests/data/Calculation/LookupRef/ADDRESS.php b/tests/data/Calculation/LookupRef/ADDRESS.php index b9e170d5..14a3cf7d 100644 --- a/tests/data/Calculation/LookupRef/ADDRESS.php +++ b/tests/data/Calculation/LookupRef/ADDRESS.php @@ -48,6 +48,22 @@ return [ false, 'EXCEL SHEET', ], + '0 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!R2C3", + 2, + 3, + null, + 0, + 'EXCEL SHEET', + ], + '1 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!\$C\$2", + 2, + 3, + null, + 1, + 'EXCEL SHEET', + ], [ "'EXCEL SHEET'!\$C\$2", 2, diff --git a/tests/data/Calculation/Translations.php b/tests/data/Calculation/Translations.php index 604c6721..0965bac1 100644 --- a/tests/data/Calculation/Translations.php +++ b/tests/data/Calculation/Translations.php @@ -80,4 +80,14 @@ return [ 'nb', '=MAX(ABS({2,-3;-4,5}), ABS{-2,3;4,-5})', ], + 'not fooled by *RC' => [ + '=3*RC(B1)', + 'fr', + '=3*RC(B1)', + ], + 'handle * for ROW' => [ + '=3*LIGNE(B1)', + 'fr', + '=3*ROW(B1)', + ], ]; From b7fa4701382794dac85ed4caf4a2f760e1c92cde Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 16 Sep 2022 09:16:11 -0700 Subject: [PATCH 61/69] Scrutinizer Changes (#3060) * Scrutinizer Changes Scrutinizer appears to be working again. But the PRs that have used it have neither added new issues nor fixed existing ones. This PR should fix some exisiting; let's see what Scrutinizer does with it. * Address Some False Positives In Reader/Xlsx/Chart. --- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 15 ++++++++++----- src/PhpSpreadsheet/Writer/Html.php | 3 --- .../Reader/Xlsx/AutoFilter2Test.php | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 6bc6d7c5..c22334ca 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -14,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Chart\TrendLine; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; @@ -94,7 +95,7 @@ class Chart break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { - $chartDetailsC = $chartDetails->children($this->cNamespace); + $chartDetails = Xlsx::testSimpleXml($chartDetails); switch ($chartDetailsKey) { case 'autoTitleDeleted': /** @var bool */ @@ -113,8 +114,8 @@ class Chart $plotSeries = $plotAttributes = []; $catAxRead = false; $plotNoFill = false; - /** @var SimpleXMLElement $chartDetail */ foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { case 'spPr': $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); @@ -122,8 +123,8 @@ class Chart $plotNoFill = true; } if (isset($possibleNoFill->gradFill->gsLst)) { - /** @var SimpleXMLElement $gradient */ foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + $gradient = Xlsx::testSimpleXml($gradient); /** @var float */ $pos = self::getAttribute($gradient, 'pos', 'float'); $gradientArray[] = [ @@ -348,6 +349,7 @@ class Chart $legendLayout = null; $legendOverlay = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { case 'legendPos': $legendPos = self::getAttribute($chartDetail, 'val', 'string'); @@ -399,11 +401,13 @@ class Chart $caption = []; $titleLayout = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { case 'tx': if (isset($chartDetail->rich)) { $titleDetails = $chartDetail->rich->children($this->aNamespace); foreach ($titleDetails as $titleKey => $titleDetail) { + $titleDetail = Xlsx::testSimpleXml($titleDetail); switch ($titleKey) { case 'p': $titleDetailPart = $titleDetail->children($this->aNamespace); @@ -440,6 +444,7 @@ class Chart } $layout = []; foreach ($details as $detailKey => $detail) { + $detail = Xlsx::testSimpleXml($detail); $layout[$detailKey] = self::getAttribute($detail, 'val', 'string'); } @@ -472,8 +477,8 @@ class Chart $lineStyle = null; $labelLayout = null; $trendLines = []; - /** @var SimpleXMLElement $seriesDetail */ foreach ($seriesDetails as $seriesKey => $seriesDetail) { + $seriesDetail = Xlsx::testSimpleXml($seriesDetail); switch ($seriesKey) { case 'idx': $seriesIndex = self::getAttribute($seriesDetail, 'val', 'integer'); @@ -786,6 +791,7 @@ class Chart $pointCount = 0; foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) { + $seriesValue = Xlsx::testSimpleXml($seriesValue); switch ($seriesValueIdx) { case 'ptCount': $pointCount = self::getAttribute($seriesValue, 'val', 'integer'); @@ -858,7 +864,6 @@ class Chart private function parseRichText(SimpleXMLElement $titleDetailPart): RichText { $value = new RichText(); - $objText = null; $defaultFontSize = null; $defaultBold = null; $defaultItalic = null; diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index fca5ee89..0fef0f60 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1461,9 +1461,6 @@ class Html extends BaseWriter foreach ($values as $cellAddress) { [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum); - $colSpan = 1; - $rowSpan = 1; - // Cell Data $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 06d6b562..7a264394 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -93,6 +93,7 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; From ceb3bb2f38411d9efd7e606eaa27e4df0d325e68 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 16 Sep 2022 13:16:45 -0700 Subject: [PATCH 62/69] Changelog Catch-up (#3069) Added 5 changes. --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15e9d5b..ad275147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions - Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of JpGraph library to render charts added. -- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes. ### Changed @@ -49,6 +49,11 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) - Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) - Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) +- Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016) +- Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018) +- Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048) +- R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052) +- Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053) ## 1.24.1 - 2022-07-18 From c465b1c28390719831a889a9521951f0732a588f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 17 Sep 2022 07:32:12 -0700 Subject: [PATCH 63/69] Minor Changes for Cygwin (#3070) No source code changes. Mitoteam added a change to better accomodate Cygwin; pick up their new release. While testing, discovered one test that was already skipped on Windows and needs to be skipped on Cygwin as well. --- composer.json | 2 +- composer.lock | 14 +++++++------- tests/PhpSpreadsheetTests/Shared/FileTest.php | 6 ++++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index a4b033cd..ea7f57b1 100644 --- a/composer.json +++ b/composer.json @@ -81,7 +81,7 @@ "dealerdirect/phpcodesniffer-composer-installer": "dev-master", "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "10.2.3", + "mitoteam/jpgraph": "10.2.4", "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", diff --git a/composer.lock b/composer.lock index 508733ee..af246273 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b5bdb9f96d18ce59557436521053fdd9", + "content-hash": "6d946e91cbe5d38e1cfb0208512ab981", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1342,16 +1342,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.2.3", + "version": "10.2.4", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "21121535537e05c32e7964327b80746462a6057d" + "reference": "9ce4d106a89f120c7e220ea22205ef7956a7027b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/21121535537e05c32e7964327b80746462a6057d", - "reference": "21121535537e05c32e7964327b80746462a6057d", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/9ce4d106a89f120c7e220ea22205ef7956a7027b", + "reference": "9ce4d106a89f120c7e220ea22205ef7956a7027b", "shasum": "" }, "require": { @@ -1382,9 +1382,9 @@ ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.2.3" + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.4" }, - "time": "2022-09-14T04:02:09+00:00" + "time": "2022-09-15T05:57:43+00:00" }, { "name": "mpdf/mpdf", diff --git a/tests/PhpSpreadsheetTests/Shared/FileTest.php b/tests/PhpSpreadsheetTests/Shared/FileTest.php index ddc54b5e..e65ec810 100644 --- a/tests/PhpSpreadsheetTests/Shared/FileTest.php +++ b/tests/PhpSpreadsheetTests/Shared/FileTest.php @@ -87,12 +87,14 @@ class FileTest extends TestCase public function testNotReadable(): void { - if (PHP_OS_FAMILY === 'Windows') { + if (PHP_OS_FAMILY === 'Windows' || stristr(PHP_OS, 'CYGWIN') !== false) { self::markTestSkipped('chmod does not work reliably on Windows'); } $this->tempfile = $temp = File::temporaryFileName(); file_put_contents($temp, ''); - chmod($temp, 0070); + if (chmod($temp, 0070) === false) { + self::markTestSkipped('chmod failed'); + } self::assertFalse(File::testFileNoThrow($temp)); $this->expectException(ReaderException::class); $this->expectExceptionMessage('for reading'); From 1746a5ac26bddcbce9521c0e8b91d2334da0b68e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 18 Sep 2022 16:58:56 +0200 Subject: [PATCH 64/69] Ensure that the sqRef stored for a DataValidation is updated on insert/delete rows/columns, together with the DataValidationCollection value --- CHANGELOG.md | 1 + src/PhpSpreadsheet/ReferenceHelper.php | 5 +++-- tests/PhpSpreadsheetTests/ReferenceHelperTest.php | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f15e9d5b..b6010729 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) - Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) - Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) - cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 08a38b3b..822b754a 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -253,10 +253,11 @@ class ReferenceHelper ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) : uksort($aDataValidationCollection, [self::class, 'cellSort']); - foreach ($aDataValidationCollection as $cellAddress => $value) { + foreach ($aDataValidationCollection as $cellAddress => $dataValidation) { $newReference = $this->updateCellReference($cellAddress); if ($cellAddress !== $newReference) { - $worksheet->setDataValidation($newReference, $value); + $dataValidation->setSqref($newReference); + $worksheet->setDataValidation($newReference, $dataValidation); $worksheet->setDataValidation($cellAddress, null); } } diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index 2640d80c..fbd60155 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -311,6 +311,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('E7')->hasDataValidation()); + self::assertSame('E7', $sheet->getDataValidation('E7')->getSqref()); } public function testDeleteRowsWithDataValidation(): void @@ -326,6 +327,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('E3')->hasDataValidation()); + self::assertSame('E3', $sheet->getDataValidation('E3')->getSqref()); } public function testDeleteColumnsWithDataValidation(): void @@ -341,6 +343,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('C5')->hasDataValidation()); + self::assertSame('C5', $sheet->getDataValidation('C5')->getSqref()); } public function testInsertColumnsWithDataValidation(): void @@ -356,6 +359,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('G5')->hasDataValidation()); + self::assertSame('G5', $sheet->getDataValidation('G5')->getSqref()); } private function setDataValidation(Worksheet $sheet, string $cellAddress): void From 1f5ae85d193cac15dd6e23069adf40831dbe883c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 18 Sep 2022 21:16:36 +0200 Subject: [PATCH 65/69] Minor documentation update --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index dd712bff..c3320d70 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ For Chart export, we support following packages, which you will also need to ins You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) +One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. + and then configure PhpSpreadsheet using: ```php Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph From 579d2f9f6987960a5ab55b14ec994b79e4445ec1 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 18 Sep 2022 21:17:03 +0200 Subject: [PATCH 66/69] Minor documentation update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c3320d70..ef04cd07 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,6 @@ For Chart export, we support following packages, which you will also need to ins You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) -One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. - and then configure PhpSpreadsheet using: ```php Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph @@ -85,6 +83,8 @@ Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::cla Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph ``` +One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). From a3921d20bc5c9a2ba6add05ad96fb41f873e36bd Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 19 Sep 2022 06:20:48 -0700 Subject: [PATCH 67/69] Upgrade ezyang/htmlpurifier for Php8.2 (#3073) They have just issued a new release. Using that should eliminate the last of our Php8.2 unit test problems, so we will be able to change that to non-experimental when it is convenient for us. --- composer.json | 2 +- composer.lock | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index ea7f57b1..858bd819 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", - "ezyang/htmlpurifier": "^4.13", + "ezyang/htmlpurifier": "4.15", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", diff --git a/composer.lock b/composer.lock index af246273..a5bc7d7b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,24 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6d946e91cbe5d38e1cfb0208512ab981", + "content-hash": "9a7fc81b4223c114749fc07401fb4b6f", "packages": [ { "name": "ezyang/htmlpurifier", - "version": "v4.14.0", + "version": "v4.15.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" + "reference": "8d9f4c9ec154922ff19690ffade9ed915b27a017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/8d9f4c9ec154922ff19690ffade9ed915b27a017", + "reference": "8d9f4c9ec154922ff19690ffade9ed915b27a017", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { @@ -53,9 +63,9 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.15.0" }, - "time": "2021-12-25T01:21:49+00:00" + "time": "2022-09-18T06:23:57+00:00" }, { "name": "maennchen/zipstream-php", From a2b29841041ca1aa2c5d54e921b8f1dae0098b3f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 19 Sep 2022 06:49:01 -0700 Subject: [PATCH 68/69] Document Charset Restriction for Html/Xml Reader (#3068) Fix #1681, although probably not to the originator's satisfaction. Html and Xml readers will handle documents only with a charset of UTF-8. This PR documents that restriction. No change to source code; see the original issue for explanation. --- docs/topics/reading-and-writing-to-file.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 0bcc1909..55929e85 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -282,6 +282,7 @@ versions of Microsoft Excel. **Excel 2003 XML limitations** Please note that Excel 2003 XML format has some limits regarding to styling cells and handling large spreadsheets via PHP. +Also, only files using charset UTF-8 are supported. ### \PhpOffice\PhpSpreadsheet\Reader\Xml @@ -701,6 +702,7 @@ extension. **HTML limitations** Please note that HTML file format has some limits regarding to styling cells, number formatting, ... +Also, only files using charset UTF-8 are supported. ### \PhpOffice\PhpSpreadsheet\Reader\Html From 53e0828d49c3a330d15d19f94053db1dbb23be8a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:37:00 -0700 Subject: [PATCH 69/69] Sync composer.lock (#3075) When I cloned this morning, composer gave me a message that the lock file was not up to date with the latest changes in composer.json. I do not understand why, but it suggested to run `composer update`, which I did. This led to a handful of problems with php-cs-fixer, all fixed with changes to doc-blocks, and phpstan (only Writer/Xls/Worksheet required a change to code). We would presumably have had these problems at the start of next month when dependabot did its thing, so fix them now. --- composer.lock | 146 +++++++++--------- phpstan-baseline.neon | 2 +- .../Calculation/Information/Value.php | 2 +- src/PhpSpreadsheet/Cell/Cell.php | 10 -- src/PhpSpreadsheet/Style/Color.php | 2 - src/PhpSpreadsheet/Worksheet/CellIterator.php | 1 + src/PhpSpreadsheet/Writer/Xls.php | 2 - src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 8 +- .../Functions/LookupRef/FormulaTextTest.php | 1 + .../PhpSpreadsheetTests/Helper/SampleTest.php | 2 + .../Worksheet/WorksheetTest.php | 3 + 11 files changed, 87 insertions(+), 92 deletions(-) diff --git a/composer.lock b/composer.lock index a5bc7d7b..22513999 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9a7fc81b4223c114749fc07401fb4b6f", + "content-hash": "5062910e2e463ba84b1c753c58624ca9", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1194,16 +1194,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.10.0", + "version": "v3.11.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe" + "reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", - "reference": "76d7da666e66d83a1dc27a9d1c625c80cc4ac1fe", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/7dcdea3f2f5f473464e835be9be55283ff8cfdc3", + "reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3", "shasum": "" }, "require": { @@ -1271,7 +1271,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.10.0" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.11.0" }, "funding": [ { @@ -1279,7 +1279,7 @@ "type": "github" } ], - "time": "2022-08-17T22:13:10+00:00" + "time": "2022-09-01T18:24:51+00:00" }, { "name": "masterminds/html5", @@ -1534,16 +1534,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.14.0", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/34bea19b6e03d8153165d8f30bba4c3be86184c1", - "reference": "34bea19b6e03d8153165d8f30bba4c3be86184c1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -1584,9 +1584,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.14.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2022-05-31T20:59:12+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "paragonie/random_compat", @@ -1957,16 +1957,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.8.2", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c" + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c53312ecc575caf07b0e90dee43883fdf90ca67c", - "reference": "c53312ecc575caf07b0e90dee43883fdf90ca67c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f6598a5ff12ca4499a836815e08b4d77a2ddeb20", + "reference": "f6598a5ff12ca4499a836815e08b4d77a2ddeb20", "shasum": "" }, "require": { @@ -1990,9 +1990,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.8.2" + "source": "https://github.com/phpstan/phpstan/tree/1.8.5" }, "funding": [ { @@ -2003,16 +2007,12 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-07-20T09:57:31+00:00" + "time": "2022-09-07T16:05:32+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -2068,16 +2068,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.16", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2593003befdcc10db5e213f9f28814f5aa8ac073", - "reference": "2593003befdcc10db5e213f9f28814f5aa8ac073", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { @@ -2133,7 +2133,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.16" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -2141,7 +2141,7 @@ "type": "github" } ], - "time": "2022-08-20T05:26:47+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2386,16 +2386,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.23", + "version": "9.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34" + "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/888556852e7e9bbeeedb9656afe46118765ade34", - "reference": "888556852e7e9bbeeedb9656afe46118765ade34", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", + "reference": "d0aa6097bef9fd42458a9b3c49da32c6ce6129c5", "shasum": "" }, "require": { @@ -2424,7 +2424,7 @@ "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -2468,7 +2468,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.24" }, "funding": [ { @@ -2480,7 +2480,7 @@ "type": "github" } ], - "time": "2022-08-22T14:01:36+00:00" + "time": "2022-08-30T07:42:16+00:00" }, { "name": "psr/cache", @@ -2901,16 +2901,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -2963,7 +2963,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -2971,7 +2971,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -3161,16 +3161,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -3226,7 +3226,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -3234,7 +3234,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -3589,16 +3589,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { @@ -3610,7 +3610,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -3633,7 +3633,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" }, "funding": [ { @@ -3641,7 +3641,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", @@ -3826,16 +3826,16 @@ }, { "name": "symfony/console", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10" + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/535846c7ee6bc4dd027ca0d93220601456734b10", - "reference": "535846c7ee6bc4dd027ca0d93220601456734b10", + "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", "shasum": "" }, "require": { @@ -3905,7 +3905,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.11" + "source": "https://github.com/symfony/console/tree/v5.4.12" }, "funding": [ { @@ -3921,7 +3921,7 @@ "type": "tidelift" } ], - "time": "2022-07-22T10:42:43+00:00" + "time": "2022-08-17T13:18:05+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4156,16 +4156,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd" + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6699fb0228d1bc35b12aed6dd5e7455457609ddd", - "reference": "6699fb0228d1bc35b12aed6dd5e7455457609ddd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d67c1f9a1937406a9be3171b4b22250c0a11447", + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447", "shasum": "" }, "require": { @@ -4200,7 +4200,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.11" + "source": "https://github.com/symfony/filesystem/tree/v5.4.12" }, "funding": [ { @@ -4216,7 +4216,7 @@ "type": "tidelift" } ], - "time": "2022-07-20T13:00:38+00:00" + "time": "2022-08-02T13:48:16+00:00" }, { "name": "symfony/finder", @@ -5047,16 +5047,16 @@ }, { "name": "symfony/string", - "version": "v5.4.11", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322" + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5eb661e49ad389e4ae2b6e4df8d783a8a6548322", - "reference": "5eb661e49ad389e4ae2b6e4df8d783a8a6548322", + "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", "shasum": "" }, "require": { @@ -5113,7 +5113,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.11" + "source": "https://github.com/symfony/string/tree/v5.4.12" }, "funding": [ { @@ -5129,7 +5129,7 @@ "type": "tidelift" } ], - "time": "2022-07-24T16:15:25+00:00" + "time": "2022-08-12T17:03:11+00:00" }, { "name": "tecnickcom/tcpdf", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3e2ccfc1..b0271f35 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2912,7 +2912,7 @@ parameters: - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index 0ac6b669..2e524db5 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -240,7 +240,7 @@ class Value * * @param null|mixed $value The value you want converted * - * @return number N converts values listed in the following table + * @return number|string N converts values listed in the following table * If value is or refers to N returns * A number That number value * A date The Excel serialized number of that date diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index bc02fbf0..d4d793d7 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -313,8 +313,6 @@ class Cell * Set old calculated value (cached). * * @param mixed $originalValue Value - * - * @return Cell */ public function setCalculatedValue($originalValue): self { @@ -352,8 +350,6 @@ class Cell * Set cell data type. * * @param string $dataType see DataType::TYPE_* - * - * @return Cell */ public function setDataType($dataType): self { @@ -447,8 +443,6 @@ class Cell /** * Set Hyperlink. - * - * @return Cell */ public function setHyperlink(?Hyperlink $hyperlink = null): self { @@ -556,8 +550,6 @@ class Cell /** * Re-bind parent. - * - * @return Cell */ public function rebindParent(Worksheet $parent): self { @@ -650,8 +642,6 @@ class Cell /** * Set index to cellXf. - * - * @return Cell */ public function setXfIndex(int $indexValue): self { diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index 9ab0c98f..922be803 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -387,8 +387,6 @@ class Color extends Supervisor * @param int $colorIndex Index entry point into the colour array * @param bool $background Flag to indicate whether default background or foreground colour * should be returned if the indexed colour doesn't exist - * - * @return Color */ public static function indexedColor($colorIndex, $background = false, ?array $palette = null): self { diff --git a/src/PhpSpreadsheet/Worksheet/CellIterator.php b/src/PhpSpreadsheet/Worksheet/CellIterator.php index 17286f9c..94877f66 100644 --- a/src/PhpSpreadsheet/Worksheet/CellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/CellIterator.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Cells; /** * @template TKey + * * @implements Iterator */ abstract class CellIterator implements Iterator diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index eadf0083..69457357 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -732,7 +732,6 @@ class Xls extends BaseWriter } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length // Null-terminated string $dataProp['data']['data'] .= chr(0); - // @phpstan-ignore-next-line ++$dataProp['data']['length']; // Complete the string with null string for being a %4 $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4)); @@ -750,7 +749,6 @@ class Xls extends BaseWriter } else { $dataSection_Content .= $dataProp['data']['data']; - // @phpstan-ignore-next-line $dataSection_Content_Offset += 4 + $dataProp['data']['length']; } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 847d772a..78fda517 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2405,10 +2405,12 @@ class Worksheet extends BIFFwriter for ($i = 0; $i < $width; ++$i) { /** @phpstan-ignore-next-line */ $color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); - foreach (['red', 'green', 'blue'] as $key) { - $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); + if ($color !== false) { + foreach (['red', 'green', 'blue'] as $key) { + $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); + } + $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } - $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } if (3 * $width % 4) { $data .= str_repeat("\x00", 4 - 3 * $width % 4); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php index ccd689b1..6c91f3fc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php @@ -11,6 +11,7 @@ class FormulaTextTest extends AllSetupTeardown { /** * @param mixed $value + * * @dataProvider providerFormulaText */ public function testFormulaText(string $expectedResult, $value): void diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index 2195155f..384011e8 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -9,7 +9,9 @@ class SampleTest extends TestCase { /** * @runInSeparateProcess + * * @preserveGlobalState disabled + * * @dataProvider providerSample */ public function testSample(string $sample): void diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 17de5c32..30da1d75 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -31,6 +31,7 @@ class WorksheetTest extends TestCase /** * @param string $title * @param string $expectMessage + * * @dataProvider setTitleInvalidProvider */ public function testSetTitleInvalid($title, $expectMessage): void @@ -89,6 +90,7 @@ class WorksheetTest extends TestCase /** * @param string $codeName * @param string $expectMessage + * * @dataProvider setCodeNameInvalidProvider */ public function testSetCodeNameInvalid($codeName, $expectMessage): void @@ -153,6 +155,7 @@ class WorksheetTest extends TestCase * @param string $expectTitle * @param string $expectCell * @param string $expectCell2 + * * @dataProvider extractSheetTitleProvider */ public function testExtractSheetTitle($range, $expectTitle, $expectCell, $expectCell2): void