From 3a4da89bfe1450c69adfa2d35fcd724c4c11aa95 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 11 Mar 2022 11:24:48 -0500 Subject: [PATCH 01/39] allow psr/simple-cache 2.x --- CHANGELOG.md | 1 + composer.json | 2 +- composer.lock | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 030efbf5..b3b9caad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is determined by the Calculation Engine locale setting. (i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.) +- Allow `psr/simple-cache` 2.x ### Deprecated diff --git a/composer.json b/composer.json index c0664de3..a0064df2 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,7 @@ "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0 || ^2.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", diff --git a/composer.lock b/composer.lock index 5434aa3a..7b76a2c4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9dadb265f548dd4ddbfd2ee97d5a6aa6", + "content-hash": "ed42c40a4281d97171980367f24d0a25", "packages": [ { "name": "ezyang/htmlpurifier", From 762300da4748836e133a9377cbff9812439ebd9c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 12 Mar 2022 20:54:48 +0100 Subject: [PATCH 02/39] Ensure that some CF style is displayed in Xls samples, even while background colour is still a problem, so that it's easy to verify that the rule is beng written correctly --- .../01_Basic_Comparisons.php | 46 ++++++++++++++++++- .../02_Text_Comparisons.php | 3 ++ .../03_Blank_Comparisons.php | 2 + .../04_Error_Comparisons.php | 2 + .../05_Date_Comparisons.php | 3 +- .../06_Duplicate_Comparisons.php | 10 ++-- .../07_Expression_Comparisons.php | 10 ++-- 7 files changed, 65 insertions(+), 11 deletions(-) diff --git a/samples/ConditionalFormatting/01_Basic_Comparisons.php b/samples/ConditionalFormatting/01_Basic_Comparisons.php index b201005b..5c5381ae 100644 --- a/samples/ConditionalFormatting/01_Basic_Comparisons.php +++ b/samples/ConditionalFormatting/01_Basic_Comparisons.php @@ -30,7 +30,8 @@ $spreadsheet->getActiveSheet() ->setCellValue('A1', 'Literal Value Comparison') ->setCellValue('A9', 'Value Comparison with Absolute Cell Reference $H$9') ->setCellValue('A17', 'Value Comparison with Relative Cell References') - ->setCellValue('A23', 'Value Comparison with Formula based on AVERAGE() ± STDEV()'); + ->setCellValue('A23', 'Value Comparison with Formula based on AVERAGE() ± STDEV()') + ->setCellValue('A30', 'Literal String Value Comparison'); $dataArray = [ [-2, -1, 0, 1, 2], @@ -45,11 +46,18 @@ $betweenDataArray = [ [4, 3, 8], ]; +$stringArray = [ + ['I'], + ['Love'], + ['PHP'], +]; + $spreadsheet->getActiveSheet() ->fromArray($dataArray, null, 'A2', true) ->fromArray($dataArray, null, 'A10', true) ->fromArray($betweenDataArray, null, 'A18', true) ->fromArray($dataArray, null, 'A24', true) + ->fromArray($stringArray, null, 'A31', true) ->setCellValue('H9', 1); // Set title row bold @@ -58,21 +66,31 @@ $spreadsheet->getActiveSheet()->getStyle('A1:E1')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A9:E9')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A17:E17')->getFont()->setBold(true); $spreadsheet->getActiveSheet()->getStyle('A23:E23')->getFont()->setBold(true); +$spreadsheet->getActiveSheet()->getStyle('A30:E30')->getFont()->setBold(true); // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFill() ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFill() ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB(Color::COLOR_RED); +$redStyle->getFill() ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); @@ -166,6 +184,32 @@ $cellWizard->lessThan('AVERAGE(' . $formulaRange . ')-STDEV(' . $formulaRange . ->setStyle($redStyle); $conditionalStyles[] = $cellWizard->getConditional(); +$spreadsheet->getActiveSheet() + ->getStyle($cellWizard->getCellRange()) + ->setConditionalStyles($conditionalStyles); + +// Set rules for Value Comparison with String Literal +$cellRange = 'A31:A33'; +$formulaRange = implode( + ':', + array_map( + [Coordinate::class, 'absoluteCoordinate'], + Coordinate::splitRange($cellRange)[0] + ) +); +$conditionalStyles = []; +$wizardFactory = new Wizard($cellRange); +/** @var Wizard\CellValue $cellWizard */ +$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE); + +$cellWizard->equals('LOVE') + ->setStyle($redStyle); +$conditionalStyles[] = $cellWizard->getConditional(); + +$cellWizard->equals('PHP') + ->setStyle($greenStyle); +$conditionalStyles[] = $cellWizard->getConditional(); + $spreadsheet->getActiveSheet() ->getStyle($cellWizard->getCellRange()) ->setConditionalStyles($conditionalStyles); diff --git a/samples/ConditionalFormatting/02_Text_Comparisons.php b/samples/ConditionalFormatting/02_Text_Comparisons.php index 53aa84ad..10cd25b8 100644 --- a/samples/ConditionalFormatting/02_Text_Comparisons.php +++ b/samples/ConditionalFormatting/02_Text_Comparisons.php @@ -74,14 +74,17 @@ $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/03_Blank_Comparisons.php b/samples/ConditionalFormatting/03_Blank_Comparisons.php index c8584c3a..33b17ef6 100644 --- a/samples/ConditionalFormatting/03_Blank_Comparisons.php +++ b/samples/ConditionalFormatting/03_Blank_Comparisons.php @@ -46,10 +46,12 @@ $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/04_Error_Comparisons.php b/samples/ConditionalFormatting/04_Error_Comparisons.php index 957569cf..1c9c7b58 100644 --- a/samples/ConditionalFormatting/04_Error_Comparisons.php +++ b/samples/ConditionalFormatting/04_Error_Comparisons.php @@ -49,10 +49,12 @@ $greenStyle = new Style(false, true); $greenStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $redStyle = new Style(false, true); $redStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_RED); +$redStyle->getFont()->setColor(new Color(Color::COLOR_GREEN)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/05_Date_Comparisons.php b/samples/ConditionalFormatting/05_Date_Comparisons.php index ef9ad405..0834939d 100644 --- a/samples/ConditionalFormatting/05_Date_Comparisons.php +++ b/samples/ConditionalFormatting/05_Date_Comparisons.php @@ -108,12 +108,11 @@ $spreadsheet->getActiveSheet()->getStyle('B1:K1')->getAlignment()->setHorizontal // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); - $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); -$yellowStyle->getNumberFormat()->setFormatCode('ddd dd-mmm-yyyy'); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/06_Duplicate_Comparisons.php b/samples/ConditionalFormatting/06_Duplicate_Comparisons.php index 0b94bd47..cbed0eb2 100644 --- a/samples/ConditionalFormatting/06_Duplicate_Comparisons.php +++ b/samples/ConditionalFormatting/06_Duplicate_Comparisons.php @@ -51,14 +51,16 @@ $spreadsheet->getActiveSheet()->getStyle('A1:C1')->getFont()->setBold(true); // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); -$greenStyle = new Style(false, true); -$greenStyle->getFill() - ->setFillType(Fill::FILL_SOLID) - ->getEndColor()->setARGB(Color::COLOR_GREEN); $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); +$greenStyle = new Style(false, true); +$greenStyle->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); // Set conditional formatting rules and styles $helper->log('Define conditional formatting and set styles'); diff --git a/samples/ConditionalFormatting/07_Expression_Comparisons.php b/samples/ConditionalFormatting/07_Expression_Comparisons.php index 6ea16bcc..88d79b23 100644 --- a/samples/ConditionalFormatting/07_Expression_Comparisons.php +++ b/samples/ConditionalFormatting/07_Expression_Comparisons.php @@ -69,14 +69,16 @@ $spreadsheet->getActiveSheet()->getStyle('A25:D26')->getFont()->setBold(true); // Define some styles for our Conditionals $helper->log('Define some styles for our Conditionals'); -$greenStyle = new Style(false, true); -$greenStyle->getFill() - ->setFillType(Fill::FILL_SOLID) - ->getEndColor()->setARGB(Color::COLOR_GREEN); $yellowStyle = new Style(false, true); $yellowStyle->getFill() ->setFillType(Fill::FILL_SOLID) ->getEndColor()->setARGB(Color::COLOR_YELLOW); +$yellowStyle->getFont()->setColor(new Color(Color::COLOR_BLUE)); +$greenStyle = new Style(false, true); +$greenStyle->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(Color::COLOR_GREEN); +$greenStyle->getFont()->setColor(new Color(Color::COLOR_DARKRED)); $greenStyleMoney = clone $greenStyle; $greenStyleMoney->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_ACCOUNTING_USD); From 7e89d3397e0435a773ef81ea21f9b137ea7f5098 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 12 Mar 2022 20:55:37 +0100 Subject: [PATCH 03/39] Resolve problems with writing the CF Header for each group of rules --- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 76 ++++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index fcf7789a..c02e3420 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -539,25 +539,32 @@ class Worksheet extends BIFFwriter $this->writeSheetProtection(); $this->writeRangeProtection(); - $arrConditionalStyles = $phpSheet->getConditionalStylesCollection(); + // Write Conditional Formatting Rules and Styles + $this->writeConditionalFormatting(); + + $this->storeEof(); + } + + private function writeConditionalFormatting(): void + { + $arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection(); if (!empty($arrConditionalStyles)) { $arrConditional = []; - $cfHeaderWritten = false; // Write ConditionalFormattingTable records foreach ($arrConditionalStyles as $cellCoordinate => $conditionalStyles) { + $cfHeaderWritten = false; foreach ($conditionalStyles as $conditional) { /** @var Conditional $conditional */ if ( - $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION || - $conditional->getConditionType() == Conditional::CONDITION_CELLIS + $conditional->getConditionType() === Conditional::CONDITION_EXPRESSION || + $conditional->getConditionType() === Conditional::CONDITION_CELLIS ) { // Write CFHEADER record (only if there are Conditional Styles that we are able to write) if ($cfHeaderWritten === false) { - $this->writeCFHeader(); - $cfHeaderWritten = true; + $cfHeaderWritten = $this->writeCFHeader($cellCoordinate, $conditionalStyles); } - if (!isset($arrConditional[$conditional->getHashCode()])) { + if ($cfHeaderWritten === true && !isset($arrConditional[$conditional->getHashCode()])) { // This hash code has been handled $arrConditional[$conditional->getHashCode()] = true; @@ -568,8 +575,6 @@ class Worksheet extends BIFFwriter } } } - - $this->storeEof(); } /** @@ -3127,8 +3132,10 @@ class Worksheet extends BIFFwriter /** * Write CFHeader record. + * + * @param Conditional[] $conditionalStyles */ - private function writeCFHeader(): void + private function writeCFHeader(string $cellCoordinate, array $conditionalStyles): bool { $record = 0x01B0; // Record identifier $length = 0x0016; // Bytes to follow @@ -3137,33 +3144,32 @@ class Worksheet extends BIFFwriter $numColumnMax = null; $numRowMin = null; $numRowMax = null; + $arrConditional = []; - foreach ($this->phpSheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { - foreach ($conditionalStyles as $conditional) { - if ( - $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION || - $conditional->getConditionType() == Conditional::CONDITION_CELLIS - ) { - if (!in_array($conditional->getHashCode(), $arrConditional)) { - $arrConditional[] = $conditional->getHashCode(); - } - // Cells - $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate); - if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) { - $numColumnMin = $rangeCoordinates[0][0]; - } - if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) { - $numColumnMax = $rangeCoordinates[1][0]; - } - if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) { - $numRowMin = (int) $rangeCoordinates[0][1]; - } - if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) { - $numRowMax = (int) $rangeCoordinates[1][1]; - } - } + foreach ($conditionalStyles as $conditional) { + if (!in_array($conditional->getHashCode(), $arrConditional)) { + $arrConditional[] = $conditional->getHashCode(); + } + // Cells + $rangeCoordinates = Coordinate::rangeBoundaries($cellCoordinate); + if ($numColumnMin === null || ($numColumnMin > $rangeCoordinates[0][0])) { + $numColumnMin = $rangeCoordinates[0][0]; + } + if ($numColumnMax === null || ($numColumnMax < $rangeCoordinates[1][0])) { + $numColumnMax = $rangeCoordinates[1][0]; + } + if ($numRowMin === null || ($numRowMin > $rangeCoordinates[0][1])) { + $numRowMin = (int) $rangeCoordinates[0][1]; + } + if ($numRowMax === null || ($numRowMax < $rangeCoordinates[1][1])) { + $numRowMax = (int) $rangeCoordinates[1][1]; } } + + if (count($arrConditional) === 0) { + return false; + } + $needRedraw = 1; $cellRange = pack('vvvv', $numRowMin - 1, $numRowMax - 1, $numColumnMin - 1, $numColumnMax - 1); @@ -3173,6 +3179,8 @@ class Worksheet extends BIFFwriter $data .= pack('v', 0x0001); $data .= $cellRange; $this->append($header . $data); + + return true; } private function getDataBlockProtection(Conditional $conditional): int From 9ca9d741fecd8545c39a0c3b3dfaaa00432855c5 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 03:04:37 +0100 Subject: [PATCH 04/39] Resolve saving cell references, string literals and formula as values for conditional formatting rules in the Xls file save The code is ugly as sin; but it works... I'll do some refactoring and cleaning later (once I've had some sleep, because I'm stupidly still working on this at 3am) The main remaining issue is formulae that can't be parsed in BIFF8 files; e.g. formulae that use functions that aren't available. In this case, all CF in that worksheet is corrupted, and the file errors when opened, so it is a serious issue. The ISODD()/ISEVEN example in 07_Expression_Comparisons.php uses these, so I've temporarily commented out setting that CF range until I've solved that problem. There are other limitations listed in the BIFF documentation; but they're harder to detect. I've also left a couple of debug statements in the code to display these formula errors: I'll remove them once I've resolved the issue. --- .../07_Expression_Comparisons.php | 7 +- .../Wizard/WizardAbstract.php | 2 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 69 +++++++++++++++---- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/samples/ConditionalFormatting/07_Expression_Comparisons.php b/samples/ConditionalFormatting/07_Expression_Comparisons.php index 88d79b23..43d6fe56 100644 --- a/samples/ConditionalFormatting/07_Expression_Comparisons.php +++ b/samples/ConditionalFormatting/07_Expression_Comparisons.php @@ -28,6 +28,7 @@ $helper->log('Add data'); $spreadsheet->setActiveSheetIndex(0); $spreadsheet->getActiveSheet() ->setCellValue('A1', 'Odd/Even Expression Comparison') + ->setCellValue('A4', 'Note that these functions are not available for Xls files') ->setCellValue('A15', 'Sales Grid Expression Comparison') ->setCellValue('A25', 'Sales Grid Multiple Expression Comparison'); @@ -101,9 +102,9 @@ $expressionWizard->expression('ISEVEN(A1)') ->setStyle($yellowStyle); $conditionalStyles[] = $expressionWizard->getConditional(); -$spreadsheet->getActiveSheet() - ->getStyle($expressionWizard->getCellRange()) - ->setConditionalStyles($conditionalStyles); +//$spreadsheet->getActiveSheet() +// ->getStyle($expressionWizard->getCellRange()) +// ->setConditionalStyles($conditionalStyles); // Set rules for Sales Grid Row match against Country Comparison $cellRange = 'A17:D22'; diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php index 75d6856e..df9daab3 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php @@ -122,7 +122,7 @@ abstract class WizardAbstract return "{$worksheet}{$column}{$row}"; } - protected static function reverseAdjustCellRef(string $condition, string $cellRange): string + public static function reverseAdjustCellRef(string $condition, string $cellRange): string { $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellRange))); [$referenceCell] = $conditionalRange[0]; diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index c02e3420..ecfc1a6a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -13,6 +13,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Xls; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; @@ -569,7 +570,7 @@ class Worksheet extends BIFFwriter $arrConditional[$conditional->getHashCode()] = true; // Write CFRULE record - $this->writeCFRule($conditional); + $this->writeCFRule($conditional, $cellCoordinate); } } } @@ -2779,7 +2780,7 @@ class Worksheet extends BIFFwriter /** * Write CFRule Record. */ - private function writeCFRule(Conditional $conditional): void + private function writeCFRule(Conditional $conditional, string $cellRange): void { $record = 0x01B1; // Record identifier $type = null; // Type of the CF @@ -2832,21 +2833,59 @@ class Worksheet extends BIFFwriter // $szValue2 : size of the formula data for second value or formula $arrConditions = $conditional->getConditions(); $numConditions = count($arrConditions); + + $szValue1 = 0x0000; + $szValue2 = 0x0000; + $operand1 = null; + $operand2 = null; + if ($numConditions == 1) { - $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); - $szValue2 = 0x0000; - $operand1 = pack('Cv', 0x1E, $arrConditions[0]); - $operand2 = null; + if (is_int($arrConditions[0]) || is_float($arrConditions[0])) { + $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); + $operand1 = pack('Cv', 0x1E, $arrConditions[0]); + } else { + try { + $formula1 = Wizard\WizardAbstract::reverseAdjustCellRef((string) $arrConditions[0], $cellRange); + $this->parser->parse($formula1); + $formula1 = $this->parser->toReversePolish(); + $szValue1 = strlen($formula1); + } catch (PhpSpreadsheetException $e) { + var_dump("PARSER EXCEPTION: {$e->getMessage()}"); + $formula1 = null; + } + $operand1 = $formula1; + } } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) { - $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); - $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000); - $operand1 = pack('Cv', 0x1E, $arrConditions[0]); - $operand2 = pack('Cv', 0x1E, $arrConditions[1]); - } else { - $szValue1 = 0x0000; - $szValue2 = 0x0000; - $operand1 = null; - $operand2 = null; + if (is_int($arrConditions[0]) || is_float($arrConditions[0])) { + $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); + $operand1 = pack('Cv', 0x1E, $arrConditions[0]); + } else { + try { + $formula1 = Wizard\WizardAbstract::reverseAdjustCellRef((string) $arrConditions[0], $cellRange); + $this->parser->parse($formula1); + $formula1 = $this->parser->toReversePolish(); + $szValue1 = strlen($formula1); + } catch (PhpSpreadsheetException $e) { + var_dump("PARSER EXCEPTION: {$e->getMessage()}"); + $formula1 = null; + } + $operand1 = $formula1; + } + if (is_int($arrConditions[1]) || is_float($arrConditions[1])) { + $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000); + $operand2 = pack('Cv', 0x1E, $arrConditions[1]); + } else { + try { + $formula2 = Wizard\WizardAbstract::reverseAdjustCellRef((string) $arrConditions[1], $cellRange); + $this->parser->parse($formula2); + $formula2 = $this->parser->toReversePolish(); + $szValue2 = strlen($formula2); + } catch (PhpSpreadsheetException $e) { + var_dump("PARSER EXCEPTION: {$e->getMessage()}"); + $formula2 = null; + } + $operand2 = $formula2; + } } // $flags : Option flags From 83161de91e24570930eebb7e266ebcccd2aff717 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 04:02:05 +0100 Subject: [PATCH 05/39] Some refactoring to extract the logic for calculating the CF operand tokens and size, to reduce duplicated code --- .../Writer/Xls/ConditionalHelper.php | 76 +++++++++++++++++++ src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 70 +++++------------ 2 files changed, 95 insertions(+), 51 deletions(-) create mode 100644 src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php diff --git a/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php new file mode 100644 index 00000000..f408692a --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php @@ -0,0 +1,76 @@ +parser = $parser; + } + + /** + * @param mixed $condition + */ + public function processCondition($condition, string $cellRange): void + { + $this->condition = $condition; + $this->cellRange = $cellRange; + + if (is_int($condition) || is_float($condition)) { + $this->size = ($condition <= 65535 ? 3 : 0x0000); + $this->tokens = pack('Cv', 0x1E, $condition); + } else { + try { + $formula = Wizard\WizardAbstract::reverseAdjustCellRef((string) $condition, $cellRange); + $this->parser->parse($formula); + $this->tokens = $this->parser->toReversePolish(); + $this->size = strlen($this->tokens); + } catch (PhpSpreadsheetException $e) { + var_dump("PARSER EXCEPTION: {$e->getMessage()}"); + $this->tokens = null; + $this->size = 0; + } + } + } + + public function tokens(): ?string + { + return $this->tokens; + } + + public function size(): int + { + return $this->size; + } +} diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index ecfc1a6a..b77719bd 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -13,7 +13,6 @@ use PhpOffice\PhpSpreadsheet\Shared\Xls; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Conditional; -use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\SheetView; @@ -548,6 +547,8 @@ class Worksheet extends BIFFwriter private function writeConditionalFormatting(): void { + $conditionalFormulaHelper = new ConditionalHelper($this->parser); + $arrConditionalStyles = $this->phpSheet->getConditionalStylesCollection(); if (!empty($arrConditionalStyles)) { $arrConditional = []; @@ -570,7 +571,7 @@ class Worksheet extends BIFFwriter $arrConditional[$conditional->getHashCode()] = true; // Write CFRULE record - $this->writeCFRule($conditional, $cellCoordinate); + $this->writeCFRule($conditionalFormulaHelper, $conditional, $cellCoordinate); } } } @@ -2780,8 +2781,11 @@ class Worksheet extends BIFFwriter /** * Write CFRule Record. */ - private function writeCFRule(Conditional $conditional, string $cellRange): void - { + private function writeCFRule( + ConditionalHelper $conditionalFormulaHelper, + Conditional $conditional, + string $cellRange + ): void { $record = 0x01B1; // Record identifier $type = null; // Type of the CF $operatorType = null; // Comparison operator @@ -2839,53 +2843,17 @@ class Worksheet extends BIFFwriter $operand1 = null; $operand2 = null; - if ($numConditions == 1) { - if (is_int($arrConditions[0]) || is_float($arrConditions[0])) { - $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); - $operand1 = pack('Cv', 0x1E, $arrConditions[0]); - } else { - try { - $formula1 = Wizard\WizardAbstract::reverseAdjustCellRef((string) $arrConditions[0], $cellRange); - $this->parser->parse($formula1); - $formula1 = $this->parser->toReversePolish(); - $szValue1 = strlen($formula1); - } catch (PhpSpreadsheetException $e) { - var_dump("PARSER EXCEPTION: {$e->getMessage()}"); - $formula1 = null; - } - $operand1 = $formula1; - } - } elseif ($numConditions == 2 && ($conditional->getOperatorType() == Conditional::OPERATOR_BETWEEN)) { - if (is_int($arrConditions[0]) || is_float($arrConditions[0])) { - $szValue1 = ($arrConditions[0] <= 65535 ? 3 : 0x0000); - $operand1 = pack('Cv', 0x1E, $arrConditions[0]); - } else { - try { - $formula1 = Wizard\WizardAbstract::reverseAdjustCellRef((string) $arrConditions[0], $cellRange); - $this->parser->parse($formula1); - $formula1 = $this->parser->toReversePolish(); - $szValue1 = strlen($formula1); - } catch (PhpSpreadsheetException $e) { - var_dump("PARSER EXCEPTION: {$e->getMessage()}"); - $formula1 = null; - } - $operand1 = $formula1; - } - if (is_int($arrConditions[1]) || is_float($arrConditions[1])) { - $szValue2 = ($arrConditions[1] <= 65535 ? 3 : 0x0000); - $operand2 = pack('Cv', 0x1E, $arrConditions[1]); - } else { - try { - $formula2 = Wizard\WizardAbstract::reverseAdjustCellRef((string) $arrConditions[1], $cellRange); - $this->parser->parse($formula2); - $formula2 = $this->parser->toReversePolish(); - $szValue2 = strlen($formula2); - } catch (PhpSpreadsheetException $e) { - var_dump("PARSER EXCEPTION: {$e->getMessage()}"); - $formula2 = null; - } - $operand2 = $formula2; - } + if ($numConditions === 1) { + $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange); + $szValue1 = $conditionalFormulaHelper->size(); + $operand1 = $conditionalFormulaHelper->tokens(); + } elseif ($numConditions === 2 && ($conditional->getOperatorType() === Conditional::OPERATOR_BETWEEN)) { + $conditionalFormulaHelper->processCondition($arrConditions[0], $cellRange); + $szValue1 = $conditionalFormulaHelper->size(); + $operand1 = $conditionalFormulaHelper->tokens(); + $conditionalFormulaHelper->processCondition($arrConditions[1], $cellRange); + $szValue2 = $conditionalFormulaHelper->size(); + $operand2 = $conditionalFormulaHelper->tokens(); } // $flags : Option flags From a6e792082b440b073ac2d1d8c6929dd372a690bd Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 10:53:07 +0100 Subject: [PATCH 06/39] Handle the case of an invalid formula by defaulting to ptgInt + 0, which will avoid breaking the file --- .../ConditionalFormatting/07_Expression_Comparisons.php | 6 +++--- src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php | 8 ++++---- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/samples/ConditionalFormatting/07_Expression_Comparisons.php b/samples/ConditionalFormatting/07_Expression_Comparisons.php index 43d6fe56..0f2b1a84 100644 --- a/samples/ConditionalFormatting/07_Expression_Comparisons.php +++ b/samples/ConditionalFormatting/07_Expression_Comparisons.php @@ -102,9 +102,9 @@ $expressionWizard->expression('ISEVEN(A1)') ->setStyle($yellowStyle); $conditionalStyles[] = $expressionWizard->getConditional(); -//$spreadsheet->getActiveSheet() -// ->getStyle($expressionWizard->getCellRange()) -// ->setConditionalStyles($conditionalStyles); +$spreadsheet->getActiveSheet() + ->getStyle($expressionWizard->getCellRange()) + ->setConditionalStyles($conditionalStyles); // Set rules for Sales Grid Row match against Country Comparison $cellRange = 'A17:D22'; diff --git a/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php index f408692a..0f78b8c5 100644 --- a/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php +++ b/src/PhpSpreadsheet/Writer/Xls/ConditionalHelper.php @@ -55,11 +55,11 @@ class ConditionalHelper $formula = Wizard\WizardAbstract::reverseAdjustCellRef((string) $condition, $cellRange); $this->parser->parse($formula); $this->tokens = $this->parser->toReversePolish(); - $this->size = strlen($this->tokens); + $this->size = strlen($this->tokens ?? ''); } catch (PhpSpreadsheetException $e) { - var_dump("PARSER EXCEPTION: {$e->getMessage()}"); - $this->tokens = null; - $this->size = 0; + // In the event of a parser error with a formula value, we set the expression to ptgInt + 0 + $this->tokens = pack('Cv', 0x1E, 0); + $this->size = 3; } } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index b77719bd..16059e28 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -2787,8 +2787,8 @@ class Worksheet extends BIFFwriter string $cellRange ): void { $record = 0x01B1; // Record identifier - $type = null; // Type of the CF - $operatorType = null; // Comparison operator + $type = null; // Type of the CF + $operatorType = null; // Comparison operator if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { $type = 0x02; From 7101da80a52735b11d8ae11c5339426d53fb6b42 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 11:12:59 +0100 Subject: [PATCH 07/39] Update Change Log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ff894ab..df24ba1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. +- Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. - Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) - Fixed behaviour of XLSX font style vertical align settings. - Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. From 092f90220e9bb9e2c096b18f3626ac74d56043e6 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 12:54:40 +0100 Subject: [PATCH 08/39] Minor refactoring work Extact some processes into their own methods, and eliminate some occrrences of multiple duplicated method calls --- src/PhpSpreadsheet/ReferenceHelper.php | 169 ++++++++++++++----------- 1 file changed, 94 insertions(+), 75 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 98c4807a..ffaed083 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -372,30 +372,12 @@ class ReferenceHelper // 1. Clear column strips if we are removing columns if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) { - for ($i = 1; $i <= $highestRow - 1; ++$i) { - for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } - } - } + $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet); } // 2. Clear row strips if we are removing rows if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) { - for ($i = $beforeColumn - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) { - for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } - } - } + $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet); } // Find missing coordinates. This is important when inserting column before the last column @@ -466,9 +448,10 @@ class ReferenceHelper $highestRow = $worksheet->getHighestRow(); if ($numberOfColumns > 0 && $beforeColumn - 2 > 0) { + $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { // Style - $coordinate = Coordinate::stringFromColumnIndex($beforeColumn - 1) . $i; + $coordinate = $beforeColumnName . $i; if ($worksheet->cellExists($coordinate)) { $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? @@ -488,7 +471,8 @@ class ReferenceHelper } if ($numberOfRows > 0 && $beforeRow - 1 > 0) { - for ($i = $beforeColumn; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); + for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { // Style $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); if ($worksheet->cellExists($coordinate)) { @@ -534,59 +518,7 @@ class ReferenceHelper $this->adjustProtectedCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); // Update worksheet: autofilter - $autoFilter = $worksheet->getAutoFilter(); - $autoFilterRange = $autoFilter->getRange(); - if (!empty($autoFilterRange)) { - if ($numberOfColumns != 0) { - $autoFilterColumns = $autoFilter->getColumns(); - if (count($autoFilterColumns) > 0) { - $column = ''; - $row = 0; - sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); - $columnIndex = Coordinate::columnIndexFromString($column); - [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); - if ($columnIndex <= $rangeEnd[0]) { - if ($numberOfColumns < 0) { - // If we're actually deleting any columns that fall within the autofilter range, - // then we delete any rules for those columns - $deleteColumn = $columnIndex + $numberOfColumns - 1; - $deleteCount = abs($numberOfColumns); - for ($i = 1; $i <= $deleteCount; ++$i) { - if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { - $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); - } - ++$deleteColumn; - } - } - $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; - - // Shuffle columns in autofilter range - if ($numberOfColumns > 0) { - $startColRef = $startCol; - $endColRef = $rangeEnd[0]; - $toColRef = $rangeEnd[0] + $numberOfColumns; - - do { - $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); - --$endColRef; - --$toColRef; - } while ($startColRef <= $endColRef); - } else { - // For delete, we shuffle from beginning to end to avoid overwriting - $startColID = Coordinate::stringFromColumnIndex($startCol); - $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); - $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); - do { - $autoFilter->shiftColumn($startColID, $toColID); - ++$startColID; - ++$toColID; - } while ($startColID != $endColID); - } - } - } - } - $worksheet->setAutoFilter($this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows)); - } + $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); // Update worksheet: freeze pane if ($worksheet->getFreezePane()) { @@ -1038,6 +970,93 @@ class ReferenceHelper return $newColumn . $newRow; } + private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void + { + for ($i = 1; $i <= $highestRow - 1; ++$i) { + for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { + $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; + $worksheet->removeConditionalStyles($coordinate); + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } + } + } + + private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void + { + $lastColumnIndex = Coordinate::columnIndexFromString($highestColumn) - 1; + + for ($i = $beforeColumn - 1; $i <= $lastColumnIndex; ++$i) { + for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { + $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; + $worksheet->removeConditionalStyles($coordinate); + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } + } + } + + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + $autoFilter = $worksheet->getAutoFilter(); + $autoFilterRange = $autoFilter->getRange(); + if (!empty($autoFilterRange)) { + if ($numberOfColumns != 0) { + $autoFilterColumns = $autoFilter->getColumns(); + if (count($autoFilterColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString($column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + // If we're actually deleting any columns that fall within the autofilter range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + for ($i = 1; $i <= $deleteCount; ++$i) { + if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { + $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); + } + ++$deleteColumn; + } + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in autofilter range + if ($numberOfColumns > 0) { + $startColRef = $startCol; + $endColRef = $rangeEnd[0]; + $toColRef = $rangeEnd[0] + $numberOfColumns; + + do { + $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } else { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); + do { + $autoFilter->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID != $endColID); + } + } + } + } + $worksheet->setAutoFilter($this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows)); + } + } + /** * __clone implementation. Cloning should not be allowed in a Singleton! */ From a8d9cd700e9d8a441670c3e918e8c6b1cfe19b45 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 18:10:28 +0100 Subject: [PATCH 09/39] Allow deleting of columns beyond the end of data in a worksheet to update references (print area, etc) before early exiting the method --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 4c7c7e60..d4e1e928 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2219,16 +2219,18 @@ class Worksheet implements IComparable $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); $pColumnIndex = Coordinate::columnIndexFromString($column); - if ($pColumnIndex > $highestColumnIndex) { - return $this; - } - $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns); $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns); $objReferenceHelper = ReferenceHelper::getInstance(); $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this); + $this->columnDimensions = $holdColumnDimensions; + + if ($pColumnIndex > $highestColumnIndex) { + return $this; + } + $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1; for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) { @@ -2236,8 +2238,6 @@ class Worksheet implements IComparable $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1); } - $this->columnDimensions = $holdColumnDimensions; - $this->garbageCollect(); return $this; From 750e42c02458028845816a07386b564822132495 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 13 Mar 2022 18:12:42 +0100 Subject: [PATCH 10/39] More refactoring of the reference helper; and unit tests to validate insertion/removal of rows/columns from an autofilter range --- src/PhpSpreadsheet/ReferenceHelper.php | 186 +++++++++++------- .../Worksheet/AutoFilter/AutoFilterTest.php | 58 ++++++ 2 files changed, 170 insertions(+), 74 deletions(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index ffaed083..baac9b69 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class ReferenceHelper @@ -358,8 +359,12 @@ class ReferenceHelper * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) * @param Worksheet $worksheet The worksheet that we're editing */ - public function insertNewBefore($beforeCellAddress, $numberOfColumns, $numberOfRows, Worksheet $worksheet): void - { + public function insertNewBefore( + string $beforeCellAddress, + int $numberOfColumns, + int $numberOfRows, + Worksheet $worksheet + ): void { $remove = ($numberOfColumns < 0 || $numberOfRows < 0); $allCoordinates = $worksheet->getCoordinates(); @@ -448,49 +453,11 @@ class ReferenceHelper $highestRow = $worksheet->getHighestRow(); if ($numberOfColumns > 0 && $beforeColumn - 2 > 0) { - $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); - for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { - // Style - $coordinate = $beforeColumnName . $i; - if ($worksheet->cellExists($coordinate)) { - $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { - $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned); - } - } - } - } + $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns); } if ($numberOfRows > 0 && $beforeRow - 1 > 0) { - $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); - for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { - // Style - $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); - if ($worksheet->cellExists($coordinate)) { - $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { - $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned); - } - } - } - } + $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows); } // Update worksheet: column dimensions @@ -533,7 +500,9 @@ class ReferenceHelper // Page setup if ($worksheet->getPageSetup()->isPrintAreaSet()) { - $worksheet->getPageSetup()->setPrintArea($this->updateCellReference($worksheet->getPageSetup()->getPrintArea(), $beforeCellAddress, $numberOfColumns, $numberOfRows)); + $worksheet->getPageSetup()->setPrintArea( + $this->updateCellReference($worksheet->getPageSetup()->getPrintArea(), $beforeCellAddress, $numberOfColumns, $numberOfRows) + ); } // Update worksheet: drawings @@ -880,7 +849,7 @@ class ReferenceHelper foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getCoordinates(false) as $coordinate) { $cell = $sheet->getCell($coordinate); - if (($cell !== null) && ($cell->getDataType() == DataType::TYPE_FORMULA)) { + if (($cell !== null) && ($cell->getDataType() === DataType::TYPE_FORMULA)) { $formula = $cell->getValue(); if (strpos($formula, $oldName) !== false) { $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); @@ -1005,7 +974,7 @@ class ReferenceHelper $autoFilter = $worksheet->getAutoFilter(); $autoFilterRange = $autoFilter->getRange(); if (!empty($autoFilterRange)) { - if ($numberOfColumns != 0) { + if ($numberOfColumns !== 0) { $autoFilterColumns = $autoFilter->getColumns(); if (count($autoFilterColumns) > 0) { $column = ''; @@ -1015,45 +984,114 @@ class ReferenceHelper [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); if ($columnIndex <= $rangeEnd[0]) { if ($numberOfColumns < 0) { - // If we're actually deleting any columns that fall within the autofilter range, - // then we delete any rules for those columns - $deleteColumn = $columnIndex + $numberOfColumns - 1; - $deleteCount = abs($numberOfColumns); - for ($i = 1; $i <= $deleteCount; ++$i) { - if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { - $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); - } - ++$deleteColumn; - } + $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter); } $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; // Shuffle columns in autofilter range if ($numberOfColumns > 0) { - $startColRef = $startCol; - $endColRef = $rangeEnd[0]; - $toColRef = $rangeEnd[0] + $numberOfColumns; - - do { - $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); - --$endColRef; - --$toColRef; - } while ($startColRef <= $endColRef); + $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); } else { - // For delete, we shuffle from beginning to end to avoid overwriting - $startColID = Coordinate::stringFromColumnIndex($startCol); - $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); - $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); - do { - $autoFilter->shiftColumn($startColID, $toColID); - ++$startColID; - ++$toColID; - } while ($startColID != $endColID); + $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); } } } } - $worksheet->setAutoFilter($this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows)); + + $worksheet->setAutoFilter( + $this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows) + ); + } + } + + private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void + { + // If we're actually deleting any columns that fall within the autofilter range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($autoFilterColumns[$columnName])) { + $autoFilter->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $autoFilter->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID !== $endColID); + } + + private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void + { + $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); + for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { + // Style + $coordinate = $beforeColumnName . $i; + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? + $worksheet->getConditionalStyles($coordinate) : false; + for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { + $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); + if ($conditionalStyles) { + $cloned = []; + foreach ($conditionalStyles as $conditionalStyle) { + $cloned[] = clone $conditionalStyle; + } + $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned); + } + } + } + } + } + + private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void + { + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); + for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { + // Style + $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? + $worksheet->getConditionalStyles($coordinate) : false; + for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { + $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + if ($conditionalStyles) { + $cloned = []; + foreach ($conditionalStyles as $conditionalStyle) { + $cloned[] = clone $conditionalStyle; + } + $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned); + } + } + } } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index 28bbbb87..a80d3d63 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -134,6 +134,64 @@ class AutoFilterTest extends SetupTeardown } } + public function testRemoveColumns(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + $autoFilter->getColumn('L')->addRule((new Column\Rule())->setValue(5)); + + $sheet->removeColumn('K', 2); + $result = $autoFilter->getRange(); + self::assertEquals('H2:M256', $result); + + // Check that the rule that was set for column L is no longer set + self::assertEmpty($autoFilter->getColumn('L')->getRule(0)->getValue()); + } + + public function testRemoveRows(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + + $sheet->removeRow(42, 128); + $result = $autoFilter->getRange(); + self::assertEquals('H2:O128', $result); + } + + public function testInsertColumns(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + $autoFilter->getColumn('N')->addRule((new Column\Rule())->setValue(5)); + + $sheet->insertNewColumnBefore('N', 3); + $result = $autoFilter->getRange(); + self::assertEquals('H2:R256', $result); + + // Check that column N no longer has a rule set + self::assertEmpty($autoFilter->getColumn('N')->getRule(0)->getValue()); + // Check that the rule originally set in column N has been moved to column Q + self::assertSame(5, $autoFilter->getColumn('Q')->getRule(0)->getValue()); + } + + public function testInsertRows(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange(self::INITIAL_RANGE); + + $sheet->insertNewRowBefore(3, 4); + $result = $autoFilter->getRange(); + self::assertEquals('H2:O260', $result); + } + public function testGetInvalidColumnOffset(): void { $this->expectException(PhpSpreadsheetException::class); From cb5a451aafe9317b7065b6587c399e23c3f71c11 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 15 Mar 2022 13:25:23 +0100 Subject: [PATCH 11/39] Initial work on Reading Conditional Formatting from Xls files --- src/PhpSpreadsheet/Reader/Xls.php | 135 ++++++++++++++++++ .../Reader/Xls/ConditionalFormatting.php | 49 +++++++ .../Reader/Xls/ConditionalFormattingTest.php | 30 ++++ .../data/Reader/XLS/CF_Basic_Comparisons.xls | Bin 0 -> 9216 bytes 4 files changed, 214 insertions(+) create mode 100644 src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php create mode 100644 tests/data/Reader/XLS/CF_Basic_Comparisons.xls diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 0fd05c87..cc7ef3bc 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\NamedRange; +use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\CodePage; @@ -142,6 +143,8 @@ class Xls extends BaseReader const XLS_TYPE_SHEETLAYOUT = 0x0862; const XLS_TYPE_XFEXT = 0x087d; const XLS_TYPE_PAGELAYOUTVIEW = 0x088b; + const XLS_TYPE_CFHEADER = 0x01b0; + const XLS_TYPE_CFRULE = 0x01b1; const XLS_TYPE_UNKNOWN = 0xffff; // Encryption type @@ -1031,6 +1034,14 @@ class Xls extends BaseReader case self::XLS_TYPE_DATAVALIDATION: $this->readDataValidation(); + break; + case self::XLS_TYPE_CFHEADER: + $this->readCFHeader(); + + break; + case self::XLS_TYPE_CFRULE: + $this->readCFRule(); + break; case self::XLS_TYPE_SHEETLAYOUT: $this->readSheetLayout(); @@ -7921,4 +7932,128 @@ class Xls extends BaseReader { return $this->mapCellStyleXfIndex; } + + private function readCFHeader(): void + { + var_dump('FOUND CF HEADER'); + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer forward to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; Rule Count + $ruleCount = self::getUInt2d($recordData, 0); + + // offset: var; size: var; cell range address list with + $cellRangeAddressList = ($this->version == self::XLS_BIFF8) + ? $this->readBIFF8CellRangeAddressList(substr($recordData, 12)) + : $this->readBIFF5CellRangeAddressList(substr($recordData, 12)); + $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; + + var_dump($ruleCount, $cellRangeAddresses); + } + + private function readCFRule(): void + { + var_dump('FOUND CF RULE'); + $length = self::getUInt2d($this->data, $this->pos + 2); + $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); + + // move stream pointer forward to next record + $this->pos += 4 + $length; + + if ($this->readDataOnly) { + return; + } + + // offset: 0; size: 2; Options + $cfRule = self::getUInt2d($recordData, 0); + + // bit: 8-15; mask: 0x00FF; type + $type = (0x00FF & $cfRule) >> 0; + $type = ConditionalFormatting::type($type); + + // bit: 0-7; mask: 0xFF00; type + $operator = (0xFF00 & $cfRule) >> 8; + $operator = ConditionalFormatting::operator($operator); + + if ($type === null || $operator === null) { + return; + } + + // offset: 2; size: 2; Size1 + $size1 = self::getUInt2d($recordData, 2); + + // offset: 4; size: 2; Size2 + $size2 = self::getUInt2d($recordData, 4); + + // offset: 6; size: 4; Options + $options = self::getInt4d($recordData, 6); + + $hasFontRecord = (bool) ((0x04000000 & $options) >> 26); + $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27); + $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28); + $hasFillRecord = (bool) ((0x20000000 & $options) >> 29); + $hasProtectionRecord = (bool) ((0x40000000 & $options) >> 30); + + $offset = 12; + + if ($hasFontRecord === true) { + $offset += 118; + } + + if ($hasAlignmentRecord === true) { + $offset += 8; + } + + if ($hasBorderRecord === true) { + $offset += 8; + } + + if ($hasFillRecord === true) { + $offset += 4; + } + + if ($hasProtectionRecord === true) { + $offset += 2; + } + + var_dump($type, $operator); + + if ($size1 > 0) { + $formula1 = $this->readCFFormula($recordData, $offset, $size1); + if ($formula1 === null) { + return; + } + var_dump($formula1); + + $offset += $size1; + } + + if ($size2 > 0) { + $formula2 = $this->readCFFormula($recordData, $offset, $size2); + if ($formula2 === null) { + return; + } + var_dump($formula2); + } + } + + private function readCFFormula(string $recordData, int $offset, int $size): ?string + { + try { + $formula = substr($recordData, $offset, $size); + $formula = pack('v', $size) . $formula; // prepend the length + + return $this->getFormulaFromStructure($formula); + } catch (PhpSpreadsheetException $e) { + } + + return null; + } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php new file mode 100644 index 00000000..8400efb9 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php @@ -0,0 +1,49 @@ + + */ + private static $types = [ + 0x01 => Conditional::CONDITION_CELLIS, + 0x02 => Conditional::CONDITION_EXPRESSION, + ]; + + /** + * @var array + */ + private static $operators = [ + 0x00 => Conditional::OPERATOR_NONE, + 0x01 => Conditional::OPERATOR_BETWEEN, + 0x02 => Conditional::OPERATOR_NOTBETWEEN, + 0x03 => Conditional::OPERATOR_EQUAL, + 0x04 => Conditional::OPERATOR_NOTEQUAL, + 0x05 => Conditional::OPERATOR_GREATERTHAN, + 0x06 => Conditional::OPERATOR_LESSTHAN, + 0x07 => Conditional::OPERATOR_GREATERTHANOREQUAL, + 0x08 => Conditional::OPERATOR_LESSTHANOREQUAL, + ]; + + public static function type(int $type): ?string + { + if (isset(self::$types[$type])) { + return self::$types[$type]; + } + + return null; + } + + public static function operator(int $operator): ?string + { + if (isset(self::$operators[$operator])) { + return self::$operators[$operator]; + } + + return null; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php new file mode 100644 index 00000000..ac0e306f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php @@ -0,0 +1,30 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + public function testReadConditionalFormatting(): void + { + $hasConditionalStyles = $this->sheet->conditionalStylesExists('A2:E5'); + self::assertTrue($hasConditionalStyles); + $onditionalStyles = $this->sheet->getConditionalStyles('A2:E5'); + } +} diff --git a/tests/data/Reader/XLS/CF_Basic_Comparisons.xls b/tests/data/Reader/XLS/CF_Basic_Comparisons.xls new file mode 100644 index 0000000000000000000000000000000000000000..10c91be634e4f1915ff2fc7f7492c8eae30eff51 GIT binary patch literal 9216 zcmeHMZ)jxI6+drglFaNzGuc0xY}V*wU3IOpsDYv_ZPsUH3yjG$wD7kl7^6 zOj?AN#@3~Uwb0gO%M?rTb3q&h5nM2WAN&yUAA*7()>6ceQs@Ukcl!ICGxyD%=j`r8 z8Dt?hx$oY4-o3wb?m6e)d)}RQ-xMylw;^wD(ypSZZHd~21}kCwB=%PVI~R_tXAe%n)ruMD&yHExh&M%-zTqKvoY`a1fR5hG zHRK}Ti#0iS?wp{5Ow>S_z&~1R3Zm$Q0o9O{bTybRH1*Vr_9?Nmi8&K_j$Ec=rfuMP(fUct%<7mj`6O+#?4!$A(ZP$XNyB zUnsxvXls7WF>@eDG#qYK6)B3y_eT$kto4m!VABZtNpS;zpKQH^hVU|9JzclvVlPP#C z(g%~S4CFCjXXQ6~9*gYH+4mXS9ObuD=;^o9!1UW0=)!OJA%^qY{fOoK_J9n;ZAo1V zZUTw9pD@12rWipmabY6`LrP<>B+U+p_I?%%wbL>k#_G~d@aXT*Z=eIsY}d|%vM1$Z zJd&hY4AVZAF;TJ~~}*4T9}}Htt;DNTR3Ejei9(kwOT5!rQ=!# zwRF?BV!<{=x-HV@h1w2)0d+)*5|;cWMi4QIQDY&hFJWQWs2Ha&A+ za>$0m)4@>;XV617oIwxS;k1y=UfiMb7-{yR<;+T?xtgTNombuSCf)lDclF$D4WGjkC)AKr^hQ)KjaPw-@=oAI1mV(F2tX%N)Y}P3NF~-q(ai?_@!$GteQw(R+LpGdI5813d>LJ_Q zxnDbE!`b5@8_pgN*>LuF$PT9^v+0?;zC$*gF%Q{r#yn)h8S{`GP7B$*j%NbB$*eOD zRI|i5&|J+~XI!nry@jX7klstN#q(ka5se62p|%(!{4}*gUeN&J5L&qJ(&rg`C7)@9 zdP*I~__Q5G0l-6aoXZxFb%Z{!mFpGUZ*eCU&|?^rQEvT`Xx8e%X$jKwwMJu-FTZ2r zcR7>$GPkHiXlZyGn|S-({Z&8>zyGz-_`2DGZF;+y6KJSy3`j$ zTEpg&T60cTz)ghh46arWUWKH4Wp9rp?QUnd$%%6qO7j|nlJQ!JSLu524+;7h<$s@A zzLJ3by%VrIU%j+|8Ttjk1?*^{2fJE5_!BlCGU$zNyA!>T!Oh9TdR0>o&Ojgr-Mta0 zyCqdfd-pexWh-@|fk$xpwTx@C5cg_JdVTg}7o%ETMsy#w9~v25NX~9^)~(A?7Vft$ zS86{V??`>P)mz2I=qbGzeNrBT6h%l>Rtm2mc_(=3bO39F94%VF1#$=eokpKkA@Dns z@>;|#B=xY^sq1BFJHXxlnYA1Kl=UOKWWoH8fbWdzfH$|*P3q(BZ^55&ZoWBx{?708 o$KaEPH^X1{VI<2%&2L`r2|O=&pT+wtj${7XpZr$+s?)�hz`B2LJ#7 literal 0 HcmV?d00001 From 6f84780bb90e9bca1ef12139a482a1d135b1fafe Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 15 Mar 2022 20:08:02 +0100 Subject: [PATCH 12/39] Some work on refactoring the ReferenceHelper to extract the logic for updating cell references. This is a preliminary step toward allowing updates to absolute cell references, required to update Conditional Formatting rules. --- phpstan-baseline.neon | 20 -- src/PhpSpreadsheet/CellReferenceHelper.php | 103 +++++++ src/PhpSpreadsheet/ReferenceHelper.php | 292 +++++++------------ tests/data/ReferenceHelperFormulaUpdates.php | 28 ++ 4 files changed, 241 insertions(+), 202 deletions(-) create mode 100644 src/PhpSpreadsheet/CellReferenceHelper.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1ba11328..eb20b3cf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3165,31 +3165,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/ReferenceHelper.php - - - message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\RowDimension\\:\\:setRowIndex\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - - - message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(\\(int\\|string\\), \\(int\\|string\\)\\)\\: int, array\\{'self', 'cellReverseSort'\\} given\\.$#" - count: 4 - path: src/PhpSpreadsheet/ReferenceHelper.php - - - - message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(\\(int\\|string\\), \\(int\\|string\\)\\)\\: int, array\\{'self', 'cellSort'\\} given\\.$#" - count: 4 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" count: 1 path: src/PhpSpreadsheet/ReferenceHelper.php - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\ReferenceHelper\\:\\:\\$instance \\(PhpOffice\\\\PhpSpreadsheet\\\\ReferenceHelper\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/ReferenceHelper.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\RichText\\\\Run\\:\\:\\$font \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/CellReferenceHelper.php b/src/PhpSpreadsheet/CellReferenceHelper.php new file mode 100644 index 00000000..24021109 --- /dev/null +++ b/src/PhpSpreadsheet/CellReferenceHelper.php @@ -0,0 +1,103 @@ +beforeCellAddress = str_replace('$', '', $beforeCellAddress); + $this->numberOfColumns = $numberOfColumns; + $this->numberOfRows = $numberOfRows; + + // Get coordinate of $beforeCellAddress + [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); + $this->beforeColumn = (int) Coordinate::columnIndexFromString($beforeColumn); + $this->beforeRow = (int) $beforeRow; + } + + public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool + { + return $this->beforeCellAddress !== $beforeCellAddress || + $this->numberOfColumns !== $numberOfColumns || + $this->numberOfRows !== $numberOfRows; + } + + public function updateCellReference(string $cellReference = 'A1'): string + { + if (Coordinate::coordinateIsRange($cellReference)) { + throw new Exception('Only single cell references may be passed to this method.'); + } + + // Get coordinate of $cellReference + [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); + $newColumnIndex = (int) Coordinate::columnIndexFromString(str_replace('$', '', $newColumn)); + $newRowIndex = (int) str_replace('$', '', $newRow); + + // Verify which parts should be updated + $updateColumn = (($newColumn[0] !== '$') && $newColumnIndex >= $this->beforeColumn); + $updateRow = (($newRow[0] !== '$') && $newRow >= $this->beforeRow); + + // Create new column reference + if ($updateColumn) { + $newColumn = Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns); + } + + // Create new row reference + if ($updateRow) { + $newRow = $newRowIndex + $this->numberOfRows; + } + + // Return new reference + return "{$newColumn}{$newRow}"; + } + + public function cellAddressInDeleteRange(string $cellAddress): bool + { + [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); + $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); + // Is cell within the range of rows/columns if we're deleting + if ( + $this->numberOfRows < 0 && + ($cellRow >= ($this->beforeRow + $this->numberOfRows)) && + ($cellRow < $this->beforeRow) + ) { + return true; + } elseif ( + $this->numberOfColumns < 0 && + ($cellColumnIndex >= ($this->beforeColumn + $this->numberOfColumns)) && + ($cellColumnIndex < $this->beforeColumn) + ) { + return true; + } + + return false; + } +} diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index baac9b69..8c55c766 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -20,10 +20,15 @@ class ReferenceHelper /** * Instance of this class. * - * @var ReferenceHelper + * @var ?ReferenceHelper */ private static $instance; + /** + * @var CellReferenceHelper + */ + private $cellReferenceHelper; + /** * Get an instance of this class. * @@ -31,7 +36,7 @@ class ReferenceHelper */ public static function getInstance() { - if (!isset(self::$instance) || (self::$instance === null)) { + if (self::$instance === null) { self::$instance = new self(); } @@ -115,67 +120,32 @@ class ReferenceHelper return ($ar < $br) ? 1 : -1; } - /** - * Test whether a cell address falls within a defined range of cells. - * - * @param string $cellAddress Address of the cell we're testing - * @param int $beforeRow Number of the row we're inserting/deleting before - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before - * @param int $numberOfCols Number of columns to insert/delete (negative values indicate deletion) - * - * @return bool - */ - private static function cellAddressInDeleteRange($cellAddress, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfCols) - { - [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); - $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); - // Is cell within the range of rows/columns if we're deleting - if ( - $numberOfRows < 0 && - ($cellRow >= ($beforeRow + $numberOfRows)) && - ($cellRow < $beforeRow) - ) { - return true; - } elseif ( - $numberOfCols < 0 && - ($cellColumnIndex >= ($beforeColumnIndex + $numberOfCols)) && - ($cellColumnIndex < $beforeColumnIndex) - ) { - return true; - } - - return false; - } - /** * Update page breaks when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $beforeRow Number of the row we're inserting/deleting before * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustPageBreaks(Worksheet $worksheet, $beforeCellAddress, $beforeColumnIndex, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustPageBreaks(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aBreaks = $worksheet->getBreaks(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aBreaks, ['self', 'cellReverseSort']) : uksort($aBreaks, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aBreaks, [self::class, 'cellReverseSort']) + : uksort($aBreaks, [self::class, 'cellSort']); - foreach ($aBreaks as $key => $value) { - if (self::cellAddressInDeleteRange($key, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfColumns)) { + foreach ($aBreaks as $cellAddress => $value) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { // If we're deleting, then clear any defined breaks that are within the range // of rows/columns that we're deleting - $worksheet->setBreak($key, Worksheet::BREAK_NONE); + $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE); } else { // Otherwise update any affected breaks by inserting a new break at the appropriate point // and removing the old affected break - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->setBreak($newReference, $value) - ->setBreak($key, Worksheet::BREAK_NONE); + ->setBreak($cellAddress, Worksheet::BREAK_NONE); } } } @@ -185,22 +155,17 @@ class ReferenceHelper * Update cell comments when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $beforeRow Number of the row we're inserting/deleting before - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustComments($worksheet, $beforeCellAddress, $beforeColumnIndex, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustComments($worksheet): void { $aComments = $worksheet->getComments(); $aNewComments = []; // the new array of all comments - foreach ($aComments as $key => &$value) { + foreach ($aComments as $cellAddress => &$value) { // Any comments inside a deleted range will be ignored - if (!self::cellAddressInDeleteRange($key, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfColumns)) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) { // Otherwise build a new array of comments indexed by the adjusted cell reference - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($cellAddress); $aNewComments[$newReference] = $value; } } @@ -212,21 +177,21 @@ class ReferenceHelper * Update hyperlinks when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustHyperlinks($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows): void { $aHyperlinkCollection = $worksheet->getHyperlinkCollection(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aHyperlinkCollection, ['self', 'cellReverseSort']) : uksort($aHyperlinkCollection, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort']) + : uksort($aHyperlinkCollection, [self::class, 'cellSort']); - foreach ($aHyperlinkCollection as $key => $value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + foreach ($aHyperlinkCollection as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->setHyperlink($newReference, $value); - $worksheet->setHyperlink($key, null); + $worksheet->setHyperlink($cellAddress, null); } } } @@ -235,21 +200,21 @@ class ReferenceHelper * Update data validations when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $before Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustDataValidations(Worksheet $worksheet, $before, $numberOfColumns, $numberOfRows): void + protected function adjustDataValidations(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aDataValidationCollection = $worksheet->getDataValidationCollection(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aDataValidationCollection, ['self', 'cellReverseSort']) : uksort($aDataValidationCollection, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) + : uksort($aDataValidationCollection, [self::class, 'cellSort']); - foreach ($aDataValidationCollection as $key => $value) { - $newReference = $this->updateCellReference($key, $before, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + foreach ($aDataValidationCollection as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->setDataValidation($newReference, $value); - $worksheet->setDataValidation($key, null); + $worksheet->setDataValidation($cellAddress, null); } } } @@ -258,16 +223,13 @@ class ReferenceHelper * Update merged cells when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustMergeCells(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustMergeCells(Worksheet $worksheet): void { $aMergeCells = $worksheet->getMergeCells(); $aNewMergeCells = []; // the new array of all merge cells - foreach ($aMergeCells as $key => &$value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); + foreach ($aMergeCells as $cellAddress => &$value) { + $newReference = $this->updateCellReference($cellAddress); $aNewMergeCells[$newReference] = $newReference; } $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array @@ -277,20 +239,20 @@ class ReferenceHelper * Update protected cells when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustProtectedCells(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustProtectedCells(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aProtectedCells = $worksheet->getProtectedCells(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aProtectedCells, ['self', 'cellReverseSort']) : uksort($aProtectedCells, ['self', 'cellSort']); - foreach ($aProtectedCells as $key => $value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aProtectedCells, [self::class, 'cellReverseSort']) + : uksort($aProtectedCells, [self::class, 'cellSort']); + foreach ($aProtectedCells as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->protectCells($newReference, $value, true); - $worksheet->unprotectCells($key); + $worksheet->unprotectCells($cellAddress); } } } @@ -299,18 +261,15 @@ class ReferenceHelper * Update column dimensions when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustColumnDimensions(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustColumnDimensions(Worksheet $worksheet): void { $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true); if (!empty($aColumnDimensions)) { foreach ($aColumnDimensions as $objColumnDimension) { - $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1'); [$newReference] = Coordinate::coordinateFromString($newReference); - if ($objColumnDimension->getColumnIndex() != $newReference) { + if ($objColumnDimension->getColumnIndex() !== $newReference) { $objColumnDimension->setColumnIndex($newReference); } } @@ -322,20 +281,19 @@ class ReferenceHelper * Update row dimensions when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $beforeRow Number of the row we're inserting/deleting before * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustRowDimensions(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustRowDimensions(Worksheet $worksheet, $beforeRow, $numberOfRows): void { $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true); if (!empty($aRowDimensions)) { foreach ($aRowDimensions as $objRowDimension) { - $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex()); [, $newReference] = Coordinate::coordinateFromString($newReference); - if ($objRowDimension->getRowIndex() != $newReference) { - $objRowDimension->setRowIndex($newReference); + $newRoweference = (int) $newReference; + if ($objRowDimension->getRowIndex() !== $newRoweference) { + $objRowDimension->setRowIndex($newRoweference); } } $worksheet->refreshRowDimensions(); @@ -368,6 +326,13 @@ class ReferenceHelper $remove = ($numberOfColumns < 0 || $numberOfRows < 0); $allCoordinates = $worksheet->getCoordinates(); + if ( + $this->cellReferenceHelper === null || + $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + // Get coordinate of $beforeCellAddress [$beforeColumn, $beforeRow] = Coordinate::indexesFromString($beforeCellAddress); @@ -427,7 +392,7 @@ class ReferenceHelper $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); // Insert this cell at its new location - if ($cell->getDataType() == DataType::TYPE_FORMULA) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { // Formula should be adjusted $worksheet->getCell($newCoordinate) ->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle())); @@ -441,7 +406,7 @@ class ReferenceHelper } else { /* We don't need to update styles for rows/columns before our insertion position, but we do still need to adjust any formulae in those cells */ - if ($cell->getDataType() == DataType::TYPE_FORMULA) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { // Formula should be adjusted $cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle())); } @@ -461,39 +426,39 @@ class ReferenceHelper } // Update worksheet: column dimensions - $this->adjustColumnDimensions($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustColumnDimensions($worksheet); // Update worksheet: row dimensions - $this->adjustRowDimensions($worksheet, $beforeCellAddress, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows); // Update worksheet: page breaks - $this->adjustPageBreaks($worksheet, $beforeCellAddress, $beforeColumn, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: comments - $this->adjustComments($worksheet, $beforeCellAddress, $beforeColumn, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustComments($worksheet); // Update worksheet: hyperlinks - $this->adjustHyperlinks($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: data validations - $this->adjustDataValidations($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: merge cells - $this->adjustMergeCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustMergeCells($worksheet); // Update worksheet: protected cells - $this->adjustProtectedCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: autofilter - $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns); // Update worksheet: freeze pane if ($worksheet->getFreezePane()) { $splitCell = $worksheet->getFreezePane() ?? ''; $topLeftCell = $worksheet->getTopLeftCell() ?? ''; - $splitCell = $this->updateCellReference($splitCell, $beforeCellAddress, $numberOfColumns, $numberOfRows); - $topLeftCell = $this->updateCellReference($topLeftCell, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $splitCell = $this->updateCellReference($splitCell); + $topLeftCell = $this->updateCellReference($topLeftCell); $worksheet->freezePane($splitCell, $topLeftCell); } @@ -501,14 +466,14 @@ class ReferenceHelper // Page setup if ($worksheet->getPageSetup()->isPrintAreaSet()) { $worksheet->getPageSetup()->setPrintArea( - $this->updateCellReference($worksheet->getPageSetup()->getPrintArea(), $beforeCellAddress, $numberOfColumns, $numberOfRows) + $this->updateCellReference($worksheet->getPageSetup()->getPrintArea()) ); } // Update worksheet: drawings $aDrawings = $worksheet->getDrawingCollection(); foreach ($aDrawings as $objDrawing) { - $newReference = $this->updateCellReference($objDrawing->getCoordinates(), $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($objDrawing->getCoordinates()); if ($objDrawing->getCoordinates() != $newReference) { $objDrawing->setCoordinates($newReference); } @@ -518,7 +483,7 @@ class ReferenceHelper if (count($worksheet->getParent()->getDefinedNames()) > 0) { foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $definedName->setValue($this->updateCellReference($definedName->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows)); + $definedName->setValue($this->updateCellReference($definedName->getValue())); } } } @@ -540,6 +505,13 @@ class ReferenceHelper */ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0, $worksheetName = '') { + if ( + $this->cellReferenceHelper === null || + $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + // Update cell references in the formula $formulaBlocks = explode('"', $formula); $i = false; @@ -549,13 +521,13 @@ class ReferenceHelper $adjustCount = 0; $newCellTokens = $cellTokens = []; // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference('$A' . $match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows), 2); - $modified4 = substr($this->updateCellReference('$A' . $match[4], $beforeCellAddress, $numberOfColumns, $numberOfRows), 2); + $modified3 = substr($this->updateCellReference('$A' . $match[3]), 2); + $modified4 = substr($this->updateCellReference('$A' . $match[4]), 2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -574,13 +546,13 @@ class ReferenceHelper } } // Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference($match[3] . '$1', $beforeCellAddress, $numberOfColumns, $numberOfRows), 0, -2); - $modified4 = substr($this->updateCellReference($match[4] . '$1', $beforeCellAddress, $numberOfColumns, $numberOfRows), 0, -2); + $modified3 = substr($this->updateCellReference($match[3] . '$1'), 0, -2); + $modified4 = substr($this->updateCellReference($match[4] . '$1'), 0, -2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -599,13 +571,13 @@ class ReferenceHelper } } // Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = $this->updateCellReference($match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows); - $modified4 = $this->updateCellReference($match[4], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $modified3 = $this->updateCellReference($match[3]); + $modified4 = $this->updateCellReference($match[4]); if ($match[3] . $match[4] !== $modified3 . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -625,14 +597,14 @@ class ReferenceHelper } } // Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3]; - $modified3 = $this->updateCellReference($match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $modified3 = $this->updateCellReference($match[3]); if ($match[3] !== $modified3) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { $toString = ($match[2] > '') ? $match[2] . '!' : ''; @@ -809,13 +781,10 @@ class ReferenceHelper * Update cell reference. * * @param string $cellReference Cell address or range of addresses - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment * * @return string Updated cell range */ - public function updateCellReference($cellReference = 'A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function updateCellReference($cellReference = 'A1') { // Is it in another worksheet? Will not have to update anything. if (strpos($cellReference, '!') !== false) { @@ -823,10 +792,10 @@ class ReferenceHelper // Is it a range or a single cell? } elseif (!Coordinate::coordinateIsRange($cellReference)) { // Single cell - return $this->updateSingleCellReference($cellReference, $beforeCellAddress, $numberOfColumns, $numberOfRows); + return $this->cellReferenceHelper->updateCellReference($cellReference); } elseif (Coordinate::coordinateIsRange($cellReference)) { // Range - return $this->updateCellRange($cellReference, $beforeCellAddress, $numberOfColumns, $numberOfRows); + return $this->updateCellRange($cellReference); } // Return original @@ -865,13 +834,10 @@ class ReferenceHelper * Update cell range. * * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment * * @return string Updated cell range */ - private function updateCellRange($cellRange = 'A1:A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function updateCellRange(string $cellRange = 'A1:A1'): string { if (!Coordinate::coordinateIsRange($cellRange)) { throw new Exception('Only cell ranges may be passed to this method.'); @@ -884,13 +850,15 @@ class ReferenceHelper $jc = count($range[$i]); for ($j = 0; $j < $jc; ++$j) { if (ctype_alpha($range[$i][$j])) { - $r = Coordinate::coordinateFromString($this->updateSingleCellReference($range[$i][$j] . '1', $beforeCellAddress, $numberOfColumns, $numberOfRows)); - $range[$i][$j] = $r[0]; + $range[$i][$j] = Coordinate::coordinateFromString( + $this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1') + )[0]; } elseif (ctype_digit($range[$i][$j])) { - $r = Coordinate::coordinateFromString($this->updateSingleCellReference('A' . $range[$i][$j], $beforeCellAddress, $numberOfColumns, $numberOfRows)); - $range[$i][$j] = $r[1]; + $range[$i][$j] = Coordinate::coordinateFromString( + $this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j]) + )[1]; } else { - $range[$i][$j] = $this->updateSingleCellReference($range[$i][$j], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j]); } } } @@ -899,46 +867,6 @@ class ReferenceHelper return Coordinate::buildRange($range); } - /** - * Update single cell reference. - * - * @param string $cellReference Single cell reference - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment - * - * @return string Updated cell reference - */ - private function updateSingleCellReference($cellReference = 'A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) - { - if (Coordinate::coordinateIsRange($cellReference)) { - throw new Exception('Only single cell references may be passed to this method.'); - } - - // Get coordinate of $beforeCellAddress - [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); - - // Get coordinate of $cellReference - [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); - - // Verify which parts should be updated - $updateColumn = (($newColumn[0] != '$') && ($beforeColumn[0] != '$') && (Coordinate::columnIndexFromString($newColumn) >= Coordinate::columnIndexFromString($beforeColumn))); - $updateRow = (($newRow[0] != '$') && ($beforeRow[0] != '$') && $newRow >= $beforeRow); - - // Create new column reference - if ($updateColumn) { - $newColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($newColumn) + $numberOfColumns); - } - - // Create new row reference - if ($updateRow) { - $newRow = (int) $newRow + $numberOfRows; - } - - // Return new reference - return $newColumn . $newRow; - } - private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void { for ($i = 1; $i <= $highestRow - 1; ++$i) { @@ -969,7 +897,7 @@ class ReferenceHelper } } - private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void { $autoFilter = $worksheet->getAutoFilter(); $autoFilterRange = $autoFilter->getRange(); @@ -999,7 +927,7 @@ class ReferenceHelper } $worksheet->setAutoFilter( - $this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows) + $this->updateCellReference($autoFilterRange) ); } } diff --git a/tests/data/ReferenceHelperFormulaUpdates.php b/tests/data/ReferenceHelperFormulaUpdates.php index d0835603..ef563f31 100644 --- a/tests/data/ReferenceHelperFormulaUpdates.php +++ b/tests/data/ReferenceHelperFormulaUpdates.php @@ -22,6 +22,34 @@ return [ '2020', '=SUM(A1:C3)', ], + 'column range' => [ + '=SUM(B:C)', + 2, + 0, + '2020', + '=SUM(D:E)', + ], + 'column range with absolute' => [ + '=SUM($B:C)', + 2, + 0, + '2020', + '=SUM($B:E)', + ], + 'row range' => [ + '=SUM(2:3)', + 0, + 2, + '2020', + '=SUM(4:5)', + ], + 'row range with absolute' => [ + '=SUM($2:3)', + 0, + 2, + '2020', + '=SUM($2:5)', + ], [ '=SUM(2020!C3:E5,2019!C3:E5)', -2, From bbe6b8082e969b92f03e0d82ce9bca8faca4fd84 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 16 Mar 2022 15:29:42 +0100 Subject: [PATCH 13/39] Some additional unit tests for the ReferenceHelper And a bugfix when deleting cells that contain hyperlinks (the hperlinks weren't being deleted, so were being "inherited" by whatever cell moved to that address) --- CHANGELOG.md | 1 + src/PhpSpreadsheet/ReferenceHelper.php | 4 +- .../ReferenceHelperTest.php | 119 ++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df24ba1f..ca0ce021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. - Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. - Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. - Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 8c55c766..e8fb9057 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -189,7 +189,9 @@ class ReferenceHelper foreach ($aHyperlinkCollection as $cellAddress => $value) { $newReference = $this->updateCellReference($cellAddress); - if ($cellAddress !== $newReference) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { + $worksheet->setHyperlink($cellAddress, null); + } elseif ($cellAddress !== $newReference) { $worksheet->setHyperlink($newReference, $value); $worksheet->setHyperlink($cellAddress, null); } diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index 2c016b0b..e38ba286 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -3,8 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; +use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class ReferenceHelperTest extends TestCase @@ -177,4 +180,120 @@ class ReferenceHelperTest extends TestCase self::assertNull($cells[1][1]); self::assertArrayNotHasKey(2, $cells[1]); } + + public function testInsertRowsWithPageBreaks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->setBreak('A2', Worksheet::BREAK_ROW); + $sheet->setBreak('A5', Worksheet::BREAK_ROW); + + $sheet->insertNewRowBefore(2, 2); + + $breaks = $sheet->getBreaks(); + ksort($breaks); + self::assertSame(['A4' => Worksheet::BREAK_ROW, 'A7' => Worksheet::BREAK_ROW], $breaks); + } + + public function testDeleteRowsWithPageBreaks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->setBreak('A2', Worksheet::BREAK_ROW); + $sheet->setBreak('A5', Worksheet::BREAK_ROW); + + $sheet->removeRow(2, 2); + + $breaks = $sheet->getBreaks(); + self::assertSame(['A3' => Worksheet::BREAK_ROW], $breaks); + } + + public function testInsertRowsWithComments(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getComment('A2')->getText()->createText('First Comment'); + $sheet->getComment('A5')->getText()->createText('Second Comment'); + + $sheet->insertNewRowBefore(2, 2); + + $comments = array_map( + function (Comment $value) { + return $value->getText()->getPlainText(); + }, + $sheet->getComments() + ); + + self::assertSame(['A4' => 'First Comment', 'A7' => 'Second Comment'], $comments); + } + + public function testDeleteRowsWithComments(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getComment('A2')->getText()->createText('First Comment'); + $sheet->getComment('A5')->getText()->createText('Second Comment'); + + $sheet->removeRow(2, 2); + + $comments = array_map( + function (Comment $value) { + return $value->getText()->getPlainText(); + }, + $sheet->getComments() + ); + + self::assertSame(['A3' => 'Second Comment'], $comments); + } + + public function testInsertRowsWithHyperlinks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getCell('A2')->getHyperlink()->setUrl('https://github.com/PHPOffice/PhpSpreadsheet'); + $sheet->getCell('A5')->getHyperlink()->setUrl('https://phpspreadsheet.readthedocs.io/en/latest/'); + + $sheet->insertNewRowBefore(2, 2); + + $hyperlinks = array_map( + function (Hyperlink $value) { + return $value->getUrl(); + }, + $sheet->getHyperlinkCollection() + ); + ksort($hyperlinks); + + self::assertSame( + [ + 'A4' => 'https://github.com/PHPOffice/PhpSpreadsheet', + 'A7' => 'https://phpspreadsheet.readthedocs.io/en/latest/', + ], + $hyperlinks + ); + } + + public function testDeleteRowsWithHyperlinks(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]], null, 'A1', true); + $sheet->getCell('A2')->getHyperlink()->setUrl('https://github.com/PHPOffice/PhpSpreadsheet'); + $sheet->getCell('A5')->getHyperlink()->setUrl('https://phpspreadsheet.readthedocs.io/en/latest/'); + + $sheet->removeRow(2, 2); + + $hyperlinks = array_map( + function (Hyperlink $value) { + return $value->getUrl(); + }, + $sheet->getHyperlinkCollection() + ); + + self::assertSame(['A3' => 'https://phpspreadsheet.readthedocs.io/en/latest/'], $hyperlinks); + } } From 4bc3ed9cc1bb604cd85132477b4d5b0c7f3ad83e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 16 Mar 2022 19:14:50 +0100 Subject: [PATCH 14/39] Provide $includeAbsoluteReferences option for the CellReferenceHelper when updating cell addresses --- src/PhpSpreadsheet/CellReferenceHelper.php | 21 +- .../CellReferenceHelperTest.php | 193 ++++++++++++++++++ 2 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/CellReferenceHelperTest.php diff --git a/src/PhpSpreadsheet/CellReferenceHelper.php b/src/PhpSpreadsheet/CellReferenceHelper.php index 24021109..6c54dafc 100644 --- a/src/PhpSpreadsheet/CellReferenceHelper.php +++ b/src/PhpSpreadsheet/CellReferenceHelper.php @@ -50,7 +50,7 @@ class CellReferenceHelper $this->numberOfRows !== $numberOfRows; } - public function updateCellReference(string $cellReference = 'A1'): string + public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false): string { if (Coordinate::coordinateIsRange($cellReference)) { throw new Exception('Only single cell references may be passed to this method.'); @@ -61,18 +61,29 @@ class CellReferenceHelper $newColumnIndex = (int) Coordinate::columnIndexFromString(str_replace('$', '', $newColumn)); $newRowIndex = (int) str_replace('$', '', $newRow); + $absoluteColumn = $newColumn[0] === '$' ? '$' : ''; + $absoluteRow = $newRow[0] === '$' ? '$' : ''; // Verify which parts should be updated - $updateColumn = (($newColumn[0] !== '$') && $newColumnIndex >= $this->beforeColumn); - $updateRow = (($newRow[0] !== '$') && $newRow >= $this->beforeRow); + if ($includeAbsoluteReferences === false) { + $updateColumn = (($absoluteColumn !== '$') && $newColumnIndex >= $this->beforeColumn); + $updateRow = (($absoluteRow !== '$') && $newRowIndex >= $this->beforeRow); + } else { + $updateColumn = ($newColumnIndex >= $this->beforeColumn); + $updateRow = ($newRowIndex >= $this->beforeRow); + } // Create new column reference if ($updateColumn) { - $newColumn = Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns); + $newColumn = ($includeAbsoluteReferences === false) + ? Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns) + : $absoluteColumn . Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns); } // Create new row reference if ($updateRow) { - $newRow = $newRowIndex + $this->numberOfRows; + $newRow = ($includeAbsoluteReferences === false) + ? $newRowIndex + $this->numberOfRows + : $absoluteRow . (string) ($newRowIndex + $this->numberOfRows); } // Return new reference diff --git a/tests/PhpSpreadsheetTests/CellReferenceHelperTest.php b/tests/PhpSpreadsheetTests/CellReferenceHelperTest.php new file mode 100644 index 00000000..35352afd --- /dev/null +++ b/tests/PhpSpreadsheetTests/CellReferenceHelperTest.php @@ -0,0 +1,193 @@ +updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertColumnsProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['G5', 'E5'], + ['$E5', '$E5'], + ['G$5', 'E$5'], + ['I5', 'G5'], + ['$G$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteColumnsProvider + */ + public function testCellReferenceHelperDeleteColumns(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', -2, 0); + $result = $cellReferenceHelper->updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteColumnsProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['C5', 'E5'], + ['$E5', '$E5'], + ['C$5', 'E$5'], + ['E5', 'G5'], + ['$G$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperInsertRowsProvider + */ + public function testCellReferenceHelperInsertRows(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, 2); + $result = $cellReferenceHelper->updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertRowsProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E7', 'E5'], + ['E$5', 'E$5'], + ['$E7', '$E5'], + ['E11', 'E9'], + ['$E$9', '$E$9'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteRowsProvider + */ + public function testCellReferenceHelperDeleteRows(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, -2); + $result = $cellReferenceHelper->updateCellReference($cellAddress); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteRowsProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E3', 'E5'], + ['E$5', 'E$5'], + ['$E3', '$E5'], + ['E7', 'E9'], + ['$E$9', '$E$9'], + ]; + } + + /** + * @dataProvider cellReferenceHelperInsertColumnsAbsoluteProvider + */ + public function testCellReferenceHelperInsertColumnsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 2, 0); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertColumnsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['G5', 'E5'], + ['$G5', '$E5'], + ['G$5', 'E$5'], + ['I5', 'G5'], + ['$I$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteColumnsAbsoluteProvider + */ + public function testCellReferenceHelperDeleteColumnsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', -2, 0); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteColumnsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['D5', 'D5'], + ['C5', 'E5'], + ['$C5', '$E5'], + ['C$5', 'E$5'], + ['E5', 'G5'], + ['$E$5', '$G$5'], + ]; + } + + /** + * @dataProvider cellReferenceHelperInsertRowsAbsoluteProvider + */ + public function testCellReferenceHelperInsertRowsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, 2); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperInsertRowsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E7', 'E5'], + ['E$7', 'E$5'], + ['$E7', '$E5'], + ['E11', 'E9'], + ['$E$11', '$E$9'], + ]; + } + + /** + * @dataProvider cellReferenceHelperDeleteRowsAbsoluteProvider + */ + public function testCellReferenceHelperDeleteRowsAbsolute(string $expectedResult, string $cellAddress): void + { + $cellReferenceHelper = new CellReferenceHelper('E5', 0, -2); + $result = $cellReferenceHelper->updateCellReference($cellAddress, true); + self::assertSame($expectedResult, $result); + } + + public function cellReferenceHelperDeleteRowsAbsoluteProvider(): array + { + return [ + ['A1', 'A1'], + ['E4', 'E4'], + ['E3', 'E5'], + ['E$3', 'E$5'], + ['$E3', '$E5'], + ['E7', 'E9'], + ['$E$7', '$E$9'], + ]; + } +} From 4881e2ae9e1c7381a6a051732f6ad5269e289ef6 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 16 Mar 2022 21:30:19 +0100 Subject: [PATCH 15/39] Validte that lookup arrays are actually arrays --- src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php | 3 ++- .../Calculation/LookupRef/LookupBase.php | 10 ++++++++++ src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php | 1 + tests/data/Calculation/LookupRef/VLOOKUP.php | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index ae16e563..c7e87315 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -32,9 +32,10 @@ class HLookup extends LookupBase } $notExactMatch = (bool) ($notExactMatch ?? true); - $lookupArray = self::convertLiteralArray($lookupArray); try { + self::validateLookupArray($lookupArray); + $lookupArray = self::convertLiteralArray($lookupArray); $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); } catch (Exception $e) { return $e->getMessage(); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 6a0933d6..8e451fe4 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -7,6 +7,16 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; abstract class LookupBase { + /** + * @param mixed $lookup_array + */ + protected static function validateLookupArray($lookup_array): void + { + if (!is_array($lookup_array)) { + throw new Exception(ExcelError::REF()); + } + } + protected static function validateIndexLookup(array $lookup_array, $index_number): int { // index_number must be a number greater than or equal to 1 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 26f42eba..bc8624f3 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -33,6 +33,7 @@ class VLookup extends LookupBase $notExactMatch = (bool) ($notExactMatch ?? true); try { + self::validateLookupArray($lookupArray); $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); } catch (Exception $e) { return $e->getMessage(); diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index ac60bda3..2162d49a 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -24,6 +24,13 @@ return [ 2, false, ], + [ + '#REF!', + 1, + 'HELLO WORLD', + 2, + false, + ], [ 100, 1, From ec15c7a6de9cf08f5397a4ea0c1617422473f753 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 16 Mar 2022 23:03:19 +0100 Subject: [PATCH 16/39] more minor tweaks --- CHANGELOG.md | 1 + .../Calculation/LookupRef/Indirect.php | 28 +++++++++++++++++-- tests/data/Calculation/LookupRef/INDIRECT.php | 4 +++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca0ce021..cc26e624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) - Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. - Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. - Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index d0c13a5c..417a1f79 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -72,11 +72,17 @@ class Indirect [$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); if ( - (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) || - (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches))) + (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || + (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress2, $matches))) ) { return ExcelError::REF(); } @@ -95,4 +101,22 @@ class Indirect return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null) ->extractCellRange($cellAddress, $worksheet, false); } + + private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string + { + // Being lazy, we're only checking a single row/column to get the max + if (ctype_digit($start) && $start <= 1048576) { + // Max 16,384 columns for Excel2007 + $endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : 'XFD'; + + return "A{$start}:{$endColRef}{$end}"; + } elseif (ctype_alpha($start) && strlen($start) <= 3) { + // Max 1,048,576 rows for Excel2007 + $endRowRef = ($worksheet !== null) ? $worksheet->getHighestDataRow($start) : 1048576; + + return "{$start}1:{$end}{$endRowRef}"; + } + + return "{$start}:{$end}"; + } } diff --git a/tests/data/Calculation/LookupRef/INDIRECT.php b/tests/data/Calculation/LookupRef/INDIRECT.php index b8519657..7dc2515a 100644 --- a/tests/data/Calculation/LookupRef/INDIRECT.php +++ b/tests/data/Calculation/LookupRef/INDIRECT.php @@ -34,4 +34,8 @@ return [ 'supply a1 argument as int' => [900, 'A2:A4', 1], 'supply a1 argument as float' => [900, 'A2:A4', 7.3], 'supply a1 argument as string not permitted' => ['#VALUE!', 'A2:A4', '1'], + 'row range' => [600, '1:3'], + 'column range' => [1500, 'A:C'], + 'row range on different sheet' => [66, 'OtherSheet!1:3'], + 'column range on different sheet' => [165, 'OtherSheet!A:C'], ]; From f2f626e02cd4b6a8b9d839db8f0644e91ee98bd3 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 16 Mar 2022 15:34:40 -0700 Subject: [PATCH 17/39] Tweak to php-cs-fixer Parameters (#2683) For a recent change, I removed some errors from Phpstan baseline and instead added annotations in the source members. I did this work incrementally, and was surprised when php-cs-fixer required a change from `// @phpstan-ignore-next-line` to `/** @phpstan-ignore-next-line */`. No problem thinks I, and continue to modify several members using the new convention until php-cs-fixer required a change from `/** @phpstan-ignore-next-line */` to `// @phpstan-ignore-next-line`??? I did as directed, and continued to be surprised for the rest of that ticket. Having had time to research, the problem is due to two options in the php-cs-fixer config file `'comment_to_phpdoc' => true` and `'phpdoc_to_comment' => true`. It seems that php-cs-fixer is treating these annotations the same as doc-blocks, expecting `/**` before a `structural element`, and `//` otherwise. For the statements where I had questions, it expects `/**` before a statement which you might be able to precede with `/** @var`, and `//` where you would not be able to precede it with `/** @var`. However, in this case, what it is doing is forcing what appear to be inconsistencies between otherwise identical statements, whereas php-cs-fixer is supposed to be supporting consistent syntax throughout the project. This PR changes both options to false, allowing (but not requiring) a consistent syntax for these examples. It contains an example of a change from each format to the other, changes which php-cs-fixer would previously have flagged. An added bonus for this change is that Scrutinizer annotations can now be added to the code; these were often rejected by php-cs-fixer. These should, of course, be used very conservatively, but there are cases where Scrutinizer's analysis is either faulty or not helpful. This PR takes advantage of the change by adding annotations to eliminate the two existing problems which Scrutinizer classifies as 'Security', problems for which there is no sensible way to satisfy Scrutinizer's complaint. No executable code is changed by this PR. --- .php-cs-fixer.dist.php | 4 ++-- src/PhpSpreadsheet/Helper/Html.php | 3 ++- src/PhpSpreadsheet/Reader/Xls/MD5.php | 2 +- src/PhpSpreadsheet/Shared/XMLWriter.php | 2 ++ src/PhpSpreadsheet/Worksheet/Row.php | 3 +-- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index a393043d..8a8886c2 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -26,7 +26,7 @@ $config 'combine_consecutive_issets' => true, 'combine_consecutive_unsets' => true, 'combine_nested_dirname' => true, - 'comment_to_phpdoc' => true, + 'comment_to_phpdoc' => false, // interferes with annotations 'compact_nullable_typehint' => true, 'concat_space' => ['spacing' => 'one'], 'constant_case' => true, @@ -171,7 +171,7 @@ $config 'phpdoc_separation' => true, 'phpdoc_single_line_var_spacing' => true, 'phpdoc_summary' => true, - 'phpdoc_to_comment' => true, + 'phpdoc_to_comment' => false, // interferes with annotations 'phpdoc_to_param_type' => false, // Because experimental, but interesting for one shot use 'phpdoc_to_return_type' => false, // idem 'phpdoc_trim' => true, diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index 26526605..4737379a 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -619,6 +619,7 @@ class Html // Load the HTML file into the DOM object // Note the use of error suppression, because typically this will be an html fragment, so not fully valid markup $prefix = ''; + /** @scrutinizer ignore-unhandled */ @$dom->loadHTML($prefix . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); // Discard excess white space $dom->preserveWhiteSpace = false; @@ -808,7 +809,7 @@ class Html if (isset($callbacks[$callbackTag])) { $elementHandler = $callbacks[$callbackTag]; if (method_exists($this, $elementHandler)) { - // @phpstan-ignore-next-line + /** @phpstan-ignore-next-line */ call_user_func([$this, $elementHandler], $element); } } diff --git a/src/PhpSpreadsheet/Reader/Xls/MD5.php b/src/PhpSpreadsheet/Reader/Xls/MD5.php index a1108f92..14b6bc54 100644 --- a/src/PhpSpreadsheet/Reader/Xls/MD5.php +++ b/src/PhpSpreadsheet/Reader/Xls/MD5.php @@ -71,7 +71,7 @@ class MD5 */ public function add(string $data): void { - /** @phpstan-ignore-next-line */ + // @phpstan-ignore-next-line $words = array_values(unpack('V16', $data)); $A = $this->a; diff --git a/src/PhpSpreadsheet/Shared/XMLWriter.php b/src/PhpSpreadsheet/Shared/XMLWriter.php index 84ad8a83..3dc0aad9 100644 --- a/src/PhpSpreadsheet/Shared/XMLWriter.php +++ b/src/PhpSpreadsheet/Shared/XMLWriter.php @@ -54,7 +54,9 @@ class XMLWriter extends \XMLWriter public function __destruct() { // Unlink temporary files + // There is nothing reasonable to do if unlink fails. if ($this->tempFileName != '') { + /** @scrutinizer ignore-unhandled */ @unlink($this->tempFileName); } } diff --git a/src/PhpSpreadsheet/Worksheet/Row.php b/src/PhpSpreadsheet/Worksheet/Row.php index b5933356..a5f8f326 100644 --- a/src/PhpSpreadsheet/Worksheet/Row.php +++ b/src/PhpSpreadsheet/Worksheet/Row.php @@ -35,8 +35,7 @@ class Row */ public function __destruct() { - // @phpstan-ignore-next-line - $this->worksheet = null; + $this->worksheet = null; // @phpstan-ignore-line } /** From a8e179d5d9c78e66ba64b86a0017608454de0ead Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 16 Mar 2022 15:59:53 -0700 Subject: [PATCH 18/39] Scrutinizer Complains Some Samples Class Name Conflict with Jpgraph (#2684) Jpgraph does not use namespaces. Neither do the Samples members of this project. As a result, Scrutinizer complains about 13 cases of `use PhpOffice\PhpSpreadsheet\Chart\Legend;`, since this theoretically could cause the use of two different Legend classes in the empty namespace. It suggests using a class alias for Legend, which this PR does in every applicable case. It is a regrettable problem, but it is easy to fix. --- samples/Chart/33_Chart_create_area.php | 4 ++-- samples/Chart/33_Chart_create_bar_stacked.php | 4 ++-- samples/Chart/33_Chart_create_column.php | 4 ++-- samples/Chart/33_Chart_create_column_2.php | 4 ++-- samples/Chart/33_Chart_create_composite.php | 4 ++-- samples/Chart/33_Chart_create_line.php | 4 ++-- samples/Chart/33_Chart_create_multiple_charts.php | 6 +++--- samples/Chart/33_Chart_create_pie.php | 4 ++-- samples/Chart/33_Chart_create_pie_custom_colors.php | 4 ++-- samples/Chart/33_Chart_create_radar.php | 4 ++-- samples/Chart/33_Chart_create_scatter.php | 4 ++-- samples/Chart/33_Chart_create_stock.php | 4 ++-- samples/templates/chartSpreadsheet.php | 4 ++-- 13 files changed, 27 insertions(+), 27 deletions(-) diff --git a/samples/Chart/33_Chart_create_area.php b/samples/Chart/33_Chart_create_area.php index 57db90fc..95d9149c 100644 --- a/samples/Chart/33_Chart_create_area.php +++ b/samples/Chart/33_Chart_create_area.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -71,7 +71,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title = new Title('Test %age-Stacked Area Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_bar_stacked.php b/samples/Chart/33_Chart_create_bar_stacked.php index 0c87224e..bb2d8f6d 100644 --- a/samples/Chart/33_Chart_create_bar_stacked.php +++ b/samples/Chart/33_Chart_create_bar_stacked.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -74,7 +74,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_BAR); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_column.php b/samples/Chart/33_Chart_create_column.php index 5af0908c..68aa983d 100644 --- a/samples/Chart/33_Chart_create_column.php +++ b/samples/Chart/33_Chart_create_column.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -74,7 +74,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_COL); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Column Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_column_2.php b/samples/Chart/33_Chart_create_column_2.php index a62b4906..96f5e316 100644 --- a/samples/Chart/33_Chart_create_column_2.php +++ b/samples/Chart/33_Chart_create_column_2.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -82,7 +82,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_COL); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_BOTTOM, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_BOTTOM, null, false); $title = new Title('Test Grouped Column Chart'); $xAxisLabel = new Title('Financial Period'); diff --git a/samples/Chart/33_Chart_create_composite.php b/samples/Chart/33_Chart_create_composite.php index ce42d2fc..b2952420 100644 --- a/samples/Chart/33_Chart_create_composite.php +++ b/samples/Chart/33_Chart_create_composite.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -128,7 +128,7 @@ $series3 = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series1, $series2, $series3]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Average Weather Chart for Crete'); diff --git a/samples/Chart/33_Chart_create_line.php b/samples/Chart/33_Chart_create_line.php index feae2f27..fee2a284 100644 --- a/samples/Chart/33_Chart_create_line.php +++ b/samples/Chart/33_Chart_create_line.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -72,7 +72,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title = new Title('Test Stacked Line Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_multiple_charts.php b/samples/Chart/33_Chart_create_multiple_charts.php index 608ffc53..3032bc28 100644 --- a/samples/Chart/33_Chart_create_multiple_charts.php +++ b/samples/Chart/33_Chart_create_multiple_charts.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -71,7 +71,7 @@ $series1 = new DataSeries( // Set the series in the plot area $plotArea1 = new PlotArea(null, [$series1]); // Set the chart legend -$legend1 = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend1 = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title1 = new Title('Test %age-Stacked Area Chart'); $yAxisLabel1 = new Title('Value ($k)'); @@ -146,7 +146,7 @@ $series2->setPlotDirection(DataSeries::DIRECTION_COL); // Set the series in the plot area $plotArea2 = new PlotArea(null, [$series2]); // Set the chart legend -$legend2 = new Legend(Legend::POSITION_RIGHT, null, false); +$legend2 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title2 = new Title('Test Column Chart'); $yAxisLabel2 = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_pie.php b/samples/Chart/33_Chart_create_pie.php index 5480a18a..4b35b24e 100644 --- a/samples/Chart/33_Chart_create_pie.php +++ b/samples/Chart/33_Chart_create_pie.php @@ -4,7 +4,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Layout; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -73,7 +73,7 @@ $layout1->setShowPercent(true); // Set the series in the plot area $plotArea1 = new PlotArea($layout1, [$series1]); // Set the chart legend -$legend1 = new Legend(Legend::POSITION_RIGHT, null, false); +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title1 = new Title('Test Pie Chart'); diff --git a/samples/Chart/33_Chart_create_pie_custom_colors.php b/samples/Chart/33_Chart_create_pie_custom_colors.php index ca5397a1..91f7e51b 100644 --- a/samples/Chart/33_Chart_create_pie_custom_colors.php +++ b/samples/Chart/33_Chart_create_pie_custom_colors.php @@ -4,7 +4,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Layout; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -79,7 +79,7 @@ $layout1->setShowPercent(true); // Set the series in the plot area $plotArea1 = new PlotArea($layout1, [$series1]); // Set the chart legend -$legend1 = new Legend(Legend::POSITION_RIGHT, null, false); +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title1 = new Title('Test Pie Chart'); diff --git a/samples/Chart/33_Chart_create_radar.php b/samples/Chart/33_Chart_create_radar.php index eba4dc39..59a8a5d9 100644 --- a/samples/Chart/33_Chart_create_radar.php +++ b/samples/Chart/33_Chart_create_radar.php @@ -4,7 +4,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Layout; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -85,7 +85,7 @@ $layout = new Layout(); // Set the series in the plot area $plotArea = new PlotArea($layout, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Radar Chart'); diff --git a/samples/Chart/33_Chart_create_scatter.php b/samples/Chart/33_Chart_create_scatter.php index c67e4e95..9a54c18b 100644 --- a/samples/Chart/33_Chart_create_scatter.php +++ b/samples/Chart/33_Chart_create_scatter.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -68,7 +68,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_TOPRIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); $title = new Title('Test Scatter Chart'); $yAxisLabel = new Title('Value ($k)'); diff --git a/samples/Chart/33_Chart_create_stock.php b/samples/Chart/33_Chart_create_stock.php index 58686784..34fa3a6c 100644 --- a/samples/Chart/33_Chart_create_stock.php +++ b/samples/Chart/33_Chart_create_stock.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; @@ -79,7 +79,7 @@ $series = new DataSeries( // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Stock Chart'); $xAxisLabel = new Title('Counts'); diff --git a/samples/templates/chartSpreadsheet.php b/samples/templates/chartSpreadsheet.php index 2ad61d32..a5b1b882 100644 --- a/samples/templates/chartSpreadsheet.php +++ b/samples/templates/chartSpreadsheet.php @@ -3,7 +3,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\Legend; +use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -71,7 +71,7 @@ $series->setPlotDirection(DataSeries::DIRECTION_BAR); // Set the series in the plot area $plotArea = new PlotArea(null, [$series]); // Set the chart legend -$legend = new Legend(Legend::POSITION_RIGHT, null, false); +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); $title = new Title('Test Bar Chart'); $yAxisLabel = new Title('Value ($k)'); From 9428552d9446eca65bdaf2f98a90f4709af8a882 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 16 Mar 2022 16:12:38 -0700 Subject: [PATCH 19/39] Add editAs Property for 2-cell Anchor Drawings (#2674) * Add editAs Property for 2-cell Anchor Drawings This change builds on PR #2532 (@naotake51 as PR #2532), using ideas from PR #2237 (@AdamGaskins), which has had changes requested for several months. It covers a lot of the same ground as 2532. In Excel, two-cell anchor drawings can be edited as "twocell", "onecell", or "absolute". This PR adds support for those options, with a sample file that demonstrates the difference in addition to unit tests. Several other tests are added to improve the spotty coverage for Drawings. There have been several other tickets referencing two cell anchors, including issue #1159 and PR #1160 (@sgarwood, who also added support for editAs), PR #643, and issue #126, all now closed but not necessarily entirely resolved. I will try to ensure that those tickets are addressed with this one. And, in trying to make sure 1160 is covered, I stumbled upon a bug. If you use the same image resource to create two+ memory drawings, the MemoryDrawing destructor for the first will cause the rest to generate a very long warning message. This is not a problem for Php8+, only for Php7-. I have suppressed the message in the MemoryDrawing constructor. 1160 went stale due to an unresolved test error, but I don't think this was the problem. At any rate, its test works now. * Scrutinizer It reported 1 minor issue (fixed normally), and 2 major. One is fixed with a kludge. The other is a case where Scrutinizer's analysis is just wrong, and I can't figure out a kludge. But I was able to add an annotation (the first time I've managed to get one past phpcs/php-cs-fixer). We'll see. --- phpstan-baseline.neon | 50 --- .../Basic/48_Image_move_size_with_cells.php | 78 ++++ src/PhpSpreadsheet/Reader/Xlsx.php | 5 + src/PhpSpreadsheet/Worksheet/BaseDrawing.php | 419 ++++++------------ .../Worksheet/MemoryDrawing.php | 9 +- src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 78 ++-- .../Worksheet/DrawingTest.php | 75 ++++ .../Writer/Xlsx/DrawingsTest.php | 162 ++++++- 8 files changed, 499 insertions(+), 377 deletions(-) create mode 100644 samples/Basic/48_Image_move_size_with_cells.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eb20b3cf..ab910eee 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -4220,26 +4220,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php - - - message: "#^Cannot call method getCell\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - - - message: "#^Cannot call method getDrawingCollection\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - - - message: "#^Cannot call method getHashCode\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\BaseDrawing\\:\\:\\$shadow \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\CellIterator\\:\\:adjustForExistingOnlyRange\\(\\) has no return type specified\\.$#" count: 1 @@ -5250,36 +5230,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - - message: "#^Parameter \\#1 \\$coordinates of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:indexesFromString\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getChartByIndex\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$chart of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Drawing\\:\\:writeChart\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#" - count: 20 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" - count: 10 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#" count: 1 diff --git a/samples/Basic/48_Image_move_size_with_cells.php b/samples/Basic/48_Image_move_size_with_cells.php new file mode 100644 index 00000000..abf7ee34 --- /dev/null +++ b/samples/Basic/48_Image_move_size_with_cells.php @@ -0,0 +1,78 @@ +log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); +$sheet = $spreadsheet->getActiveSheet(); +$sheet->getCell('A1')->setValue('twocell'); +$sheet->getCell('A2')->setValue('twocell'); +$sheet->getCell('A3')->setValue('onecell'); +$sheet->getCell('A6')->setValue('absolute'); + +// Add a drawing to the worksheet +$helper->log('Add a drawing to the worksheet two-cell anchor not resized'); +$drawing = new Drawing(); +$drawing->setName('PhpSpreadsheet'); +$drawing->setDescription('PhpSpreadsheet'); +$drawing->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be two-cell because Coordinates2 is set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL); +$drawing->setCoordinates('B1'); +$drawing->setCoordinates2('B1'); +$drawing->setOffsetX2($drawing->getImageWidth()); +$drawing->setOffsetY2($drawing->getImageHeight()); +$drawing->setWorksheet($spreadsheet->getActiveSheet()); + +// Add a drawing to the worksheet +$helper->log('Add a drawing to the worksheet two-cell anchor resized'); +$drawing2 = new Drawing(); +$drawing2->setName('PhpSpreadsheet'); +$drawing2->setDescription('PhpSpreadsheet'); +$drawing2->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be two-cell because Coordinates2 is set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL); +$drawing2->setCoordinates('C2'); +$drawing2->setCoordinates2('C2'); +$drawing2->setOffsetX2($drawing->getImageWidth()); +$drawing2->setOffsetY2($drawing->getImageHeight()); +$drawing2->setWorksheet($spreadsheet->getActiveSheet()); + +$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth($drawing->getImageWidth(), Dimension::UOM_PIXELS); +$spreadsheet->getActiveSheet()->getRowDimension(2)->setRowHeight($drawing->getImageHeight(), Dimension::UOM_PIXELS); + +// Add a drawing to the worksheet one cell anchor +$helper->log('Add a drawing to the worksheet one-cell anchor'); +$drawing3 = new Drawing(); +$drawing3->setName('PhpSpreadsheet'); +$drawing3->setDescription('PhpSpreadsheet'); +$drawing3->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be one-cell because Coordinates2 is not set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_ONECELL); +$drawing3->setCoordinates('D3'); +$drawing3->setWorksheet($spreadsheet->getActiveSheet()); + +// Add a drawing to the worksheet +$helper->log('Add a drawing to the worksheet two-cell anchor resized absolute'); +$drawing4 = new Drawing(); +$drawing4->setName('PhpSpreadsheet'); +$drawing4->setDescription('PhpSpreadsheet'); +$drawing4->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png'); +// anchor type will be two-cell because Coordinates2 is set +//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL); +$drawing4->setCoordinates('C6'); +$drawing4->setCoordinates2('C6'); +$drawing4->setOffsetX2($drawing->getImageWidth()); +$drawing4->setOffsetY2($drawing->getImageHeight()); +$drawing4->setWorksheet($spreadsheet->getActiveSheet()); +$drawing4->setEditAs(Drawing::EDIT_AS_ABSOLUTE); + +//$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth($drawing->getImageWidth(), Dimension::UOM_PIXELS); +$spreadsheet->getActiveSheet()->getRowDimension(6)->setRowHeight($drawing->getImageHeight(), Dimension::UOM_PIXELS); + +$helper->write($spreadsheet, __FILE__, ['Xlsx']); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index c0b0c5ee..992002bb 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1315,6 +1315,11 @@ class Xlsx extends BaseReader $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + /** @scrutinizer ignore-call */ + $editAs = $twoCellAnchor->attributes(); + if (isset($editAs, $editAs['editAs'])) { + $objDrawing->setEditAs($editAs['editAs']); + } $objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); $embedImageKey = (string) self::getArrayItem( diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index bd90b2bc..815536b5 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -8,6 +8,22 @@ use PhpOffice\PhpSpreadsheet\IComparable; class BaseDrawing implements IComparable { + const EDIT_AS_ABSOLUTE = 'absolute'; + const EDIT_AS_ONECELL = 'onecell'; + const EDIT_AS_TWOCELL = 'twocell'; + private const VALID_EDIT_AS = [ + self::EDIT_AS_ABSOLUTE, + self::EDIT_AS_ONECELL, + self::EDIT_AS_TWOCELL, + ]; + + /** + * The editAs attribute, used only with two cell anchor. + * + * @var string + */ + protected $editAs = ''; + /** * Image counter. * @@ -27,14 +43,14 @@ class BaseDrawing implements IComparable * * @var string */ - protected $name; + protected $name = ''; /** * Description. * * @var string */ - protected $description; + protected $description = ''; /** * Worksheet. @@ -48,70 +64,84 @@ class BaseDrawing implements IComparable * * @var string */ - protected $coordinates; + protected $coordinates = 'A1'; /** * Offset X. * * @var int */ - protected $offsetX; + protected $offsetX = 0; /** * Offset Y. * * @var int */ - protected $offsetY; + protected $offsetY = 0; /** * Coordinates2. * - * @var null|string + * @var string */ - protected $coordinates2; + protected $coordinates2 = ''; /** * Offset X2. * * @var int */ - protected $offsetX2; + protected $offsetX2 = 0; /** * Offset Y2. * * @var int */ - protected $offsetY2; + protected $offsetY2 = 0; /** * Width. * * @var int */ - protected $width; + protected $width = 0; /** * Height. * * @var int */ - protected $height; + protected $height = 0; + + /** + * Pixel width of image. See $width for the size the Drawing will be in the sheet. + * + * @var int + */ + protected $imageWidth = 0; + + /** + * Pixel width of image. See $height for the size the Drawing will be in the sheet. + * + * @var int + */ + protected $imageHeight = 0; /** * Proportional resize. * * @var bool */ - protected $resizeProportional; + protected $resizeProportional = true; /** * Rotation. * * @var int */ - protected $rotation; + protected $rotation = 0; /** * Shadow. @@ -132,7 +162,7 @@ class BaseDrawing implements IComparable * * @var int */ - protected $type; + protected $type = IMAGETYPE_UNKNOWN; /** * Create a new BaseDrawing. @@ -140,91 +170,43 @@ class BaseDrawing implements IComparable public function __construct() { // Initialise values - $this->name = ''; - $this->description = ''; - $this->worksheet = null; - $this->coordinates = 'A1'; - $this->offsetX = 0; - $this->offsetY = 0; - $this->coordinates2 = null; - $this->offsetX2 = 0; - $this->offsetY2 = 0; - $this->width = 0; - $this->height = 0; - $this->resizeProportional = true; - $this->rotation = 0; - $this->shadow = new Drawing\Shadow(); - $this->type = IMAGETYPE_UNKNOWN; + $this->setShadow(); // Set image index ++self::$imageCounter; $this->imageIndex = self::$imageCounter; } - /** - * Get image index. - * - * @return int - */ - public function getImageIndex() + public function getImageIndex(): int { return $this->imageIndex; } - /** - * Get Name. - * - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * Set Name. - * - * @param string $name - * - * @return $this - */ - public function setName($name) + public function setName(string $name): self { $this->name = $name; return $this; } - /** - * Get Description. - * - * @return string - */ - public function getDescription() + public function getDescription(): string { return $this->description; } - /** - * Set Description. - * - * @param string $description - * - * @return $this - */ - public function setDescription($description) + public function setDescription(string $description): self { $this->description = $description; return $this; } - /** - * Get Worksheet. - * - * @return null|Worksheet - */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->worksheet; } @@ -233,16 +215,16 @@ class BaseDrawing implements IComparable * Set Worksheet. * * @param bool $overrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet? - * - * @return $this */ - public function setWorksheet(?Worksheet $worksheet = null, $overrideOld = false) + public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = false): self { if ($this->worksheet === null) { // Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet - $this->worksheet = $worksheet; - $this->worksheet->getCell($this->coordinates); - $this->worksheet->getDrawingCollection()->append($this); + if ($worksheet !== null) { + $this->worksheet = $worksheet; + $this->worksheet->getCell($this->coordinates); + $this->worksheet->getDrawingCollection()->append($this); + } } else { if ($overrideOld) { // Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet @@ -267,168 +249,84 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get Coordinates. - * - * @return string - */ - public function getCoordinates() + public function getCoordinates(): string { return $this->coordinates; } - /** - * Set Coordinates. - * - * @param string $coordinates eg: 'A1' - * - * @return $this - */ - public function setCoordinates($coordinates) + public function setCoordinates(string $coordinates): self { $this->coordinates = $coordinates; return $this; } - /** - * Get OffsetX. - * - * @return int - */ - public function getOffsetX() + public function getOffsetX(): int { return $this->offsetX; } - /** - * Set OffsetX. - * - * @param int $offsetX - * - * @return $this - */ - public function setOffsetX($offsetX) + public function setOffsetX(int $offsetX): self { $this->offsetX = $offsetX; return $this; } - /** - * Get OffsetY. - * - * @return int - */ - public function getOffsetY() + public function getOffsetY(): int { return $this->offsetY; } - /** - * Get Coordinates2. - * - * @return null|string - */ - public function getCoordinates2() - { - return $this->coordinates2; - } - - /** - * Set Coordinates2. - * - * @param null|string $coordinates2 eg: 'A1' - * - * @return $this - */ - public function setCoordinates2($coordinates2) - { - $this->coordinates2 = $coordinates2; - - return $this; - } - - /** - * Get OffsetX2. - * - * @return int - */ - public function getOffsetX2() - { - return $this->offsetX2; - } - - /** - * Set OffsetX2. - * - * @param int $offsetX2 - * - * @return $this - */ - public function setOffsetX2($offsetX2) - { - $this->offsetX2 = $offsetX2; - - return $this; - } - - /** - * Get OffsetY2. - * - * @return int - */ - public function getOffsetY2() - { - return $this->offsetY2; - } - - /** - * Set OffsetY2. - * - * @param int $offsetY2 - * - * @return $this - */ - public function setOffsetY2($offsetY2) - { - $this->offsetY2 = $offsetY2; - - return $this; - } - - /** - * Set OffsetY. - * - * @param int $offsetY - * - * @return $this - */ - public function setOffsetY($offsetY) + public function setOffsetY(int $offsetY): self { $this->offsetY = $offsetY; return $this; } - /** - * Get Width. - * - * @return int - */ - public function getWidth() + public function getCoordinates2(): string + { + return $this->coordinates2; + } + + public function setCoordinates2(string $coordinates2): self + { + $this->coordinates2 = $coordinates2; + + return $this; + } + + public function getOffsetX2(): int + { + return $this->offsetX2; + } + + public function setOffsetX2(int $offsetX2): self + { + $this->offsetX2 = $offsetX2; + + return $this; + } + + public function getOffsetY2(): int + { + return $this->offsetY2; + } + + public function setOffsetY2(int $offsetY2): self + { + $this->offsetY2 = $offsetY2; + + return $this; + } + + public function getWidth(): int { return $this->width; } - /** - * Set Width. - * - * @param int $width - * - * @return $this - */ - public function setWidth($width) + public function setWidth(int $width): self { // Resize proportional? if ($this->resizeProportional && $width != 0) { @@ -442,24 +340,12 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get Height. - * - * @return int - */ - public function getHeight() + public function getHeight(): int { return $this->height; } - /** - * Set Height. - * - * @param int $height - * - * @return $this - */ - public function setHeight($height) + public function setHeight(int $height): self { // Resize proportional? if ($this->resizeProportional && $height != 0) { @@ -482,14 +368,9 @@ class BaseDrawing implements IComparable * $objDrawing->setWidthAndHeight(160,120); * * - * @param int $width - * @param int $height - * - * @return $this - * * @author Vincent@luo MSN:kele_100@hotmail.com */ - public function setWidthAndHeight($width, $height) + public function setWidthAndHeight(int $width, int $height): self { $xratio = $width / ($this->width != 0 ? $this->width : 1); $yratio = $height / ($this->height != 0 ? $this->height : 1); @@ -509,72 +390,38 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get ResizeProportional. - * - * @return bool - */ - public function getResizeProportional() + public function getResizeProportional(): bool { return $this->resizeProportional; } - /** - * Set ResizeProportional. - * - * @param bool $resizeProportional - * - * @return $this - */ - public function setResizeProportional($resizeProportional) + public function setResizeProportional(bool $resizeProportional): self { $this->resizeProportional = $resizeProportional; return $this; } - /** - * Get Rotation. - * - * @return int - */ - public function getRotation() + public function getRotation(): int { return $this->rotation; } - /** - * Set Rotation. - * - * @param int $rotation - * - * @return $this - */ - public function setRotation($rotation) + public function setRotation(int $rotation): self { $this->rotation = $rotation; return $this; } - /** - * Get Shadow. - * - * @return Drawing\Shadow - */ - public function getShadow() + public function getShadow(): Drawing\Shadow { return $this->shadow; } - /** - * Set Shadow. - * - * @return $this - */ - public function setShadow(?Drawing\Shadow $shadow = null) + public function setShadow(?Drawing\Shadow $shadow = null): self { - $this->shadow = $shadow; + $this->shadow = $shadow ?? new Drawing\Shadow(); return $this; } @@ -589,7 +436,7 @@ class BaseDrawing implements IComparable return md5( $this->name . $this->description . - $this->worksheet->getHashCode() . + (($this->worksheet === null) ? '' : $this->worksheet->getHashCode()) . $this->coordinates . $this->offsetX . $this->offsetY . @@ -626,10 +473,7 @@ class BaseDrawing implements IComparable $this->hyperlink = $hyperlink; } - /** - * @return null|Hyperlink - */ - public function getHyperlink() + public function getHyperlink(): ?Hyperlink { return $this->hyperlink; } @@ -639,15 +483,19 @@ class BaseDrawing implements IComparable */ protected function setSizesAndType(string $path): void { - if ($this->width == 0 && $this->height == 0 && $this->type == IMAGETYPE_UNKNOWN) { + if ($this->imageWidth === 0 && $this->imageHeight === 0 && $this->type === IMAGETYPE_UNKNOWN) { $imageData = getimagesize($path); if (is_array($imageData)) { - $this->width = $imageData[0]; - $this->height = $imageData[1]; + $this->imageWidth = $imageData[0]; + $this->imageHeight = $imageData[1]; $this->type = $imageData[2]; } } + if ($this->width === 0 && $this->height === 0) { + $this->width = $this->imageWidth; + $this->height = $this->imageHeight; + } } /** @@ -657,4 +505,31 @@ class BaseDrawing implements IComparable { return $this->type; } + + public function getImageWidth(): int + { + return $this->imageWidth; + } + + public function getImageHeight(): int + { + return $this->imageHeight; + } + + public function getEditAs(): string + { + return $this->editAs; + } + + public function setEditAs(string $editAs): self + { + $this->editAs = $editAs; + + return $this; + } + + public function validEditAs(): bool + { + return in_array($this->editAs, self::VALID_EDIT_AS); + } } diff --git a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index 91acbb7b..e65541dc 100644 --- a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -47,6 +47,9 @@ class MemoryDrawing extends BaseDrawing */ private $uniqueName; + /** @var null|resource */ + private $alwaysNull; + /** * Create a new MemoryDrawing. */ @@ -56,6 +59,7 @@ class MemoryDrawing extends BaseDrawing $this->renderingFunction = self::RENDERING_DEFAULT; $this->mimeType = self::MIMETYPE_DEFAULT; $this->uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999)); + $this->alwaysNull = null; // Initialize parent parent::__construct(); @@ -64,8 +68,9 @@ class MemoryDrawing extends BaseDrawing public function __destruct() { if ($this->imageResource) { - imagedestroy($this->imageResource); - $this->imageResource = null; + $rslt = @imagedestroy($this->imageResource); + // "Fix" for Scrutinizer + $this->imageResource = $rslt ? null : $this->alwaysNull; } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 6868212a..816bb9d4 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; @@ -56,7 +57,10 @@ class Drawing extends WriterPart // Loop through charts and write the chart position if ($chartCount > 0) { for ($c = 0; $c < $chartCount; ++$c) { - $this->writeChart($objWriter, $worksheet->getChartByIndex($c), $c + $i); + $chart = $worksheet->getChartByIndex((string) $c); + if ($chart !== false) { + $this->writeChart($objWriter, $chart, $c + $i); + } } } } @@ -90,16 +94,16 @@ class Drawing extends WriterPart $objWriter->startElement('xdr:twoCellAnchor'); $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $tlColRow[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset'])); - $objWriter->writeElement('xdr:row', $tlColRow[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset'])); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); $objWriter->endElement(); $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', $brColRow[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset'])); - $objWriter->writeElement('xdr:row', $brColRow[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset'])); + $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); $objWriter->endElement(); $objWriter->startElement('xdr:graphicFrame'); @@ -107,7 +111,7 @@ class Drawing extends WriterPart $objWriter->startElement('xdr:nvGraphicFramePr'); $objWriter->startElement('xdr:cNvPr'); $objWriter->writeAttribute('name', 'Chart ' . $relationId); - $objWriter->writeAttribute('id', 1025 * $relationId); + $objWriter->writeAttribute('id', (string) (1025 * $relationId)); $objWriter->endElement(); $objWriter->startElement('xdr:cNvGraphicFramePr'); $objWriter->startElement('a:graphicFrameLocks'); @@ -153,28 +157,31 @@ class Drawing extends WriterPart public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relationId = -1, $hlinkClickId = null): void { if ($relationId >= 0) { - $isTwoCellAnchor = $drawing->getCoordinates2() !== null; + $isTwoCellAnchor = $drawing->getCoordinates2() !== ''; if ($isTwoCellAnchor) { // xdr:twoCellAnchor $objWriter->startElement('xdr:twoCellAnchor'); + if ($drawing->validEditAs()) { + $objWriter->writeAttribute('editAs', $drawing->getEditAs()); + } // Image location $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates()); $aCoordinates2 = Coordinate::indexesFromString($drawing->getCoordinates2()); // xdr:from $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX())); - $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY())); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY())); $objWriter->endElement(); // xdr:to $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', $aCoordinates2[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX2())); - $objWriter->writeElement('xdr:row', $aCoordinates2[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY2())); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates2[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX2())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates2[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY2())); $objWriter->endElement(); } else { // xdr:oneCellAnchor @@ -184,16 +191,16 @@ class Drawing extends WriterPart // xdr:from $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX())); - $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY())); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY())); $objWriter->endElement(); // xdr:ext $objWriter->startElement('xdr:ext'); - $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth())); - $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight())); + $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); + $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight())); $objWriter->endElement(); } @@ -205,7 +212,7 @@ class Drawing extends WriterPart // xdr:cNvPr $objWriter->startElement('xdr:cNvPr'); - $objWriter->writeAttribute('id', $relationId); + $objWriter->writeAttribute('id', (string) $relationId); $objWriter->writeAttribute('name', $drawing->getName()); $objWriter->writeAttribute('descr', $drawing->getDescription()); @@ -247,11 +254,11 @@ class Drawing extends WriterPart // a:xfrm $objWriter->startElement('a:xfrm'); - $objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getRotation())); + $objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation())); if ($isTwoCellAnchor) { $objWriter->startElement('a:ext'); - $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth())); - $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight())); + $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); + $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight())); $objWriter->endElement(); } $objWriter->endElement(); @@ -271,9 +278,9 @@ class Drawing extends WriterPart // a:outerShdw $objWriter->startElement('a:outerShdw'); - $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getBlurRadius())); - $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getDistance())); - $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getShadow()->getDirection())); + $objWriter->writeAttribute('blurRad', self::stringEmu($drawing->getShadow()->getBlurRadius())); + $objWriter->writeAttribute('dist', self::stringEmu($drawing->getShadow()->getDistance())); + $objWriter->writeAttribute('dir', (string) SharedDrawing::degreesToAngle($drawing->getShadow()->getDirection())); $objWriter->writeAttribute('algn', $drawing->getShadow()->getAlignment()); $objWriter->writeAttribute('rotWithShape', '0'); @@ -283,7 +290,7 @@ class Drawing extends WriterPart // a:alpha $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $drawing->getShadow()->getAlpha() * 1000); + $objWriter->writeAttribute('val', (string) ($drawing->getShadow()->getAlpha() * 1000)); $objWriter->endElement(); $objWriter->endElement(); @@ -528,4 +535,9 @@ class Drawing extends WriterPart $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId); $objWriter->endElement(); } + + private static function stringEmu(int $pixelValue): string + { + return (string) SharedDrawing::pixelsToEMU($pixelValue); + } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php b/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php index d18ff002..17e17e19 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/DrawingTest.php @@ -2,7 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Worksheet; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; +use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PHPUnit\Framework\TestCase; @@ -41,4 +44,76 @@ class DrawingTest extends TestCase $spreadsheet->disconnectWorksheets(); } } + + public function testChangeWorksheet(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + + $drawing = new Drawing(); + $drawing->setName('Green Square'); + $drawing->setPath('tests/data/Writer/XLSX/green_square.gif'); + self::assertEquals($drawing->getWidth(), 150); + self::assertEquals($drawing->getHeight(), 150); + $drawing->setCoordinates('A1'); + $drawing->setOffsetX(30); + $drawing->setOffsetY(10); + $drawing->setWorksheet($sheet1); + + try { + $drawing->setWorksheet($sheet2); + self::fail('Should throw exception when attempting set worksheet without specifying override'); + } catch (PhpSpreadsheetException $e) { + self::assertStringContainsString('A Worksheet has already been assigned.', $e->getMessage()); + } + self::assertSame($sheet1, $drawing->getWorksheet()); + self::assertCount(1, $sheet1->getDrawingCollection()); + self::assertCount(0, $sheet2->getDrawingCollection()); + $drawing->setWorksheet($sheet2, true); + self::assertSame($sheet2, $drawing->getWorksheet()); + self::assertCount(0, $sheet1->getDrawingCollection()); + self::assertCount(1, $sheet2->getDrawingCollection()); + } + + public function testHeaderFooter(): void + { + $drawing1 = new HeaderFooterDrawing(); + $drawing1->setName('Blue Square'); + $drawing1->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertEquals($drawing1->getWidth(), 100); + self::assertEquals($drawing1->getHeight(), 100); + $drawing2 = new HeaderFooterDrawing(); + $drawing2->setName('Blue Square'); + $drawing2->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertSame($drawing1->getHashCode(), $drawing2->getHashCode()); + $drawing2->setOffsetX(100); + self::assertNotEquals($drawing1->getHashCode(), $drawing2->getHashCode()); + } + + public function testSetWidthAndHeight(): void + { + $drawing = new Drawing(); + $drawing->setName('Blue Square'); + $drawing->setPath('tests/data/Writer/XLSX/blue_square.png'); + self::assertSame(100, $drawing->getWidth()); + self::assertSame(100, $drawing->getHeight()); + self::assertTrue($drawing->getResizeProportional()); + $drawing->setResizeProportional(false); + $drawing->setWidthAndHeight(150, 200); + self::assertSame(150, $drawing->getWidth()); + self::assertSame(200, $drawing->getHeight()); + $drawing->setResizeProportional(true); + $drawing->setWidthAndHeight(300, 250); + // width increase% more than height, so scale width + self::assertSame(188, $drawing->getWidth()); + self::assertSame(250, $drawing->getHeight()); + $drawing->setResizeProportional(false); + $drawing->setWidthAndHeight(150, 200); + $drawing->setResizeProportional(true); + // height increase% more than width, so scale height + $drawing->setWidthAndHeight(175, 350); + self::assertSame(175, $drawing->getWidth()); + self::assertSame(234, $drawing->getHeight()); + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index f9df2b1c..ccbced32 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -8,7 +8,9 @@ use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; +use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class DrawingsTest extends AbstractFunctional @@ -28,6 +30,8 @@ class DrawingsTest extends AbstractFunctional // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -59,6 +63,8 @@ class DrawingsTest extends AbstractFunctional // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -96,6 +102,8 @@ class DrawingsTest extends AbstractFunctional unlink($tempFileName); self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -173,7 +181,8 @@ class DrawingsTest extends AbstractFunctional self::assertEquals($comment->getBackgroundImage()->getType(), IMAGETYPE_PNG); unlink($tempFileName); - self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -287,6 +296,7 @@ class DrawingsTest extends AbstractFunctional $drawing->setPath('tests/data/Writer/XLSX/orange_square_24_bit.bmp'); self::assertEquals($drawing->getWidth(), 70); self::assertEquals($drawing->getHeight(), 70); + self::assertSame(IMAGETYPE_PNG, $drawing->getImageTypeForSave()); $comment = $sheet->getComment('A6'); $comment->setBackgroundImage($drawing); $comment->setSizeAsBackgroundImage(); @@ -304,6 +314,8 @@ class DrawingsTest extends AbstractFunctional $drawing = new Drawing(); $drawing->setName('Purple Square'); $drawing->setPath('tests/data/Writer/XLSX/purple_square.tiff'); + self::assertStringContainsString('purple_square.tiff', $drawing->getFilename()); + self::assertFalse($drawing->getIsUrl()); $comment = $sheet->getComment('A7'); self::assertTrue($comment instanceof Comment); self::assertFalse($comment->hasBackgroundImage()); @@ -326,6 +338,14 @@ class DrawingsTest extends AbstractFunctional self::assertEquals($e->getMessage(), 'Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); } + try { + $drawing->getMediaFilename(); + self::fail('Should throw exception when attempting to get media file name for tiff'); + } catch (PhpSpreadsheetException $e) { + self::assertTrue($e instanceof PhpSpreadsheetException); + self::assertEquals($e->getMessage(), 'Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.'); + } + try { $drawing->getImageFileExtensionForSave(); self::fail('Should throw exception when attempting to get image file extention for tiff'); @@ -428,7 +448,8 @@ class DrawingsTest extends AbstractFunctional unlink($tempFileName); - self::assertNotNull($reloadedSpreadsheet); + $spreadsheet->disconnectWorksheets(); + $reloadedSpreadsheet->disconnectWorksheets(); } /** @@ -436,7 +457,6 @@ class DrawingsTest extends AbstractFunctional */ public function testTwoCellAnchorDrawing(): void { - $reader = new Xlsx(); $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); @@ -455,31 +475,133 @@ class DrawingsTest extends AbstractFunctional $drawing->setWorksheet($sheet); // Write file - $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); - $tempFileName = File::sysGetTempDir() . '/drawings_image_that_two_cell_anchor.xlsx'; - $writer->save($tempFileName); - - // Read new file - $reloadedSpreadsheet = $reader->load($tempFileName); - $sheet = $reloadedSpreadsheet->getActiveSheet(); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); // Check image coordinates. - $drawingCollection = $sheet->getDrawingCollection(); + $drawingCollection = $rsheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection); $drawing = $drawingCollection[0]; self::assertNotNull($drawing); + self::assertSame(150, $drawing->getWidth()); + self::assertSame(150, $drawing->getHeight()); + self::assertSame('A1', $drawing->getCoordinates()); + self::assertSame(30, $drawing->getOffsetX()); + self::assertSame(10, $drawing->getOffsetY()); + self::assertSame('E8', $drawing->getCoordinates2()); + self::assertSame(-50, $drawing->getOffsetX2()); + self::assertSame(-20, $drawing->getOffsetY2()); + self::assertSame($rsheet, $drawing->getWorksheet()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + /** + * Test editAs attribute for two-cell anchors. + * + * @dataProvider providerEditAs + */ + public function testTwoCellEditAs(string $editAs, ?string $expectedResult = null): void + { + if ($expectedResult === null) { + $expectedResult = $editAs; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // Add gif image that coordinates is two cell anchor. + $drawing = new Drawing(); + $drawing->setName('Green Square'); + $drawing->setPath('tests/data/Writer/XLSX/green_square.gif'); self::assertEquals($drawing->getWidth(), 150); self::assertEquals($drawing->getHeight(), 150); - self::assertEquals($drawing->getCoordinates(), 'A1'); - self::assertEquals($drawing->getOffsetX(), 30); - self::assertEquals($drawing->getOffsetY(), 10); - self::assertEquals($drawing->getCoordinates2(), 'E8'); - self::assertEquals($drawing->getOffsetX2(), -50); - self::assertEquals($drawing->getOffsetY2(), -20); - self::assertEquals($drawing->getWorksheet(), $sheet); + $drawing->setCoordinates('A1'); + $drawing->setOffsetX(30); + $drawing->setOffsetY(10); + $drawing->setCoordinates2('E8'); + $drawing->setOffsetX2(-50); + $drawing->setOffsetY2(-20); + if ($editAs !== '') { + $drawing->setEditAs($editAs); + } + $drawing->setWorksheet($sheet); - unlink($tempFileName); + // Write file + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); - self::assertNotNull($reloadedSpreadsheet); + // Check image coordinates. + $drawingCollection = $rsheet->getDrawingCollection(); + $drawing = $drawingCollection[0]; + self::assertNotNull($drawing); + + self::assertSame(150, $drawing->getWidth()); + self::assertSame(150, $drawing->getHeight()); + self::assertSame('A1', $drawing->getCoordinates()); + self::assertSame(30, $drawing->getOffsetX()); + self::assertSame(10, $drawing->getOffsetY()); + self::assertSame('E8', $drawing->getCoordinates2()); + self::assertSame(-50, $drawing->getOffsetX2()); + self::assertSame(-20, $drawing->getOffsetY2()); + self::assertSame($rsheet, $drawing->getWorksheet()); + self::assertSame($expectedResult, $drawing->getEditAs()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function providerEditAs(): array + { + return [ + 'absolute' => ['absolute'], + 'onecell' => ['onecell'], + 'twocell' => ['twocell'], + 'unset (will be treated as twocell)' => [''], + 'unknown (will be treated as twocell)' => ['unknown', ''], + ]; + } + + public function testMemoryDrawingDuplicateResource(): void + { + $gdImage = imagecreatetruecolor(120, 20); + $textColor = ($gdImage === false) ? false : imagecolorallocate($gdImage, 255, 255, 255); + if ($gdImage === false || $textColor === false) { + self::fail('imagecreatetruecolor or imagecolorallocate failed'); + } else { + $spreadsheet = new Spreadsheet(); + $aSheet = $spreadsheet->getActiveSheet(); + imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); + $listOfModes = [ + BaseDrawing::EDIT_AS_TWOCELL, + BaseDrawing::EDIT_AS_ABSOLUTE, + BaseDrawing::EDIT_AS_ONECELL, + ]; + + foreach ($listOfModes as $i => $mode) { + $drawing = new MemoryDrawing(); + $drawing->setName('In-Memory image ' . $i); + $drawing->setDescription('In-Memory image ' . $i); + + $drawing->setCoordinates('A' . ((4 * $i) + 1)); + $drawing->setCoordinates2('D' . ((4 * $i) + 4)); + $drawing->setEditAs($mode); + + $drawing->setImageResource($gdImage); + $drawing->setRenderingFunction( + MemoryDrawing::RENDERING_JPEG + ); + + $drawing->setMimeType(MemoryDrawing::MIMETYPE_DEFAULT); + + $drawing->setWorksheet($aSheet); + } + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $index => $pDrawing) { + self::assertEquals($listOfModes[$index], $pDrawing->getEditAs(), 'functional test drawing twoCellAnchor'); + } + $reloadedSpreadsheet->disconnectWorksheets(); + } } } From 6faf828db7d8d1373dee7bcf41b2edb26bcddd77 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 17 Mar 2022 11:10:29 +0100 Subject: [PATCH 20/39] Fix issues with updating Conditional Formatting when inserting/deleting rows/columns - Update existing ranges, expanding if necessary, rather than trying to clone for each individual cell - Update conditions, so that cell references and formulae are maintained correctly Note that absolute as well as relative cell references should be updated in conditions --- CHANGELOG.md | 1 + src/PhpSpreadsheet/CellReferenceHelper.php | 5 + src/PhpSpreadsheet/ReferenceHelper.php | 99 ++++++++++++------- .../ReferenceHelperTest.php | 39 ++++++++ 4 files changed, 110 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc26e624..3dc40f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689) - Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) - Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. - Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. diff --git a/src/PhpSpreadsheet/CellReferenceHelper.php b/src/PhpSpreadsheet/CellReferenceHelper.php index 6c54dafc..0f079d69 100644 --- a/src/PhpSpreadsheet/CellReferenceHelper.php +++ b/src/PhpSpreadsheet/CellReferenceHelper.php @@ -43,6 +43,11 @@ class CellReferenceHelper $this->beforeRow = (int) $beforeRow; } + public function beforeCellAddress(): string + { + return $this->beforeCellAddress; + } + public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool { return $this->beforeCellAddress !== $beforeCellAddress || diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index e8fb9057..665b2e18 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Style\Conditional; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -198,6 +199,45 @@ class ReferenceHelper } } + /** + * Update conditional formatting styles when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows): void + { + $aStyles = $worksheet->getConditionalStylesCollection(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aStyles, [self::class, 'cellReverseSort']) + : uksort($aStyles, [self::class, 'cellSort']); + + foreach ($aStyles as $cellAddress => $cfRules) { + $worksheet->removeConditionalStyles($cellAddress); + $newReference = $this->updateCellReference($cellAddress); + + foreach ($cfRules as &$cfRule) { + /** @var Conditional $cfRule */ + $conditions = $cfRule->getConditions(); + foreach ($conditions as &$condition) { + if (is_string($condition)) { + $condition = $this->updateFormulaReferences( + $condition, + $this->cellReferenceHelper->beforeCellAddress(), + $numberOfColumns, + $numberOfRows, + $worksheet->getTitle(), + true + ); + } + } + $cfRule->setConditions($conditions); + } + $worksheet->setConditionalStyles($newReference, $cfRules); + } + } + /** * Update data validations when inserting/deleting rows/columns. * @@ -442,6 +482,9 @@ class ReferenceHelper // Update worksheet: hyperlinks $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows); + // Update worksheet: conditional formatting styles + $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows); + // Update worksheet: data validations $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows); @@ -505,8 +548,14 @@ class ReferenceHelper * * @return string Updated formula */ - public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0, $worksheetName = '') - { + public function updateFormulaReferences( + $formula = '', + $beforeCellAddress = 'A1', + $numberOfColumns = 0, + $numberOfRows = 0, + $worksheetName = '', + bool $includeAbsoluteReferences = false + ) { if ( $this->cellReferenceHelper === null || $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) @@ -528,8 +577,8 @@ class ReferenceHelper foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference('$A' . $match[3]), 2); - $modified4 = substr($this->updateCellReference('$A' . $match[4]), 2); + $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2); + $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -553,8 +602,8 @@ class ReferenceHelper foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference($match[3] . '$1'), 0, -2); - $modified4 = substr($this->updateCellReference($match[4] . '$1'), 0, -2); + $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2); + $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -578,8 +627,8 @@ class ReferenceHelper foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = $this->updateCellReference($match[3]); - $modified4 = $this->updateCellReference($match[4]); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); + $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences); if ($match[3] . $match[4] !== $modified3 . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -606,7 +655,7 @@ class ReferenceHelper $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3]; - $modified3 = $this->updateCellReference($match[3]); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); if ($match[3] !== $modified3) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { $toString = ($match[2] > '') ? $match[2] . '!' : ''; @@ -786,7 +835,7 @@ class ReferenceHelper * * @return string Updated cell range */ - private function updateCellReference($cellReference = 'A1') + private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false) { // Is it in another worksheet? Will not have to update anything. if (strpos($cellReference, '!') !== false) { @@ -794,10 +843,10 @@ class ReferenceHelper // Is it a range or a single cell? } elseif (!Coordinate::coordinateIsRange($cellReference)) { // Single cell - return $this->cellReferenceHelper->updateCellReference($cellReference); + return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences); } elseif (Coordinate::coordinateIsRange($cellReference)) { // Range - return $this->updateCellRange($cellReference); + return $this->updateCellRange($cellReference, $includeAbsoluteReferences); } // Return original @@ -839,7 +888,7 @@ class ReferenceHelper * * @return string Updated cell range */ - private function updateCellRange(string $cellRange = 'A1:A1'): string + private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false): string { if (!Coordinate::coordinateIsRange($cellRange)) { throw new Exception('Only cell ranges may be passed to this method.'); @@ -853,14 +902,14 @@ class ReferenceHelper for ($j = 0; $j < $jc; ++$j) { if (ctype_alpha($range[$i][$j])) { $range[$i][$j] = Coordinate::coordinateFromString( - $this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1') + $this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences) )[0]; } elseif (ctype_digit($range[$i][$j])) { $range[$i][$j] = Coordinate::coordinateFromString( - $this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j]) + $this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences) )[1]; } else { - $range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j]); + $range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences); } } } @@ -985,17 +1034,8 @@ class ReferenceHelper $coordinate = $beforeColumnName . $i; if ($worksheet->cellExists($coordinate)) { $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned); - } } } } @@ -1009,17 +1049,8 @@ class ReferenceHelper $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); if ($worksheet->cellExists($coordinate)) { $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned); - } } } } diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index e38ba286..6526219f 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -296,4 +297,42 @@ class ReferenceHelperTest extends TestCase self::assertSame(['A3' => 'https://phpspreadsheet.readthedocs.io/en/latest/'], $hyperlinks); } + + public function testInsertRowsWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $conditionalStyles = []; + $wizardFactory = new Wizard($cellRange); + /** @var Wizard\CellValue $cellWizard */ + $cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE); + + $cellWizard->equals('$H$5', Wizard::VALUE_TYPE_CELL); + $conditionalStyles[] = $cellWizard->getConditional(); + + $cellWizard->greaterThan('$H$5', Wizard::VALUE_TYPE_CELL); + $conditionalStyles[] = $cellWizard->getConditional(); + + $cellWizard->lessThan('$H$5', Wizard::VALUE_TYPE_CELL); + $conditionalStyles[] = $cellWizard->getConditional(); + + $spreadsheet->getActiveSheet() + ->getStyle($cellWizard->getCellRange()) + ->setConditionalStyles($conditionalStyles); + $sheet->insertNewRowBefore(4, 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:F9', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$H$7', $conditions->getConditions()[0]); + } + } + } } From f6fcc4de87ea6f19d257f0eb3fa1be4f61b6b714 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 17 Mar 2022 14:54:32 +0100 Subject: [PATCH 21/39] More unit testing for inserting/deleting rows/columns with DataValidation, ConditionalFormatting and PrintArea --- .../ReferenceHelperTest.php | 223 +++++++++++++++++- 1 file changed, 211 insertions(+), 12 deletions(-) diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index 6526219f..2640d80c 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -298,6 +298,83 @@ class ReferenceHelperTest extends TestCase self::assertSame(['A3' => 'https://phpspreadsheet.readthedocs.io/en/latest/'], $hyperlinks); } + public function testInsertRowsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->insertNewRowBefore(2, 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('E7')->hasDataValidation()); + } + + public function testDeleteRowsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->removeRow(2, 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('E3')->hasDataValidation()); + } + + public function testDeleteColumnsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->removeColumn('B', 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('C5')->hasDataValidation()); + } + + public function testInsertColumnsWithDataValidation(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray([['First'], ['Second'], ['Third'], ['Fourth']], null, 'A5', true); + $cellAddress = 'E5'; + $this->setDataValidation($sheet, $cellAddress); + + $sheet->insertNewColumnBefore('C', 2); + + self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); + self::assertTrue($sheet->getCell('G5')->hasDataValidation()); + } + + private function setDataValidation(Worksheet $sheet, string $cellAddress): void + { + $validation = $sheet->getCell($cellAddress) + ->getDataValidation(); + $validation->setType(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::TYPE_LIST); + $validation->setErrorStyle(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::STYLE_INFORMATION); + $validation->setAllowBlank(false); + $validation->setShowInputMessage(true); + $validation->setShowErrorMessage(true); + $validation->setShowDropDown(true); + $validation->setErrorTitle('Input error'); + $validation->setError('Value is not in list.'); + $validation->setPromptTitle('Pick from list'); + $validation->setPrompt('Please pick a value from the drop-down list.'); + $validation->setFormula1('$A5:$A8'); + } + public function testInsertRowsWithConditionalFormatting(): void { $spreadsheet = new Spreadsheet(); @@ -306,6 +383,92 @@ class ReferenceHelperTest extends TestCase $sheet->getCell('H5')->setValue(5); $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->insertNewRowBefore(4, 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:F9', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$H$7', $conditions->getConditions()[0]); + } + } + } + + public function testInsertColumnssWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->insertNewColumnBefore('C', 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('E3:H7', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$J$5', $conditions->getConditions()[0]); + } + } + } + + public function testDeleteRowsWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->removeRow(4, 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:F5', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$H$5', $conditions->getConditions()[0]); + } + } + } + + public function testDeleteColumnsWithConditionalFormatting(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true); + $sheet->getCell('H5')->setValue(5); + + $cellRange = 'C3:F7'; + $this->setConditionalFormatting($sheet, $cellRange); + + $sheet->removeColumn('D', 2); + + $styles = $sheet->getConditionalStylesCollection(); + // verify that the conditional range has been updated + self::assertSame('C3:D7', array_keys($styles)[0]); + // verify that the conditions have been updated + foreach ($styles as $style) { + foreach ($style as $conditions) { + self::assertSame('$F$5', $conditions->getConditions()[0]); + } + } + } + + private function setConditionalFormatting(Worksheet $sheet, string $cellRange): void + { $conditionalStyles = []; $wizardFactory = new Wizard($cellRange); /** @var Wizard\CellValue $cellWizard */ @@ -320,19 +483,55 @@ class ReferenceHelperTest extends TestCase $cellWizard->lessThan('$H$5', Wizard::VALUE_TYPE_CELL); $conditionalStyles[] = $cellWizard->getConditional(); - $spreadsheet->getActiveSheet() - ->getStyle($cellWizard->getCellRange()) + $sheet->getStyle($cellWizard->getCellRange()) ->setConditionalStyles($conditionalStyles); - $sheet->insertNewRowBefore(4, 2); + } - $styles = $sheet->getConditionalStylesCollection(); - // verify that the conditional range has been updated - self::assertSame('C3:F9', array_keys($styles)[0]); - // verify that the conditions have been updated - foreach ($styles as $style) { - foreach ($style as $conditions) { - self::assertSame('$H$7', $conditions->getConditions()[0]); - } - } + public function testInsertRowsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->insertNewRowBefore(2, 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:J12', $printArea); + } + + public function testInsertColumnsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->insertNewColumnBefore('B', 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:L10', $printArea); + } + + public function testDeleteRowsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->removeRow(2, 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:J8', $printArea); + } + + public function testDeleteColumnsWithPrintArea(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getPageSetup()->setPrintArea('A1:J10'); + + $sheet->removeColumn('B', 2); + + $printArea = $sheet->getPageSetup()->getPrintArea(); + self::assertSame('A1:H10', $printArea); } } From c8cf193301f373daf9d32b9d5e4adf943545b814 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 18 Mar 2022 13:34:59 +0100 Subject: [PATCH 22/39] Initial work implementing the new UNIQUE() Lookup/Reference array function --- CHANGELOG.md | 3 +- .../Calculation/Calculation.php | 2 +- .../Calculation/Information/ExcelError.php | 12 +- .../Calculation/LookupRef/Unique.php | 141 ++++++++++++++++ .../Functions/LookupRef/UniqueTest.php | 157 ++++++++++++++++++ 5 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Unique.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc40f8a..d35f4649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Implementation of the ISREF() information function. +- Implementation of the UNIQUE() Lookup/Reference (array) function +- Implementation of the ISREF() Information function. - Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. (i.e a value of "12,345.67" can be read as numeric `1235.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled. diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 36adef01..b3988260 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2583,7 +2583,7 @@ class Calculation ], 'UNIQUE' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Unique::class, 'unique'], 'argumentCount' => '1+', ], 'UPPER' => [ diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 98242eb6..e0f40848 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -127,10 +127,20 @@ class ExcelError /** * DIV0. * - * @return string #Not Yet Implemented + * @return string #DIV/0! */ public static function DIV0() { return self::$errorCodes['divisionbyzero']; } + + /** + * CALC. + * + * @return string #Not Yet Implemented + */ + public static function CALC() + { + return '#CALC!'; + } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php b/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php new file mode 100644 index 00000000..67b911aa --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php @@ -0,0 +1,141 @@ + count($flattenedLookupVector, COUNT_RECURSIVE) + 1) { + // We're looking at a full column check (multiple rows) + $transpose = Matrix::transpose($lookupVector); + $result = self::uniqueByRow($transpose, $exactlyOnce); + + return (is_array($result)) ? Matrix::transpose($result) : $result; + } + + $result = self::countValuesCaseInsensitive($flattenedLookupVector); + + if ($exactlyOnce === true) { + $result = self::exactlyOnceFilter($result); + } + + if (count($result) === 0) { + return ExcelError::CALC(); + } + + $result = array_keys($result); + + return $result; + } + + private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array + { + $caseInsensitiveCounts = array_count_values( + array_map( + function (string $value) { + return StringHelper::strToUpper($value); + }, + $caseSensitiveLookupValues + ) + ); + + $caseSensitiveCounts = []; + foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) { + if (is_numeric($caseInsensitiveKey)) { + $caseSensitiveCounts[$caseInsensitiveKey] = $count; + } else { + foreach ($caseSensitiveLookupValues as $caseSensitiveValue) { + if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) { + $caseSensitiveCounts[$caseSensitiveValue] = $count; + + break; + } + } + } + } + + return $caseSensitiveCounts; + } + + private static function exactlyOnceFilter(array $values): array + { + return array_filter( + $values, + function ($value) { + return $value === 1; + } + ); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php new file mode 100644 index 00000000..c0a6f88e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php @@ -0,0 +1,157 @@ + Date: Fri, 18 Mar 2022 14:59:37 +0100 Subject: [PATCH 23/39] Fix scrutinizer issue complaint --- .../Calculation/Functions/LookupRef/UniqueTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php index c0a6f88e..6232505d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/UniqueTest.php @@ -11,9 +11,9 @@ class UniqueTest extends TestCase /** * @dataProvider uniqueTestProvider */ - public function testUnique(array $expectedResult, ...$args): void + public function testUnique(array $expectedResult, array $lookupRef, bool $byColumn = false, bool $exactlyOnce = false): void { - $result = LookupRef\Unique::unique(...$args); + $result = LookupRef\Unique::unique($lookupRef, $byColumn, $exactlyOnce); self::assertEquals($expectedResult, $result); } From 21b784f20061e0e1c2bbbb1ddb9702d13645a38c Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Fri, 18 Mar 2022 17:44:26 +0100 Subject: [PATCH 24/39] Add basic support for Error functions when the error is #SPILL! or #CALC! --- src/PhpSpreadsheet/Calculation/Information/ErrorValue.php | 2 +- src/PhpSpreadsheet/Calculation/Information/ExcelError.php | 5 +++++ tests/data/Calculation/Information/ERROR_TYPE.php | 8 ++++++++ tests/data/Calculation/Information/IS_ERROR.php | 8 ++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dabe7d1c..cffad6a6 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes); + return in_array($value, ExcelError::$errorCodes) || $value === ExcelError::CALC(); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index e0f40848..585dfdc8 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -22,6 +22,7 @@ class ExcelError 'num' => '#NUM!', 'na' => '#N/A', 'gettingdata' => '#GETTING_DATA', + 'spill' => '#SPILL!', ]; /** @@ -45,6 +46,10 @@ class ExcelError ++$i; } + if ($value === self::CALC()) { + return 14; + } + return self::NA(); } diff --git a/tests/data/Calculation/Information/ERROR_TYPE.php b/tests/data/Calculation/Information/ERROR_TYPE.php index b08d69a6..45f27970 100644 --- a/tests/data/Calculation/Information/ERROR_TYPE.php +++ b/tests/data/Calculation/Information/ERROR_TYPE.php @@ -53,4 +53,12 @@ return [ 7, '#N/A', ], + [ + 9, + '#SPILL!', + ], + [ + 14, + '#CALC!', + ], ]; diff --git a/tests/data/Calculation/Information/IS_ERROR.php b/tests/data/Calculation/Information/IS_ERROR.php index 0755fd81..24e864fe 100644 --- a/tests/data/Calculation/Information/IS_ERROR.php +++ b/tests/data/Calculation/Information/IS_ERROR.php @@ -49,6 +49,14 @@ return [ true, '#N/A', ], + [ + true, + '#SPILL!', + ], + [ + true, + '#CALC!', + ], [ false, 'TRUE', From 45c08d6cd434b70ba55e794ea937f4f785ccb1cb Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 17 Mar 2022 13:47:58 +0100 Subject: [PATCH 25/39] Initial work on reading conditional styles for the Xls Reader Successfully reading the CF ranges and CF rules; not yet reading the styles --- src/PhpSpreadsheet/Reader/Xls.php | 69 ++++++-- .../Xls/ConditionalFormattingBasicTest.php | 158 ++++++++++++++++++ .../ConditionalFormattingExpressionTest.php | 71 ++++++++ .../Reader/Xls/ConditionalFormattingTest.php | 30 ---- .../Reader/XLS/CF_Expression_Comparisons.xls | Bin 0 -> 26624 bytes 5 files changed, 284 insertions(+), 44 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php delete mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php create mode 100644 tests/data/Reader/XLS/CF_Expression_Comparisons.xls diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index cc7ef3bc..6924a678 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -22,6 +22,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Xls as SharedXls; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Borders; +use PhpOffice\PhpSpreadsheet\Style\Conditional; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; @@ -1036,11 +1037,11 @@ class Xls extends BaseReader break; case self::XLS_TYPE_CFHEADER: - $this->readCFHeader(); + $cellRangeAddresses = $this->readCFHeader(); break; case self::XLS_TYPE_CFRULE: - $this->readCFRule(); + $this->readCFRule($cellRangeAddresses ?? []); break; case self::XLS_TYPE_SHEETLAYOUT: @@ -7933,9 +7934,9 @@ class Xls extends BaseReader return $this->mapCellStyleXfIndex; } - private function readCFHeader(): void + private function readCFHeader(): array { - var_dump('FOUND CF HEADER'); +// var_dump('FOUND CF HEADER'); $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -7943,7 +7944,7 @@ class Xls extends BaseReader $this->pos += 4 + $length; if ($this->readDataOnly) { - return; + return []; } // offset: 0; size: 2; Rule Count @@ -7955,12 +7956,14 @@ class Xls extends BaseReader : $this->readBIFF5CellRangeAddressList(substr($recordData, 12)); $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; - var_dump($ruleCount, $cellRangeAddresses); +// var_dump($ruleCount, $cellRangeAddresses); +// + return $cellRangeAddresses; } - private function readCFRule(): void + private function readCFRule(array $cellRangeAddresses): void { - var_dump('FOUND CF RULE'); +// var_dump('FOUND CF RULE'); $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -8023,14 +8026,15 @@ class Xls extends BaseReader $offset += 2; } - var_dump($type, $operator); - +// var_dump($type, $operator); +// + $formula1 = $formula2 = null; if ($size1 > 0) { $formula1 = $this->readCFFormula($recordData, $offset, $size1); if ($formula1 === null) { return; } - var_dump($formula1); +// var_dump($formula1); $offset += $size1; } @@ -8040,20 +8044,57 @@ class Xls extends BaseReader if ($formula2 === null) { return; } - var_dump($formula2); +// var_dump($formula2); + + $offset += $size2; } + + $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2); } - private function readCFFormula(string $recordData, int $offset, int $size): ?string + /** + * @return null|float|int|string + */ + private function readCFFormula(string $recordData, int $offset, int $size) { try { $formula = substr($recordData, $offset, $size); $formula = pack('v', $size) . $formula; // prepend the length - return $this->getFormulaFromStructure($formula); + $formula = $this->getFormulaFromStructure($formula); + if (is_numeric($formula)) { + return (strpos($formula, '.') !== false) ? (float) $formula : (int) $formula; + } + + return $formula; } catch (PhpSpreadsheetException $e) { } return null; } + + /** + * @param null|float|int|string $formula1 + * @param null|float|int|string $formula2 + */ + private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2): void + { + foreach ($cellRanges as $cellRange) { + $conditional = new Conditional(); + $conditional->setConditionType($type); + $conditional->setOperatorType($operator); + if ($formula1 !== null) { + $conditional->addCondition($formula1); + } + if ($formula2 !== null) { + $conditional->addCondition($formula2); + } + + $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles(); + $conditionalStyles[] = $conditional; + + $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + $this->phpSheet->getStyle($cellRange)->setConditionalStyles($conditionalStyles); + } + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php new file mode 100644 index 00000000..ce224864 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingBasicTest.php @@ -0,0 +1,158 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + /** + * @dataProvider conditionalFormattingProvider + */ + public function testReadConditionalFormatting(string $expectedRange, array $expectedRules): void + { + $hasConditionalStyles = $this->sheet->conditionalStylesExists($expectedRange); + self::assertTrue($hasConditionalStyles); + + $conditionalStyles = $this->sheet->getConditionalStyles($expectedRange); + + foreach ($conditionalStyles as $index => $conditionalStyle) { + self::assertSame($expectedRules[$index]['type'], $conditionalStyle->getConditionType()); + self::assertSame($expectedRules[$index]['operator'], $conditionalStyle->getOperatorType()); + self::assertSame($expectedRules[$index]['conditions'], $conditionalStyle->getConditions()); + } + } + + public function conditionalFormattingProvider(): array + { + return [ + [ + 'A2:E5', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + 0, + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_GREATERTHAN, + 'conditions' => [ + 0, + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_LESSTHAN, + 'conditions' => [ + 0, + ], + ], + ], + ], + [ + 'A10:E13', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + '$H$9', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_GREATERTHAN, + 'conditions' => [ + '$H$9', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_LESSTHAN, + 'conditions' => [ + '$H$9', + ], + ], + ], + ], + [ + 'A18:A20', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_BETWEEN, + 'conditions' => [ + '$B1', + '$C1', + ], + ], + ], + ], + [ + 'A24:E27', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_BETWEEN, + 'conditions' => [ + 'AVERAGE($A$24:$E$27)-STDEV($A$24:$E$27)', + 'AVERAGE($A$24:$E$27)+STDEV($A$24:$E$27)', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_GREATERTHAN, + 'conditions' => [ + 'AVERAGE($A$24:$E$27)+STDEV($A$24:$E$27)', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_LESSTHAN, + 'conditions' => [ + 'AVERAGE($A$24:$E$27)-STDEV($A$24:$E$27)', + ], + ], + ], + ], + [ + 'A31:A33', + [ + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + '"LOVE"', + ], + ], + [ + 'type' => Conditional::CONDITION_CELLIS, + 'operator' => Conditional::OPERATOR_EQUAL, + 'conditions' => [ + '"PHP"', + ], + ], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php new file mode 100644 index 00000000..6cc566c1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingExpressionTest.php @@ -0,0 +1,71 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + /** + * @dataProvider conditionalFormattingProvider + */ + public function testReadConditionalFormatting(string $expectedRange, array $expectedRule): void + { + $hasConditionalStyles = $this->sheet->conditionalStylesExists($expectedRange); + self::assertTrue($hasConditionalStyles); + + $conditionalStyles = $this->sheet->getConditionalStyles($expectedRange); + + foreach ($conditionalStyles as $index => $conditionalStyle) { + self::assertSame($expectedRule[$index]['type'], $conditionalStyle->getConditionType()); + self::assertSame($expectedRule[$index]['operator'], $conditionalStyle->getOperatorType()); + self::assertSame($expectedRule[$index]['conditions'], $conditionalStyle->getConditions()); + } + } + + public function conditionalFormattingProvider(): array + { + return [ + [ + 'A3:D8', + [ + [ + 'type' => Conditional::CONDITION_EXPRESSION, + 'operator' => Conditional::OPERATOR_NONE, + 'conditions' => [ + '$C1="USA"', + ], + ], + ], + ], + [ + 'A13:D18', + [ + [ + 'type' => Conditional::CONDITION_EXPRESSION, + 'operator' => Conditional::OPERATOR_NONE, + 'conditions' => [ + 'AND($C1="USA",$D1="Q4")', + ], + ], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php deleted file mode 100644 index ac0e306f..00000000 --- a/tests/PhpSpreadsheetTests/Reader/Xls/ConditionalFormattingTest.php +++ /dev/null @@ -1,30 +0,0 @@ -load($filename); - $this->sheet = $spreadsheet->getActiveSheet(); - } - - public function testReadConditionalFormatting(): void - { - $hasConditionalStyles = $this->sheet->conditionalStylesExists('A2:E5'); - self::assertTrue($hasConditionalStyles); - $onditionalStyles = $this->sheet->getConditionalStyles('A2:E5'); - } -} diff --git a/tests/data/Reader/XLS/CF_Expression_Comparisons.xls b/tests/data/Reader/XLS/CF_Expression_Comparisons.xls new file mode 100644 index 0000000000000000000000000000000000000000..34487aa882c7aaf3c6ff1dd19e290a3bc1e3bc2b GIT binary patch literal 26624 zcmeHQ3tUZE+h6B&s*_3=l!Q(fl&&h}mQo0na%WtUoKq>qsmM!HLxxt9 zYZGx0iov+#HoPu_L1Sh(-}CH!c4wd6IY;xo-}`>Q@B6j=*4}$Pd#(TTtmk>weVtTZ zHoL!Jf2$h8xq1>E@>y;`^jLHryn8ZnQv%=Ra*Q(1PVf$-#_Ru(1`LE?NIhNhRrp2y zn}kpFgoM<)O|0Nu7tY#5pyEXkY|*%o7SQWp~S3CF?=^7B}+E(B~oJa@Es7yA7^^SCv^nS#JK*eV> z0s%O@Xo`}ZzrT{9H*qHJEQU;O1%D4e57kplia{lzzVk zoPW1GM=8E7z(lI{IveuYGG!sdhx5zz8LYfIDL!3%ZK{%-*9s^z8q1V`Bvy^2J@s|{0M^i}B=3>^k( zOnMO^nb3gK_;qv+Un3@co&2yPE&d{r1ctv{FmDh}+bJF8*jH>VF;Dx%F_QZvzmnrG9gw4q;R7Bm? zog`GB9h5F~CFv@3A4tI`#?Wr-z#9gt4}f;PDfF1UbYU+}23IPXrVM~#*GqEE z<#$v8$tQa1LF8w(fq1EcjL;y7{CPf0l4Tro;(S$~QX)$2?>|k0B(l6Z5Pd}vGcq#d zXkNU~>Iz`}@}uicMpLLYhJ*nAg$5 zvuX8(a#9iMk8};{Rwoo&EB~6NxU~_NBIr7R+OH=R!%UvZtJ75dl69;r8PPbg~h$U?4>VrT>{bdkYsCJOn z4FRcZ<24k=UIhdq>UDx0wPu}Qx7+}oXa})u2#73HJ4l;`fOvXo2XRmVfrxsY;Gj&i zPH-sH0G((Dfq~nXL1dxYL16CvWe`s)CCpaM0tP>peHb5uZL13}r%OJ8) z?I19s{4$8Amv)f$Dj*P1uM-?oYSsx3bsL})?I18P|1yXyR6B^MAt0V!+Ce&~fIvjO zPH>{5StmICYJg6(gIG5NL>8(Y#I_+Io?dK_#lm2+7?q^BuuIX0S%?wQ!D*A6$aV1@ z`jJeCSrI*0nG-?pOla%IDg$+G%q1fn&B`;sT zRL`avmyLIQHXXIF!2zK{Y5HP`{H;tqo90|Lo$Ir4)4~QPD+)G3vEyNaM}3O zXXB}b4NlY*Yz)Mb2fyA@&&C4Tcx(4G8(`zz08f*J9xVSwJsV3d8#SJ0uZ0cU(`2E) zU)ZglO-n8tHJ;W%3mdkl$wF`6zO9~(6_)&WTEBv52|O=8rk@0_cRf(@o9jkd3r6`uE92KxNOvT znu8WLY)|v_dbcl2eQDZq*{Ja}M=fmFp62OQQBk3ujSZKL8c%c4!iMc>o?h~s67_6E zTsCSv%|i%SlS1wV<#)#O#*n-sco4q*wW*fP_ z#=eq}-qJD}6gx_|6n&);u~&kEM{z-IIiPGWBIIRyC-tBXN>K10E@(RrDBEWUwJ?FB zCLE8eNZL*b3f{s6b>e`sJp_PG(u1CQpzW2Q;2T^}R}LuK9|#$}u3CNZJ19ZH6S$zA zIG}9n6EbCYusTp7afEJ<1V&BF6?Rp%JkMILLjqy(d}3YKsEg}FuxN^U#CKd-Rao2< zc2Hq#8WfcfothAp5|@;iEBrN7!i!GIfCk#!bz@v zAQ1XX90(c%0^;ho4kemCI9czZlV~_JpFT)NHy+FRO^F*D9NG&mxS1Moy3L?$QXgFI zzwt;76a>ND+2GJ_aKX*hfYWV<25`DN(t z5P-3yQt&CPjP&rFDlhE0Fy+mMjTwc*gG*%+d9Kh-RaPDWmIJ}lrDU=Kihu|PFi3|S z1Cvt3xxxvmS@=z$dSI3mkb!_ra5hD8hIMbI*5jJBvMr)xOs%J5Os%J5%lU?+GtAXV zcxnn|;OQKuEdV;4Dsp`jvs*=ML{vsV0`s6g6y0!Ry;FAvb552=%$X$ENNnHYMA z862>IrGybt(nM@==+@C+RZza6+=z?-4pQ(#rqNN?DuOH0JeLSZk|nHlfgF&=4E_xT zkO@?2hk(d4sJAf6vE#rXlc7RH0E20iM<`RUPssU@TsX;y+bX~d7elBQ`i5s%DGIz~ zB1v(}gl8s3;N|4WlkmtSd3kb@=OqCQ8IeJH3H*}-ok$=qL)1(P3o#)P#PA=I@eV{t zgZX5TUoOuhc@UT9L0-U>%kqeqL{7Y9GN40h40#YF058cSt5&TdkbrpQ0RfsoF9>;5 z0Gb!%1;QB9rZ{3iUX%!gjHk{w&IK)A#(fuP5Jx0Q;*_<*mSinU-L&~}qn(gV za4d8a>l7?yw=fkmwEaj)+T0h>Mz>jB!uMHXtccoD9!EY0~sW zClz7BQWH|*rX+~fOC^U>q(Tx2+k@cehE7N*1==9-17~;xqEo_H?3Osh}vSZ=3Vje5=Y;8nf}6dZRs`L6tW&hF%}Bpjk3X()c#w0j z*xzU1t^q;9IR{SWR=p_r*tfzW(Cz%u4)5JIJ7;gWIO_D!k5>DhjuUsj(z)QS=&j)* z^E3O5vLi3Yh^$^Ip6Xd=r zozysH#M)Wkwtjv){H$J+W5TS>zt(Q|`@Co2$C?$th%DCb&^z-W$p1oY%{0Fg8M7KzAW_ZcqJ%JOiRG3~J(C4=I1c!`m z%ce%&uvzoH)qS7PYws-v-|!v3XK6u&{JhHi784Ix+?SYp#f=!9aw7!{Ia?Q446 z@@dA%7p40rJ~y*krt`c-hy4lmg9ktA*T2fm^_8^O;Att&xkKD_n{4jmuKSPY8ys8x zz-#sHs!g=0*SgR1n;yOD<}~PyPTyq~MI@#(<_UNre^u&Ed9}E$DbQk zRD3A^w&iJ0^TQ)&NB=%qmX_r(=I!|CvMWwc7e6hRY>2oy-!wbjy{}n;U+bk6rZdyG zT)j8SF=@z=MOz{agU-(!(7DLLdz!aX$E-Ek?H}JY*qCbb!D|1to?o|5UutJtGp{(Z zGUIdkrSFUn`dMz2F5YE(<4Q*I(W5703%A!kIwiV&aA?QX9gpRGZTy3@#f?2h&MyaS z4cDohBJ2=$^rvn+*Im5YIzIF2-llhUPm13eTVwobv~&LS3;&F+>Fih?`PHfE-NN>5 zoO|Pk=8@x0t+qbY_rmqPy_}90&faS=ZS+Tud$ev1v!<=sg{zygPCzpf##TN>LgST{eH*WqqYgFs=4 z;K$ID(x?NKP6?+n-a9=@y0t0R+CJ#3H~jV!#QopADa%>j`}!ySAF6#tN%u;+&iwqx zi;qb$?&I?PpX+~n8Dua_s5F0yR@`Of9Y<9~iy zYO;M^vtPER{rPg|^u3X#pI+_s&-ll)%HtQVoXo22pEY-SW_W3^%WR!)zdz)i$nWjn zyDT@r_UW{O#0!=DwldSynpQvg)HG{4s$93k*RSZvhrpgI%l7|v#-lKEYe%b;ybR2IWhx z`L!!6j9+ckwON8o>9(?X(F5=P>y35=oS*xlpWy4fqr%xcU2mtXPc!a2c3WaiGi!sx zx*MCmO%HYTx*Iek+ivywo(l@%UZ*<*SytR#WD+)JbidarnNGjmvUkY}d6IKxhVeH4 zETf0hX0E~gqZsqN9VE8dbpCqj>&SzYUK;K5)&K$=8CbZi!6R^qj4CW@TcpGHHg} z#((axSl8`w`G>G|$9S#mcizkHH!JbC163bZ$iFK(JJokg_?d^p6SnLMDeCro@cFxg z=NxESYm*TFRctGpr~-$bU7v5=dGy(&zvSs74!jz7{F(TAYHQ!pMJ9gbVeVe*cZ~^m zUw+uV@@7F?aJR=-yw=CQJQow{__D(4$3FwZ(*-@QEb6we;L*Dy&0_3NI%MtLAv>jK z;dZ~@g|z&N%h^UVZ3m3^9{O%&Zh&l<{^^i4nc2(6P5bBYv~G)w6IKlDwy;m<>QLvM z0iKcWD-NWt8C=nP?Di+uj)j&=te-kPpPiXoYGe@YwXST{oZhMX3l?pTDIV{wZ;)OX zA2ld>-;v!_?qd#}<`;;^eG{B$(zfdP*GC0*BEgw1v6q9-hi~6$dAIsf$-wZvURzu6 zDkr$xiE2~ArjDK2BemGO%I$vK!Ys2yAA`wOckXAKw$1wG%J$*zfe#Ch?|JNY=&93? zr=LUzrLC%O+f0}`EOO4z(!?M3=Q+i``0o0r^TVwICG&HO{lCrbG;6W>{5BiDFE%y4 zf9OKQ+jrld7$E6?B*)d^bk+TC&o{9(2CF6oC^)qC13 z)cHQ>rCp!X=l)nZ_g1@O;?Ad!ws9G^{Vgc0>h#sLD_;GUA}#EWwzV& z0b45fWZiTNt~7n05}v!o*tPgKJT6KNpERhEXD2SBFH6Xt)?$Cs1PCgrzyQG+JCDB19L4 zQn;j{OU6R!t6*)EkaaK@TgRGOzz4DjXp2}hOoxf-pYxfj{G88_-yAfH?PIBye@zqY@*8a5%qKa!$%r0CAneKtkOq$V{qW;;0e3kMr` z-@unsFySGj1%x?N>MZKs9#TuF>xq8{*1KaJ%2R(Q*82w7_TYh3Fw2C0I`H$ok$jlm z!gbA`dBTqYh3z5GGN`+I!{nI0hJh49l1@$KppF_~Y2hd%#1WsED&u_M#(XERJc$?T zj6RMiqVCam>n=os8|Fbl3nLoeW_!|4;3 zhSORu4K_9F(NG86bl47ZX{Z@44bKg-X`d(`C8R6R2{}&Lg9kDEr$g{7YO*VJr(^gP zHA%<#R5~W612NPde#0~-xZ*{0z-s8%-Qb;&fH9Q;^mjgdM;+2J9f+a!FosqEJHj3F z;zA~!m%NZsd36BK1Xq$3sWC5V8S^546KZ;jd2wAtk@tKVl~+LJW%vpJ-GnMN=0#~u za2Ju{i_4gbyhm12dEsCSd}sLTQh6Df5zz!z>gl|C%DlV-R9-!myn0k#MrOoN%8U!u z3cexdsJ!|rGV80zj2K-ZH*Gz%7u$O1XG-fCQePSC83HO=nbw;jyeX}RzNoYw)=F9; z#(LOBDXj;Gt02Z`0`;e~o^f4X#(LPwDYa-!!A*m z)-zG&Rag((6eV916`8TFD6NO>hr$91>zS(LHC2%r>y%PvGi6?d^{@?B%4}9A^9?w9 zRs!zo!fOi)p*>jtRO3D2EL9P@7YV={E_;O#vJm>4g{-y~scnFd4yvDCGK4qzKAr^# zgcSzOgH?%n+t15|cQmwI?zfqriR;#M8X&=?Y^)o$K89!L@XF!QE1h)1Ovv{T#YTi~ zpOsHci?oHJ1Yn(Mse`MAhP1r6_)Q;i#_o`Q+t1qo65<&{MqcI%BK>d43$p8ETJmA7 z(=d?lKxPV^At;rMg3$%n0p&9)K`M1I$=M~r>WPiCn&dG;OA^DfIkc*vT)8}h9%@h- z8*l%|HSk%E)}+0FT@-Yq`*YX7{TLo=x^tP3ICnU3(*tM|;R80G_2+?%0FH~=f#EAB#9(0S}fwQni4xjfgb=*it$7RW053D z%1Q1biV-J@rBNxc8Jn7nn_&_CB1F*%QOU{TWH*`@mp|fWj<$Yw+~+4A^*;Me!n>|N z@lc12SEB|RHPEPmMh!G-piu*j8fer&qXrr^(5Qh%4K!-t-=P6*^M8Kz#p--FYt!Yc zVgB#(aSzV_e}aHZ(lBMG{!|gp+i@KLf8CE0b6f(R1;G?T76hE%FM@z)gH}Mmd4E0x zoD&y9V9)z;>xi0f!y!R9oF|gRC5VAUtt}uTV&kRh*%p|hT(~|_cgm3%0TsF6$^z!B z`;trnV}{17Q3H({Xw*QX1{yWcsDVZeG-{wx1C1JJ)Ig&K8a43$Tmv}$#o00LJ>%Xz z?%U&@J)X_MJ$sz%<9r%t&Nyes`8$1Vik{=+X&F2qiSvA%zvIzQoa^IyKuZYt*W6k` zz>_q%=Z=5C4`<>w5JV90{|>Q(r#%D*2<;%WhtL6nBLw{35uVNvTp+kYfNgd7-8h^l zfPmjWLBMC@_4hR}7=AU2KLi%RA2PtdwuOJZ;O|@(M3+7l1gz1IL_)k1a+11}zVt)& zj<#YbBY?XUex^Qwk|UlWH~0zv3rHP*EPX>Wb_<|W>XISUZ;0{7=U8_94Znnv2`S1y xfuRZ45%^;r!ur!t{ Date: Fri, 18 Mar 2022 22:38:24 +0100 Subject: [PATCH 26/39] Move DataValidation switch statements into a dedicated helper class --- phpstan-baseline.neon | 16 +--- src/PhpSpreadsheet/Reader/Xls.php | 85 ++----------------- .../Reader/Xls/DataValidationHelper.php | 72 ++++++++++++++++ 3 files changed, 78 insertions(+), 95 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ab910eee..9ecc0f01 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2300,16 +2300,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$errorStyle of method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\DataValidation\\:\\:setErrorStyle\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - - - message: "#^Parameter \\#1 \\$operator of method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\DataValidation\\:\\:setOperator\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#1 \\$showSummaryBelow of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:setShowSummaryBelow\\(\\) expects bool, int given\\.$#" count: 1 @@ -2320,11 +2310,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Parameter \\#1 \\$type of method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\DataValidation\\:\\:setType\\(\\) expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls.php - - message: "#^Parameter \\#2 \\$row of method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\IReadFilter\\:\\:readCell\\(\\) expects int, string given\\.$#" count: 1 @@ -5484,3 +5469,4 @@ parameters: message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Xlfn\\:\\:addXlfn\\(\\) should return string but returns string\\|null\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php + diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 0fd05c87..bff2d41e 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4776,57 +4776,11 @@ class Xls extends BaseReader // bit: 0-3; mask: 0x0000000F; type $type = (0x0000000F & $options) >> 0; - switch ($type) { - case 0x00: - $type = DataValidation::TYPE_NONE; - - break; - case 0x01: - $type = DataValidation::TYPE_WHOLE; - - break; - case 0x02: - $type = DataValidation::TYPE_DECIMAL; - - break; - case 0x03: - $type = DataValidation::TYPE_LIST; - - break; - case 0x04: - $type = DataValidation::TYPE_DATE; - - break; - case 0x05: - $type = DataValidation::TYPE_TIME; - - break; - case 0x06: - $type = DataValidation::TYPE_TEXTLENGTH; - - break; - case 0x07: - $type = DataValidation::TYPE_CUSTOM; - - break; - } + $type = Xls\DataValidationHelper::type($type); // bit: 4-6; mask: 0x00000070; error type $errorStyle = (0x00000070 & $options) >> 4; - switch ($errorStyle) { - case 0x00: - $errorStyle = DataValidation::STYLE_STOP; - - break; - case 0x01: - $errorStyle = DataValidation::STYLE_WARNING; - - break; - case 0x02: - $errorStyle = DataValidation::STYLE_INFORMATION; - - break; - } + $errorStyle = Xls\DataValidationHelper::errorStyle($errorStyle); // bit: 7; mask: 0x00000080; 1= formula is explicit (only applies to list) // I have only seen cases where this is 1 @@ -4846,39 +4800,10 @@ class Xls extends BaseReader // bit: 20-23; mask: 0x00F00000; condition operator $operator = (0x00F00000 & $options) >> 20; - switch ($operator) { - case 0x00: - $operator = DataValidation::OPERATOR_BETWEEN; + $operator = Xls\DataValidationHelper::operator($operator); - break; - case 0x01: - $operator = DataValidation::OPERATOR_NOTBETWEEN; - - break; - case 0x02: - $operator = DataValidation::OPERATOR_EQUAL; - - break; - case 0x03: - $operator = DataValidation::OPERATOR_NOTEQUAL; - - break; - case 0x04: - $operator = DataValidation::OPERATOR_GREATERTHAN; - - break; - case 0x05: - $operator = DataValidation::OPERATOR_LESSTHAN; - - break; - case 0x06: - $operator = DataValidation::OPERATOR_GREATERTHANOREQUAL; - - break; - case 0x07: - $operator = DataValidation::OPERATOR_LESSTHANOREQUAL; - - break; + if ($type === null || $errorStyle === null || $operator === null) { + return; } // offset: 4; size: var; title of the prompt box diff --git a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php new file mode 100644 index 00000000..02f844e3 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php @@ -0,0 +1,72 @@ + + */ + private static $types = [ + 0x00 => DataValidation::TYPE_NONE, + 0x01 => DataValidation::TYPE_WHOLE, + 0x02 => DataValidation::TYPE_DECIMAL, + 0x03 => DataValidation::TYPE_LIST, + 0x04 => DataValidation::TYPE_DATE, + 0x05 => DataValidation::TYPE_TIME, + 0x06 => DataValidation::TYPE_TEXTLENGTH, + 0x07 => DataValidation::TYPE_CUSTOM, + ]; + + /** + * @var array + */ + private static $errorStyles = [ + 0x00 => DataValidation::STYLE_STOP, + 0x01 => DataValidation::STYLE_WARNING, + 0x02 => DataValidation::STYLE_INFORMATION, + ]; + + /** + * @var array + */ + private static $operators = [ + 0x00 => DataValidation::OPERATOR_BETWEEN, + 0x01 => DataValidation::OPERATOR_NOTBETWEEN, + 0x02 => DataValidation::OPERATOR_EQUAL, + 0x03 => DataValidation::OPERATOR_NOTEQUAL, + 0x04 => DataValidation::OPERATOR_GREATERTHAN, + 0x05 => DataValidation::OPERATOR_LESSTHAN, + 0x06 => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 0x07 => DataValidation::OPERATOR_LESSTHANOREQUAL, + ]; + + public static function type(int $type): ?string + { + if (isset(self::$types[$type])) { + return self::$types[$type]; + } + + return null; + } + + public static function errorStyle(int $errorStyle): ?string + { + if (isset(self::$errorStyles[$errorStyle])) { + return self::$errorStyles[$errorStyle]; + } + + return null; + } + + public static function operator(int $operator): ?string + { + if (isset(self::$operators[$operator])) { + return self::$operators[$operator]; + } + + return null; + } +} From c73bb612e0445ebbf0896128f66bc3bbe31de1c4 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 19 Mar 2022 12:04:14 +0100 Subject: [PATCH 27/39] Unit tests for Xls Reader DataValidation --- .../Reader/Xls/DataValidationTest.php | 60 ++++++++++++++++++ tests/data/Reader/XLS/DataValidation.xls | Bin 0 -> 26112 bytes 2 files changed, 60 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php create mode 100644 tests/data/Reader/XLS/DataValidation.xls diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php new file mode 100644 index 00000000..bdefa17e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/DataValidationTest.php @@ -0,0 +1,60 @@ +load($filename); + $this->sheet = $spreadsheet->getActiveSheet(); + } + + /** + * @dataProvider dataValidationProvider + */ + public function testDataValidation(string $expectedRange, array $expectedRule): void + { + $hasDataValidation = $this->sheet->dataValidationExists($expectedRange); + self::assertTrue($hasDataValidation); + + $dataValidation = $this->sheet->getDataValidation($expectedRange); + self::assertSame($expectedRule['type'], $dataValidation->getType()); + self::assertSame($expectedRule['operator'], $dataValidation->getOperator()); + self::assertSame($expectedRule['formula'], $dataValidation->getFormula1()); + } + + public function dataValidationProvider(): array + { + return [ + [ + 'B2', + [ + 'type' => DataValidation::TYPE_WHOLE, + 'operator' => DataValidation::OPERATOR_GREATERTHANOREQUAL, + 'formula' => '18', + ], + ], + [ + 'B3', + [ + 'type' => DataValidation::TYPE_LIST, + 'operator' => DataValidation::OPERATOR_BETWEEN, + 'formula' => '"Blocked,Pending,Approved"', + ], + ], + ]; + } +} diff --git a/tests/data/Reader/XLS/DataValidation.xls b/tests/data/Reader/XLS/DataValidation.xls new file mode 100644 index 0000000000000000000000000000000000000000..44e9d1d3c7bd57d4fa58e49fdb68d6b2b12bf5f1 GIT binary patch literal 26112 zcmeHQ30#fY`#<+~s~e?7B2l*mrB$mfZ3vZ;eJn|Ci&9*b3{4FgvdgY!Y!i_+gveHt zi4cmxSh5Wh27|`TnEQX8_wC-^``$(UexKjxzumXbd6(xs=leY8InO!od7k&Yx9Tf~ z54IlcSVve}KcYeY5$g~w2Au=vJtWr#1n!H)cquu$z&ViA+y5dB=@YFBA#>^P4{tNg#1#G7-W4e@>3>Bo<`!luD|{l2kx~LEab=OOh$69XK4w zeG=8Fq+Yzw=J*-(r5XdZXDF3Pnf8o7o1^b%bM*U64i52ASzD}&`)6_x$f4R2I7H{O z7_B~o!6ofKlY;{msjB5r>pChh1Y`^35NAm0!siSpLa>b_*bB%=GMJe2IuK3LmJqA$ zR@M&I)-HX5TO^YLk7=uYg3GlQX|bR&OBQHpI-xd+4;T7r=GVgYrecV`4h$03KJzmwcj zO~7Z8#O4+uKHl+NO-C&@3> z4{bj%|CHcQg42!?99;=rDksZ6^9&75TAP5L>3-Knk{|GCJxlt94vo*!GkP|P#zSKX zy#}U!q@Pp|MiM%sUUT`zWG*U5y7ePB%VP!Ud`FTlL&pqUq6g!=2AttoPd!WE`OpTQ zyj=MLn59Jp=&4-`IG`Xc;=pTZ0UT0W!~xr+MFr4kTT}oWr9}nMoLf`?`&Ww!;DFMi z0@yQJQ~Ghe3l|)YeJ?EuGI6su$#C3gx3%sJCz5 z$_eG;<0B^&lO_=gX^k>LAc+>9Ns|}KPDZFd)0L=Okx)#n{Ck>W_eM_;itdf5eL0~d z%*2@-six#>)?HdNw13mp6HAoj35V%mR6P)=g*9khm z&CrQz5D0009;7N*H3;~Rp9gVuQw`Ep1_Uk?>ja$}Y zFJHb?%%&}yjk`RX?kd=zAJeEbZDF+dZIxm+#%wko@@$+`ut9gBkquuMeYd_rF`IU5 zHlFfqTvf24r?r&QbcE3lf4i-ijR~@GS8Zt)z{b59mR1#fxZ)SZY}&KgD6lkZ6>ON6 zRu%lm#l4EzbYQblU}@b{uwhzSRq&lVcNDWRWwTLWY4$4EFfFYrSp0OOVm2MwY!q0U zvkEp$OREYlz4=Np8#6W=1(xQff(_Hss)8#X99GPx6SDDCZD|5vw zX0uUXX*Md@FfGm1?cISa#kDbKvr%Aawkp^#EzQ-fva(V!8w)lY1(xQZf(_HsT;0UC zq7}0du-PcEG#3?Yn3m@1mV550Vm6j+HVQ1wT?HGarMbH8s$Q;$jV`eQCzeQ`n|`e4 zW@oXs(q|VuqZaDB@2{kMTERe@1YpfrU?}6hYi|=1{Fz_G-2_vV(CB_KAS6~UMVE~#P1OlP$$AO?RARzq~0^ziw!}K39 zm4-u8)CS4ufp9gq4LBJLa2Wg8;D!po>G4kqxY%dgu{x*-g1a%nVGLx08z}&%$3Z3F z^wfY$fx9!oVJu_~2pVk_fa7>bx0sAtR&!es4`@g-IB|Lkm_mwdlW~oqK7&EC)4>+8 z(VM!z7~=ktM#Gl_|7Q0$fC&_{rX-6ZVR&H;%$!azFb@S_tf>fW3I`V2qoKxAcv&K;Zyvl+ zX*75+sZ0Xh(M?uX4gr=!!O}(40D(}A+ULM2jLy>j(i8j3(4=Hx4u7&N2DcUT7sN;g zp$OCuy&g&w2vkdkGz>B~4Yc%?WJsg0Btsf~wVKhm;!d^3R;XKsgwmZn=D<3|eH=V;cuQ`6O+W8E}7xuoy{0+Ww z0N0BT`sPEBoZmo*!QaFZr%96J7b65qAD$eUoH7%7ewZ*3pCn?KXYh$5yg`SeB#LhW z2~HX}lg1;LtmBW?UB|eix0Fl5f;4!>pd=()lIQpIPeUt_P9SYtIF1WQN=$~ZR>X+e z(}go9IHgTX*t6>5&@vZ;0S$xxp8v#gNM4aIzsvJ^=a-+~Huubr8*NM~e=6EJ@~?*A zYa{#%1SSa%x7-@O>+?udYN36kY(w78A^YF@+uPKe1y0Dn?-hD$@2KprJ9g+BnXkX$ zUCF7Fwq6S}2WM{z&!6*jdQ5PU_wtn@iB8}D@14H4&)#}z z?70yiO%FVqDD=4MQE*T2w)Ik@vIDx=5m#aarjAcf_xUdS#hn{t6HeY;UbK9!=ftX^ zXT`>o-upajd%Y@STn1=F4D(W1edXWS`O6v5lP9z=E+mN%2@_^IJK;WS#g(aLF1ibU zTTnXxYWlEiWgWVtI_IwpaNeFy>TEM&t<3J2)!zv{uhr@VKkNJ78g_aAvv0}Ax;4KD zOg8P-Dtj25X;B)7D!oyyUd#=`Y#jg#XmkzaUf zvVG2QXU$gM4{+A}vVN;=$1R+W@2*)y3H);ZS=8qEH7C1aZ!`w3GASkjLFsOP_jK#y z)@RNyuMRExvfGB%=N^_soE-VuyOaK{Mdf~XGk*SUZt9(etp0EK!%QBP_>`SHed71$ z-DXu6>(0pXUb*52&)t7*T~qm?;+qcVT#b&7nHTlPw5qf$oAGZaMU`K*d$#OZMfBFN zTZ;^`)13zz7I=4BQE4zIJ^$MMv9?LWk1fp)YaMW5&Jd4c8~0RqyY5*Vvb#RHr?V}^ z;)ChI>;1m!n!duyvTk8XM0LhL6_>x&JM7(ln`qe{%bQm-W*$F&Cbno-!{f7pJBLSf zU*G-2#;^3Yh}zxUS8V@s$c|8r>goJ$A;*8}U6_06TBrETYx~>W-8(hDFt$$b(>VLQ zSr>nes`Id|jrij1tll99wk^21#W-T(+4W{e241|e-_Pz;(Y*a8spIl@=SR%Q@(O_ z{~MpQx72zGlJ1}EHRqo{Uwll8ah{mxEf$_SI!ajia%)eQC-=mEyuW`xqwdrDdi~!b zzFM7Fy0m@Qf9_t1IQ7@375cjtw*6&C+FvgVXYG$D{q(BPC*#ZK)u%3AJ(JZiIBUVI z%+S(6hj|*k|9HeXo!8%|e|e6t<+Id+#EaEj^D2Xsx{g12*0pUjwnFoymv`~85B~ku zmLL4R%%v!ELPhNS89O=VyGwOjH|(78;9g#6$j{bUL&wkY8kW%On0JVv)M)*M=tYj3 z0*!w#((GArZTRVfIu*;Wdv_@>ieInWt8Icq>CWPSYDWVP|4pTQi-bn%mmEO%HZ-yB9D#+iLxVev1p@UZ>jxw6DCkR6k_=xIwRz zGwpuAZS9a1^mJv}Y`vX6S-Ou>=d8bMWmef`SB;6uL(<8QYaR@{5^R~s$8bz?lQ4EEt$7QToaJ;~cQbADr&iUGyoN9K23i(PU0-7ID(%}D7c<%)KmXuV-{RQu4^Ht+=f;^1daa3QD(}O`5)AA~>Wb4kc95Ts$#JjaQzEvZ&&joGB%w9Dy_195p zy_c0FtQp#S$pDYqVEaN}*9hk|hf+3#R~k>)_4N9Q;L>QbXLj}TGIL6Gb)wvI%h%2C zpK`EZ>Gv@uliam+(u?9Fhs`{2Y;TS8_#@}I1;UA62PW#9*VKP?oM$EAmGz9h5_lnW zS7G~mwUgulGEDT;nPz(TLAHT;)-P9g zjdJ#XRCH?J6Q?83?1n%4BseVUSbN7}@{ExY^M4j4ZaKKoF80N@H$Gh$W$GWjD5u2d zo9rHQml-YUy!E>h1HA`FE{45*_s!`c(SwhzbhJ5F^PuDLtoWIs4cixFP zb!x-+jMr zv}??y&zJevAdv`ax`&TPw0yxnIu4Lq_tZj?uOoA|<4AN?5IVd;bYZWf=x+Gtl_Par(xQC=YfF9oTne7# zj;%3%RO`M+^kHu8zRpWDz6*G1HQ?Njf396{yUPio$GPL39VYI23ks__cP;Je=&3>V zqn|zH;=E*bDA|Izj|NREvLY0gZIgyIr(~yCBNfZQ2FP#-qn?xi{XwLJl*lt zE%OmbPg*Dn<0;-ai3w?S&c!&ZVcsl$QquJjgH>JcE~?&JqmlL4L(kL4X4s3Ddixys zP75PXZHzmy{@cp7QLguT#P06M@BMu3j7?{!9ec6dH{4`s_N_is4osT2Jjv^2yO>o$ zwjo_@!fl;;Y!J2SHe_{P_2Lm9Gxzn}zGBp4tL;1demqee(?n#@WJT!au|OX5Lz$X}hhW>Vb9c?qJ@9aU~_?_oY?ma2;WNjJAh50125!mWyBeCZ&O-MHsAyG3LHU`@W znlNrbxOc%=AaE1~MPL zw&oh+ZOT(Zf2TDp4x0~25Bw&K5^%u>)Tho3DNo7(9G1U#y}1@LIm{=5qIak0_SAkJ zMaiU`8VZ(lL6?l!aK45+XTjkiq#dOBRPHQl-xYF4Q`=LYZj5tVDatb+JI47|czuD0 z;2XjptYZUb%8lBt`$&%Zr}H2Xn#a-s6q9a!F-AshBS8wt2%$FA9qNGg8TniTioyk6 z6p^D(kOr*|sfqRpbc-3V{}b{R1O`wpyz1$T_b|Ucm4m**dq}$pTqnWN80Pgaz`q9S z>p^f08h(v}BPO`OK-YNf0_XaK_u7`C>%2yfmA!FEE2Bc3Uk%uOh1Ey$RJYEq8-DSy!?|D$3Z|vZOcqQQ3X)PGUyq)Gh)~eJ1MyEtih`)3#Q$&P1q2AdaG+JO&u~Z#L=j>bItD6gP@_NH4JBGtAOHI_ z@DBuA=&mU%1Ge>p1)JY~42?A?T*W8$-45Nt&Q|~l;y%#0g!n?jzA^|Bw(@vLXphq& zVH{`?BnT&wHIOitvjr0Np2tuKmkMH@rXLkhPQy+f5*H;(nwb=xEEtd$B}|}rXO{Kt z{AHh?IKFT{Gvn^OIdD!7TR>26Y7MA0pw@s|18NPZHK5jjS_5hgs5PM0fLa4;4X8CB z)qpDhpIdvWHqXh-VD)|2M)Cj#iMYA=yCc0;ww`Tq20~?cmrQl075`NU%tT zz`_;y?vlR$3G!e*_4co6ARInr!mqOg@Odi!9t?hx+LFzLXi{r>kUQFufRB4Z&QN1o3R;J@LwRL{85Hx6rr-r?MLU-B*Uo>yYQQ1tUG?0 z8BNKAl%|igmEqa~f9ylpf11l*fMD2pVACZ1fg64lJ`FxI!xm_nJU#z~`r9JoM*jZ= D1Cl>t literal 0 HcmV?d00001 From 3767917ee5d13e7d0bef395861deef3d0ae1addf Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 19 Mar 2022 14:48:50 +0100 Subject: [PATCH 28/39] Stubs for reading style information for Conditional Formatting --- src/PhpSpreadsheet/Reader/Xls.php | 42 +++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 6924a678..1f8c1be2 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -7998,6 +7998,9 @@ class Xls extends BaseReader // offset: 6; size: 4; Options $options = self::getInt4d($recordData, 6); + $style = new Style(); + $this->getCFStyleOptions($options, $style); + $hasFontRecord = (bool) ((0x04000000 & $options) >> 26); $hasAlignmentRecord = (bool) ((0x08000000 & $options) >> 27); $hasBorderRecord = (bool) ((0x10000000 & $options) >> 28); @@ -8007,22 +8010,32 @@ class Xls extends BaseReader $offset = 12; if ($hasFontRecord === true) { + $fontStyle = substr($recordData, $offset, 118); + $this->getCFFontStyle($fontStyle, $style); $offset += 118; } if ($hasAlignmentRecord === true) { + $alignmentStyle = substr($recordData, $offset, 8); + $this->getCFAlignmentStyle($alignmentStyle, $style); $offset += 8; } if ($hasBorderRecord === true) { + $borderStyle = substr($recordData, $offset, 8); + $this->getCFBorderStyle($borderStyle, $style); $offset += 8; } if ($hasFillRecord === true) { + $fillStyle = substr($recordData, $offset, 4); + $this->getCFFillStyle($fillStyle, $style); $offset += 4; } if ($hasProtectionRecord === true) { + $protectionStyle = substr($recordData, $offset, 4); + $this->getCFProtectionStyle($protectionStyle, $style); $offset += 2; } @@ -8049,7 +8062,31 @@ class Xls extends BaseReader $offset += $size2; } - $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2); + $this->setCFRules($cellRangeAddresses, $type, $operator, $formula1, $formula2, $style); + } + + private function getCFStyleOptions(int $options, Style $style): void + { + } + + private function getCFFontStyle(string $options, Style $style): void + { + } + + private function getCFAlignmentStyle(string $options, Style $style): void + { + } + + private function getCFBorderStyle(string $options, Style $style): void + { + } + + private function getCFFillStyle(string $options, Style $style): void + { + } + + private function getCFProtectionStyle(string $options, Style $style): void + { } /** @@ -8077,7 +8114,7 @@ class Xls extends BaseReader * @param null|float|int|string $formula1 * @param null|float|int|string $formula2 */ - private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2): void + private function setCFRules(array $cellRanges, string $type, string $operator, $formula1, $formula2, Style $style): void { foreach ($cellRanges as $cellRange) { $conditional = new Conditional(); @@ -8089,6 +8126,7 @@ class Xls extends BaseReader if ($formula2 !== null) { $conditional->addCondition($formula2); } + $conditional->setStyle($style); $conditionalStyles = $this->phpSheet->getStyle($cellRange)->getConditionalStyles(); $conditionalStyles[] = $conditional; From 3803658897737c4e02a133d186b45cacc48108a0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 19 Mar 2022 15:43:07 +0100 Subject: [PATCH 29/39] Some basic reading of font style information for CF Styles --- src/PhpSpreadsheet/Reader/Xls.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 1f8c1be2..94fec859 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\NamedRange; +use PhpOffice\PhpSpreadsheet\Reader\Xls\Color; use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -8071,6 +8072,21 @@ class Xls extends BaseReader private function getCFFontStyle(string $options, Style $style): void { + $fontSize = self::getInt4d($options, 64); + $fontSize = ($fontSize !== -1) ? $fontSize / 20 : -1; + $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold + $color = self::getInt4d($options, 80); + + if ($fontSize !== -1) { + $style->getFont()->setSize($fontSize); + } + if ($color !== -1) { + $style->getFont()->setColor( + new \PhpOffice\PhpSpreadsheet\Style\Color(Color::map($color, $this->palette, $this->version)['rgb']) + ); + } + $style->getFont() + ->setBold($bold); } private function getCFAlignmentStyle(string $options, Style $style): void From aea156ff76a02e93721b9695a6441a2077628a7b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 19 Mar 2022 16:03:17 +0100 Subject: [PATCH 30/39] Minor tweaks --- src/PhpSpreadsheet/Reader/Xls.php | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 94fec859..5e287472 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -7937,7 +7937,6 @@ class Xls extends BaseReader private function readCFHeader(): array { -// var_dump('FOUND CF HEADER'); $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -7949,7 +7948,7 @@ class Xls extends BaseReader } // offset: 0; size: 2; Rule Count - $ruleCount = self::getUInt2d($recordData, 0); +// $ruleCount = self::getUInt2d($recordData, 0); // offset: var; size: var; cell range address list with $cellRangeAddressList = ($this->version == self::XLS_BIFF8) @@ -7957,14 +7956,11 @@ class Xls extends BaseReader : $this->readBIFF5CellRangeAddressList(substr($recordData, 12)); $cellRangeAddresses = $cellRangeAddressList['cellRangeAddresses']; -// var_dump($ruleCount, $cellRangeAddresses); -// return $cellRangeAddresses; } private function readCFRule(array $cellRangeAddresses): void { -// var_dump('FOUND CF RULE'); $length = self::getUInt2d($this->data, $this->pos + 2); $recordData = $this->readRecordData($this->data, $this->pos + 4, $length); @@ -8040,15 +8036,12 @@ class Xls extends BaseReader $offset += 2; } -// var_dump($type, $operator); -// $formula1 = $formula2 = null; if ($size1 > 0) { $formula1 = $this->readCFFormula($recordData, $offset, $size1); if ($formula1 === null) { return; } -// var_dump($formula1); $offset += $size1; } @@ -8058,7 +8051,6 @@ class Xls extends BaseReader if ($formula2 === null) { return; } -// var_dump($formula2); $offset += $size2; } @@ -8073,20 +8065,19 @@ class Xls extends BaseReader private function getCFFontStyle(string $options, Style $style): void { $fontSize = self::getInt4d($options, 64); - $fontSize = ($fontSize !== -1) ? $fontSize / 20 : -1; + if ($fontSize !== -1) { + $style->getFont()->setSize($fontSize / 20); // Convert twips to points + } + + $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold + $style->getFont()->setBold($bold); + $color = self::getInt4d($options, 80); - if ($fontSize !== -1) { - $style->getFont()->setSize($fontSize); - } if ($color !== -1) { - $style->getFont()->setColor( - new \PhpOffice\PhpSpreadsheet\Style\Color(Color::map($color, $this->palette, $this->version)['rgb']) - ); + $style->getFont()->getColor()->setRGB(Color::map($color, $this->palette, $this->version)['rgb']); } - $style->getFont() - ->setBold($bold); } private function getCFAlignmentStyle(string $options, Style $style): void From be8c4449514507cc1baeca72ab89fde10a36d7c0 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 19 Mar 2022 16:15:48 +0100 Subject: [PATCH 31/39] More minor tweaks --- CHANGELOG.md | 3 +++ src/PhpSpreadsheet/Reader/Xls.php | 4 +--- .../Reader/XLS/CF_Expression_Comparisons.xls | Bin 26624 -> 26624 bytes 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc40f8a..b8aa5771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. - Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) +- Limited support for Xls Reader to handle Conditional Formatting: + + Ranges and Rules are read, but style is currently limited to font size, weight and color. ### Changed diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 5e287472..0b2d17a8 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -7,7 +7,6 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\NamedRange; -use PhpOffice\PhpSpreadsheet\Reader\Xls\Color; use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -8069,14 +8068,13 @@ class Xls extends BaseReader $style->getFont()->setSize($fontSize / 20); // Convert twips to points } - $bold = self::getUInt2d($options, 72) === 700; // 400 = normal, 700 = bold $style->getFont()->setBold($bold); $color = self::getInt4d($options, 80); if ($color !== -1) { - $style->getFont()->getColor()->setRGB(Color::map($color, $this->palette, $this->version)['rgb']); + $style->getFont()->getColor()->setRGB(Xls\Color::map($color, $this->palette, $this->version)['rgb']); } } diff --git a/tests/data/Reader/XLS/CF_Expression_Comparisons.xls b/tests/data/Reader/XLS/CF_Expression_Comparisons.xls index 34487aa882c7aaf3c6ff1dd19e290a3bc1e3bc2b..c9c8fe42203346b8a04ec8df3054fa0898ce5b6e 100644 GIT binary patch delta 1457 zcmb_bT}TvB6h3!nciq{Y9cNbEpRk;*EKqmX2hF5tM@x{fu*f2M5G%4G2&`UPBYhOh zqe)pL1p45Ewrf3vV9;ZN7J)tV;A3P^P=Uq5bY}LBTd@^H7v`LE&-a~g&bi#%yTp2z z*i4szNnx}3F~B|{%AVtd5F@J#rTA9xQ#9@jqwEx|To`NZ$Xg0G7yvlHB!|7bf8OrN zZ^xacEXjE5V9t>*!K|fa>Thz{`Z|w-k^@3NYJ|O>eD#dWl+?|BGcR3P2Mgk&Clm>i z_!mP1n5Dp6&S`B0N+F0wLG%ts6Jm%IF8WN{DgH@kupm0cbqd$o=!b%}c87merrx@} zlNgi&sQKKHVj9nyby!tLeWN~b-m-xD)6*LHyY;^skb5pu7A%!BcjZ*D=#e9X`n;Sc&1PkDxbj(@nnr%fR zpyRkdgq5Xn%Qu$g{DEN3cZG8PN;v0_mgM|-&cDj}L!3Xs`A;~1jPs{C{~6~mbN(vl z8=Suoh~o+BXNA{&=5-#TNh zPRcF#SZ&aa)w@Pa@AS}>G~Yowoi-qqN*S7_i3M`rUvsD`Wq7kV9=8;VIGju-*A;AO zp0gB0WJ<}962yL0!K@U;Q-R|YCsG5RZW!=TBYX+uJ-|Q$!AGG1hRciMZWZ+JZj^dno8ddT1&Z z^iZfiZK(%QN)_zEHc@&Js~{*^uoMNM9=s@e(qaWI!aB3bOjL4cJBQgf@B7~O-kW{9 z*(H))BC~xGIfs^%r28Fbe<3wF9s^Z(z_B9O?DjiWRMlaO0DucvP*gSkJ?DR|@(K@nW%YFH~P#SEP?&!Cnx3S}`37npy$nW(QMIxEv$`8D~si@0#rXU15z zA9e!Z0X5ii-%or116-I?%Aq%kXj(C;!WBQ;hs!d#&Rkh^rl?asmZQ2_u7(XO)7mbp zcZrHS{bnpkn{zt7rt1^_2|u{Uof<;e=Wihj>m}yloHuZonsI<94#$g6De5bs34m;r zqdr`vsK!yDU#PPJlbRXxb5z`Op~p&0Q#8O)8oLl*i1XTH9M)Qhj&s^kcmLbxX`GC< zPMxVDSXY(7@o+m1R)jEPw&SaC2nQo0*lUC^5^YAs2;huy5IxZlu7pRfn)@BffN>q$ zV{yD>c39KX({_{9<6|$IrqAVaHl$K1JCR5zWqj)YKKy3v!o}z@{H%A%jtXi4t7zQ| za;rPUt%gc&r3LS+;Ef92fZ&Y_URwo^T^78Dg0~`g=LOHydBnQlB}BHzf_Gf-Qehr( zQt%{^?Y3?WIbDwW4!|fIh2oOv(+dPNkl0JM!IqgFq7y<-G4RmOF#$3_l5N}R*Z7{& zcCf=HrG~bsMW@T=Ww9xxr!AFLx3L!U+o~sZ@*3wGPDrOe5v*-oRC?-xUJAf`{(Ix1 zCV$)w@FoAOquUc?qYw1@09ct=ZPnMXpQe{O*v+!l)>?Q1)VQ$MYShPwO&bEBckHa2 zc&MuhUpk>0=+w8V&S`vCU+!8bxqTOjlD)9nzt-`NNO?VTHkdo|vy$w||2pL* Fe*mN6FWUeB From 0a9d15407fa5b070f72d5bf5cf54bce7af93f475 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 19 Mar 2022 19:47:14 +0100 Subject: [PATCH 32/39] Suport fill style and color for reading CF Formats --- CHANGELOG.md | 2 +- src/PhpSpreadsheet/Reader/Xls.php | 21 ++++++++++++++++++ .../Reader/XLS/CF_Expression_Comparisons.xls | Bin 26624 -> 27136 bytes 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57fc4f95..500d1ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) - Limited support for Xls Reader to handle Conditional Formatting: - Ranges and Rules are read, but style is currently limited to font size, weight and color. + Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color. ### Changed diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 7d2e8fc4..402fea9f 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Reader\Xls\ConditionalFormatting; use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\CellFont; +use PhpOffice\PhpSpreadsheet\Reader\Xls\Style\FillPattern; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\CodePage; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -23,6 +24,7 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; @@ -8013,6 +8015,25 @@ class Xls extends BaseReader private function getCFFillStyle(string $options, Style $style): void { + $fillPattern = self::getUInt2d($options, 0); + // bit: 10-15; mask: 0xFC00; type + $fillPattern = (0xFC00 & $fillPattern) >> 10; + $fillPattern = FillPattern::lookup($fillPattern); + $fillPattern = $fillPattern === Fill::FILL_NONE ? Fill::FILL_SOLID : $fillPattern; + + if ($fillPattern !== Fill::FILL_NONE) { + $style->getFill()->setFillType($fillPattern); + + $fillColors = self::getUInt2d($options, 2); + + // bit: 0-6; mask: 0x007F; type + $color1 = (0x007F & $fillColors) >> 0; + $style->getFill()->getStartColor()->setRGB(Xls\Color::map($color1, $this->palette, $this->version)['rgb']); + + // bit: 7-13; mask: 0x3F80; type + $color2 = (0x3F80 & $fillColors) >> 7; + $style->getFill()->getEndColor()->setRGB(Xls\Color::map($color2, $this->palette, $this->version)['rgb']); + } } private function getCFProtectionStyle(string $options, Style $style): void diff --git a/tests/data/Reader/XLS/CF_Expression_Comparisons.xls b/tests/data/Reader/XLS/CF_Expression_Comparisons.xls index c9c8fe42203346b8a04ec8df3054fa0898ce5b6e..58273caeba9bcdc9d5fabb4f1737896536b31a24 100644 GIT binary patch delta 1352 zcmbtU&ubGw6n?Y2NjKR|n$$JvDm97LA2sPuK^v7OK|!cRTTna~q#gthErPTMBj~M# zv`;KZ5B4I`9-4L)EVf1ug4j!i3LZp@e}Tn{rBv59+ubz994ZccJ8$0i-uKPSW|LiH z*(H{1V#kJ?_ew1QFx@r943o_=%t>w>6Fn@2c~^{{X|dX?L5@+80DKw3bxE;^5VpDO zNEIMlT2$=~mM%4af-yJ_b3#!@L$BnsWg0BON0RM_`&`_gJD)C938@THTN;l=UjJ5% z`ab~!d5hqU-eRM%>e>*P8&~A8?k4*mW)$El1$;--__r}o60gaD`DN^ zDBlkTN0m_pqz6{77rW`k6kJil*s4}VCakQ_sa}Fk3L^loth^szlUC&x8wJQ#J8UIa z(E|=!blSX~AjZ@%tHPw(sP8JNDMShpXE&Oqu-jpmpc|mA=pMpp^@{ZU$>fBg`r6fa z9}LM7w`o)7^Adla%;z+wMNl4^;uDiZp6{Y51HJ|*?UZ`IiH_D^^u82@N%Dj z=2AA0iILWC(=-pzTuV4eI7B#1I6|l+a6V2m-3rtb8VHSqCPK3@e>KHMde(&TGq~Q< Q&-UZZYq!;-M1g&RKi=Xf0RR91 delta 1127 zcmbu8%WD%+6voe;N6b7DGcnC1W|4{3SJIjXrB&M0Xz_uo2GML>q#H@Wg&=P1!Vz)R zP2v>`mV%-?7bTr`Q4rd0%|eANT=q|}pdccSXEM2Y=vQ5His1blksN)J8v;u^FOdO@Vq7WP-1i^dG1+>IR=m#M{TC^(0 z=vP6)U5Ss$TJLNpj0}e}!{KZ{tcoeuOP&}k62p)+<5ZHRVJi3Oyucj%LpgMxcbh|O zeG`9iBuWqbdOV6Z`EDBko49d4Vcpn=2t;X0@}e)hv@Gf9&`_N9v1capX?oIkT_5kW zjB2u15C7x5-Kjf5U!(H~UU&HJTV49ulUiNsboK99*IA)?MUNk5&d)V%MOjfmc+okl z(FJ_5BlG}Q*?GPAA(q*OdE6!eb~~AVx`$NiQv<+^F)BF@@c^T}D(=gKXiC+IO3$m= zcyJ(RC%Kbko%lKq5k}jv05IFD9;QvTBILx)YnB?gu3Bq}YM5J{vh48!kuzxk6%Rg1 zThrN=>T8|@*eY7;%#zPne9M{LajDGR=9UdA&K3UpySQxJ#kn$d9&X;asNkMAbq>lC z$|Y_Tmi!qM!MSsuhDQN7a|ndTDCF!0KbEe7hQHl(XF7ohz|(t|>(;BQt7PNKQ{ta` zGr2bXh47YExgXrPWrm8sZQDMExyMnXs1vA@s8guZD2L-^A%hx2jib(@vZ$Q3cjp3W aRE1}?ADpj5f7Tw76n(JxNIip;HTnm4 Date: Sun, 20 Mar 2022 14:03:36 +0100 Subject: [PATCH 33/39] Initial work implementing the FILTER() Lookup/Reference function Tighten up on vector formats; and provide a couple of helper methods for testing row/column vectors --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 2 +- .../Calculation/LookupRef/Filter.php | 65 ++++++++++++++ .../Calculation/LookupRef/Matrix.php | 17 ++++ .../Calculation/LookupRef/Unique.php | 4 +- .../Functions/LookupRef/FilterTest.php | 85 +++++++++++++++++++ .../LookupRef/MatrixHelperFunctionsTest.php | 77 +++++++++++++++++ 7 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Filter.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 500d1ac4..3ac8c9a9 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 UNIQUE() Lookup/Reference (array) function +- Implementation of the FILTER() and UNIQUE() Lookup/Reference (array) function - Implementation of the ISREF() Information function. - Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index b3988260..0f94a7e7 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1040,7 +1040,7 @@ class Calculation ], 'FILTER' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Filter::class, 'filter'], 'argumentCount' => '2-3', ], 'FILTERXML' => [ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php new file mode 100644 index 00000000..a5e7dc17 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -0,0 +1,65 @@ + 1 && + (count($values, COUNT_NORMAL) === 1 || count($values, COUNT_RECURSIVE) === count($values, COUNT_NORMAL)); + } + /** * TRANSPOSE. * diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php b/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php index 67b911aa..2ba51281 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php @@ -29,8 +29,8 @@ class Unique $exactlyOnce = (bool) $exactlyOnce; return ($byColumn === true) - ? self::uniqueByColumn($lookupVector, $exactlyOnce) - : self::uniqueByRow($lookupVector, $exactlyOnce); + ? self::uniqueByColumn($lookupVector, $exactlyOnce) + : self::uniqueByRow($lookupVector, $exactlyOnce); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php new file mode 100644 index 00000000..710a42e0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php @@ -0,0 +1,85 @@ +sampleDataForRow(), $criteria); + self::assertSame($expectedResult, $result); + } + + public function testFilterByColumn(): void + { + $criteria = [[false, false, true, false, true, false, false, false, true, true]]; + $expectedResult = [ + ['Betty', 'Charlotte', 'Oliver', 'Zoe'], + ['B', 'B', 'B', 'B'], + [1, 2, 4, 8], + ]; + $result = Filter::filter($this->sampleDataForColumn(), $criteria); + self::assertSame($expectedResult, $result); + } + + public function testFilterException(): void + { + $criteria = 'INVALID'; + $result = Filter::filter($this->sampleDataForColumn(), $criteria); + self::assertSame(ExcelError::VALUE(), $result); + } + + public function testFilterEmpty(): void + { + $criteria = [[false], [false], [false]]; + $expectedResult = ExcelError::CALC(); + $result = Filter::filter([[1], [2], [3]], $criteria); + self::assertSame($expectedResult, $result); + + $expectedResult = 'Invalid Data'; + $result = Filter::filter([[1], [2], [3]], $criteria, $expectedResult); + self::assertSame($expectedResult, $result); + } + + protected function sampleDataForRow(): array + { + return [ + ['East', 'Tom', 'Apple', 6830], + ['West', 'Fred', 'Grape', 5619], + ['North', 'Amy', 'Pear', 4565], + ['South', 'Sal', 'Banana', 5323], + ['East', 'Fritz', 'Apple', 4394], + ['West', 'Sravan', 'Grape', 7195], + ['North', 'Xi', 'Pear', 5231], + ['South', 'Hector', 'Banana', 2427], + ['East', 'Tom', 'Banana', 4213], + ['West', 'Fred', 'Pear', 3239], + ['North', 'Amy', 'Grape', 6420], + ['South', 'Sal', 'Apple', 1310], + ['East', 'Fritz', 'Banana', 6274], + ['West', 'Sravan', 'Pear', 4894], + ['North', 'Xi', 'Grape', 7580], + ['South', 'Hector', 'Apple', 98144], + ]; + } + + protected function sampleDataForColumn(): array + { + return [ + ['Aiden', 'Andrew', 'Betty', 'Caden', 'Charlotte', 'Emma', 'Isabella', 'Mason', 'Oliver', 'Zoe'], + ['A', 'C', 'B', 'A', 'B', 'C', 'A', 'A', 'B', 'B'], + [0, 4, 1, 2, 2, 0, 2, 4, 4, 8], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php new file mode 100644 index 00000000..e5b4cace --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/MatrixHelperFunctionsTest.php @@ -0,0 +1,77 @@ + Date: Mon, 21 Mar 2022 13:17:25 -0700 Subject: [PATCH 34/39] Resolve Scrutinizer Reports for Samples (#2691) These are handled about 50-50 between code changes when reasonable, and annotations when not. No source code is changed. --- samples/Basic/44_Worksheet_info.php | 7 ++++--- .../01_Simple_file_reader_using_IOFactory.php | 2 +- ..._Simple_file_reader_using_a_specified_reader.php | 2 +- ...eader_using_the_IOFactory_to_return_a_reader.php | 2 +- ...ng_the_IOFactory_to_identify_a_reader_to_use.php | 4 ++-- ..._file_reader_using_the_read_data_only_option.php | 2 +- ...06_Simple_file_reader_loading_all_worksheets.php | 2 +- ...file_reader_loading_a_single_named_worksheet.php | 2 +- ...file_reader_loading_several_named_worksheets.php | 2 +- .../09_Simple_file_reader_using_a_read_filter.php | 2 +- ...file_reader_using_a_configurable_read_filter.php | 2 +- ...using_a_configurable_read_filter_(version_1).php | 2 +- ...using_a_configurable_read_filter_(version_2).php | 2 +- ...13_Simple_file_reader_for_multiple_CSV_files.php | 13 ++++++------- ...n_chunks_to_split_across_multiple_worksheets.php | 7 +++---- ...d_value_file_using_the_Advanced_Value_Binder.php | 9 ++++----- ...16_Handling_loader_exceptions_using_TryCatch.php | 2 +- ...file_reader_loading_several_named_worksheets.php | 7 +++---- ...st_of_worksheets_without_loading_entire_file.php | 7 +++---- ...heet_information_without_loading_entire_file.php | 6 +++--- .../Reader/20_Reader_worksheet_hyperlink_image.php | 3 +++ ...r_CSV_Long_Integers_with_String_Value_Binder.php | 4 ++-- 22 files changed, 45 insertions(+), 46 deletions(-) diff --git a/samples/Basic/44_Worksheet_info.php b/samples/Basic/44_Worksheet_info.php index 57822369..406c7be2 100644 --- a/samples/Basic/44_Worksheet_info.php +++ b/samples/Basic/44_Worksheet_info.php @@ -1,18 +1,19 @@ getTemporaryFilename(); -$writer = new Xlsx($sampleSpreadsheet); +$writer = new Writer($sampleSpreadsheet); $writer->save($filename); $inputFileType = IOFactory::identify($filename); -$reader = IOFactory::createReader($inputFileType); +$reader = new Reader(); $sheetList = $reader->listWorksheetNames($filename); $sheetInfo = $reader->listWorksheetInfo($filename); diff --git a/samples/Reader/01_Simple_file_reader_using_IOFactory.php b/samples/Reader/01_Simple_file_reader_using_IOFactory.php index 584fd5be..622df231 100644 --- a/samples/Reader/01_Simple_file_reader_using_IOFactory.php +++ b/samples/Reader/01_Simple_file_reader_using_IOFactory.php @@ -5,7 +5,7 @@ use PhpOffice\PhpSpreadsheet\IOFactory; require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); $spreadsheet = IOFactory::load($inputFileName); $sheetData = $spreadsheet->getActiveSheet()->toArray(null, true, true, true); var_dump($sheetData); diff --git a/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php b/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php index 9a705123..9d4ea686 100644 --- a/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php +++ b/samples/Reader/02_Simple_file_reader_using_a_specified_reader.php @@ -5,7 +5,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xls; require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using ' . Xls::class); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using ' . Xls::class); $reader = new Xls(); $spreadsheet = $reader->load($inputFileName); diff --git a/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php b/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php index 305651de..47b5264c 100644 --- a/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php +++ b/samples/Reader/03_Simple_file_reader_using_the_IOFactory_to_return_a_reader.php @@ -7,7 +7,7 @@ require __DIR__ . '/../Header.php'; $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $spreadsheet = $reader->load($inputFileName); diff --git a/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php b/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php index 98aabfc6..3a7dd065 100644 --- a/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php +++ b/samples/Reader/04_Simple_file_reader_using_the_IOFactory_to_identify_a_reader_to_use.php @@ -7,9 +7,9 @@ require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; $inputFileType = IOFactory::identify($inputFileName); -$helper->log('File ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' has been identified as an ' . $inputFileType . ' file'); +$helper->log('File ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' has been identified as an ' . $inputFileType . ' file'); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with the identified reader type'); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with the identified reader type'); $reader = IOFactory::createReader($inputFileType); $spreadsheet = $reader->load($inputFileName); diff --git a/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php b/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php index d3ce9d82..912040a1 100644 --- a/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php +++ b/samples/Reader/05_Simple_file_reader_using_the_read_data_only_option.php @@ -7,7 +7,7 @@ require __DIR__ . '/../Header.php'; $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Turning Formatting off for Load'); $reader->setReadDataOnly(true); diff --git a/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php b/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php index 5507c52b..247fef11 100644 --- a/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php +++ b/samples/Reader/06_Simple_file_reader_loading_all_worksheets.php @@ -7,7 +7,7 @@ require __DIR__ . '/../Header.php'; $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading all WorkSheets'); $reader->setLoadAllSheets(); diff --git a/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php b/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php index 142a17f8..a78de203 100644 --- a/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php +++ b/samples/Reader/07_Simple_file_reader_loading_a_single_named_worksheet.php @@ -8,7 +8,7 @@ $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; $sheetname = 'Data Sheet #2'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet "' . $sheetname . '" only'); $reader->setLoadSheetsOnly($sheetname); diff --git a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php index 66efc3e0..0cc58518 100644 --- a/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php +++ b/samples/Reader/08_Simple_file_reader_loading_several_named_worksheets.php @@ -8,7 +8,7 @@ $inputFileType = 'Xls'; $inputFileName = __DIR__ . '/sampleData/example1.xls'; $sheetnames = ['Data Sheet #1', 'Data Sheet #3']; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet' . ((count($sheetnames) == 1) ? '' : 's') . ' "' . implode('" and "', $sheetnames) . '" only'); $reader->setLoadSheetsOnly($sheetnames); diff --git a/samples/Reader/09_Simple_file_reader_using_a_read_filter.php b/samples/Reader/09_Simple_file_reader_using_a_read_filter.php index 04c47c64..4603164c 100644 --- a/samples/Reader/09_Simple_file_reader_using_a_read_filter.php +++ b/samples/Reader/09_Simple_file_reader_using_a_read_filter.php @@ -28,7 +28,7 @@ class MyReadFilter implements IReadFilter $filterSubset = new MyReadFilter(); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet "' . $sheetname . '" only'); $reader->setLoadSheetsOnly($sheetname); diff --git a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php index 6a600d43..82ca2234 100644 --- a/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php +++ b/samples/Reader/10_Simple_file_reader_using_a_configurable_read_filter.php @@ -40,7 +40,7 @@ class MyReadFilter implements IReadFilter $filterSubset = new MyReadFilter(9, 15, range('G', 'K')); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); $reader = IOFactory::createReader($inputFileType); $helper->log('Loading Sheet "' . $sheetname . '" only'); $reader->setLoadSheetsOnly($sheetname); diff --git a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php index 6c908703..cf070054 100644 --- a/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php +++ b/samples/Reader/11_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_1).php @@ -40,7 +40,7 @@ class ChunkReadFilter implements IReadFilter } } -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); // Create a new Reader of the type defined in $inputFileType $reader = IOFactory::createReader($inputFileType); diff --git a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php index c594c798..13692e3a 100644 --- a/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php +++ b/samples/Reader/12_Reading_a_workbook_in_chunks_using_a_configurable_read_filter_(version_2).php @@ -40,7 +40,7 @@ class ChunkReadFilter implements IReadFilter } } -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); // Create a new Reader of the type defined in $inputFileType $reader = IOFactory::createReader($inputFileType); diff --git a/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php b/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php index d4817e30..9cba25a2 100644 --- a/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php +++ b/samples/Reader/13_Simple_file_reader_for_multiple_CSV_files.php @@ -1,22 +1,21 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using Csv Reader'); $spreadsheet = $reader->load($inputFileName); -$spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); +$spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); foreach ($inputFileNames as $sheet => $inputFileName) { - $helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #' . ($sheet + 2) . ' using IOFactory with a defined reader type of ' . $inputFileType); + $helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #' . ($sheet + 2) . ' using Csv Reader'); $reader->setSheetIndex($sheet + 1); $reader->loadIntoExisting($inputFileName, $spreadsheet); - $spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); + $spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); } $helper->log($spreadsheet->getSheetCount() . ' worksheet' . (($spreadsheet->getSheetCount() == 1) ? '' : 's') . ' loaded'); diff --git a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php index 87fbb225..eab7ec01 100644 --- a/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php +++ b/samples/Reader/14_Reading_a_large_CSV_file_in_chunks_to_split_across_multiple_worksheets.php @@ -2,13 +2,12 @@ namespace Samples\Sample14; -use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Reader\Csv; use PhpOffice\PhpSpreadsheet\Reader\IReadFilter; use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../Header.php'; -$inputFileType = 'Csv'; $inputFileName = __DIR__ . '/sampleData/example2.csv'; /** Define a Read Filter class implementing IReadFilter */ @@ -41,9 +40,9 @@ class ChunkReadFilter implements IReadFilter } } -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using Csv reader'); // Create a new Reader of the type defined in $inputFileType -$reader = IOFactory::createReader($inputFileType); +$reader = new Csv(); // Define how many rows we want to read for each "chunk" $chunkSize = 100; diff --git a/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php b/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php index 8213678a..848452c7 100644 --- a/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php +++ b/samples/Reader/15_Simple_file_reader_for_tab_separated_value_file_using_the_Advanced_Value_Binder.php @@ -2,20 +2,19 @@ use PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder; use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Reader\Csv; require __DIR__ . '/../Header.php'; Cell::setValueBinder(new AdvancedValueBinder()); -$inputFileType = 'Csv'; $inputFileName = __DIR__ . '/sampleData/example1.tsv'; -$reader = IOFactory::createReader($inputFileType); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); +$reader = new Csv(); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using Csv reader'); $reader->setDelimiter("\t"); $spreadsheet = $reader->load($inputFileName); -$spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); +$spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); $helper->log($spreadsheet->getSheetCount() . ' worksheet' . (($spreadsheet->getSheetCount() == 1) ? '' : 's') . ' loaded'); $loadedSheetNames = $spreadsheet->getSheetNames(); diff --git a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php index 5b102967..603f6cb8 100644 --- a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php +++ b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php @@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; require __DIR__ . '/../Header.php'; $inputFileName = __DIR__ . '/sampleData/non-existing-file.xls'; -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory to identify the format'); try { $spreadsheet = IOFactory::load($inputFileName); diff --git a/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php b/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php index db30bff8..12c3c113 100644 --- a/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php +++ b/samples/Reader/17_Simple_file_reader_loading_several_named_worksheets.php @@ -1,14 +1,13 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' using IOFactory with a defined reader type of ' . $inputFileType); -$reader = IOFactory::createReader($inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' using Xls reader'); +$reader = new Xls(); // Read the list of Worksheet Names from the Workbook file $helper->log('Read the list of Worksheets in the WorkBook'); diff --git a/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php b/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php index bb58a2d5..6d2c3db9 100644 --- a/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php +++ b/samples/Reader/18_Reading_list_of_worksheets_without_loading_entire_file.php @@ -1,15 +1,14 @@ log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' information using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' information using Xls reader'); -$reader = IOFactory::createReader($inputFileType); +$reader = new Xls(); $worksheetNames = $reader->listWorksheetNames($inputFileName); $helper->log('

Worksheet Names

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

Worksheet Information

'); diff --git a/samples/Reader/20_Reader_worksheet_hyperlink_image.php b/samples/Reader/20_Reader_worksheet_hyperlink_image.php index 19d837a5..2b3f294a 100644 --- a/samples/Reader/20_Reader_worksheet_hyperlink_image.php +++ b/samples/Reader/20_Reader_worksheet_hyperlink_image.php @@ -13,6 +13,9 @@ $spreadsheet = new Spreadsheet(); $aSheet = $spreadsheet->getActiveSheet(); $gdImage = @imagecreatetruecolor(120, 20); +if ($gdImage === false) { + throw new \Exception('imagecreatetruecolor failed'); +} $textColor = imagecolorallocate($gdImage, 255, 255, 255); imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); diff --git a/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php b/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php index 2c80de3b..ec37b39a 100644 --- a/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php +++ b/samples/Reader/21_Reader_CSV_Long_Integers_with_String_Value_Binder.php @@ -12,10 +12,10 @@ $inputFileType = 'Csv'; $inputFileName = __DIR__ . '/sampleData/longIntegers.csv'; $reader = IOFactory::createReader($inputFileType); -$helper->log('Loading file ' . pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); +$helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . ' into WorkSheet #1 using IOFactory with a defined reader type of ' . $inputFileType); $spreadsheet = $reader->load($inputFileName); -$spreadsheet->getActiveSheet()->setTitle(pathinfo($inputFileName, PATHINFO_BASENAME)); +$spreadsheet->getActiveSheet()->setTitle(/** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME)); $helper->log($spreadsheet->getSheetCount() . ' worksheet' . (($spreadsheet->getSheetCount() == 1) ? '' : 's') . ' loaded'); $loadedSheetNames = $spreadsheet->getSheetNames(); From c112802023bfb17a61f30ca81def0991cab03eab Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 21 Mar 2022 13:58:42 -0700 Subject: [PATCH 35/39] Eliminate Most Scrutinizer Problems in Test Suite (#2699) * Eliminate Most Scrutinizer Problems in Test Suite Mostly minor code changes, with some annotations. * Missed 2 php-cs-fixer Problems They should be fixed now. --- .../CalculationFunctionListTest.php | 2 +- .../Functions/Engineering/ComplexTest.php | 16 ++++++----- .../Functions/Financial/DollarDeTest.php | 14 +++++----- .../Functions/Financial/DollarFrTest.php | 14 +++++----- .../Functions/Financial/FvTest.php | 20 ++++++++----- .../Functions/Financial/NPerTest.php | 20 ++++++++----- .../Functions/Financial/PDurationTest.php | 16 ++++++----- .../Functions/Financial/PmtTest.php | 14 +++++----- .../Functions/Financial/PvTest.php | 20 ++++++++----- .../Functions/Financial/RriTest.php | 16 ++++++----- .../Functions/Financial/UsDollarTest.php | 8 ++++-- .../Calculation/Functions/Logical/IfTest.php | 16 ++++++----- .../Calculation/Functions/Logical/NotTest.php | 12 ++++---- .../Functions/MathTrig/RandArrayTest.php | 4 +-- .../Functions/MathTrig/RandBetweenTest.php | 2 +- .../Functions/MathTrig/SequenceTest.php | 14 ++++++++-- .../Functions/Statistical/GrowthTest.php | 11 ++++++-- .../Functions/Statistical/TrendTest.php | 11 ++++++-- .../Functions/Web/WebServiceTest.php | 2 +- .../Calculation/FunctionsTest.php | 6 ++-- .../Cell/AddressHelperTest.php | 18 ++++++------ .../Cell/CoordinateTest.php | 2 +- .../Document/PropertiesTest.php | 12 ++++++-- tests/PhpSpreadsheetTests/IOFactoryTest.php | 28 +------------------ tests/PhpSpreadsheetTests/Shared/DateTest.php | 4 +-- tests/PhpSpreadsheetTests/Style/ColorTest.php | 27 ++++++++++++------ .../Worksheet/ColumnCellIteratorTest.php | 2 +- 27 files changed, 184 insertions(+), 147 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php index bb71ee6e..e95c3267 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php @@ -41,7 +41,7 @@ class CalculationFunctionListTest extends TestCase * @param array|string $functionCall * @param string $argumentCount */ - public function testGetFunctions($category, $functionCall, $argumentCount): void + public function testGetFunctions(/** @scrutinizer ignore-unused */ $category, $functionCall, /** @scrutinizer ignore-unused */ $argumentCount): void { self::assertIsCallable($functionCall); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php index 451908e0..827654ad 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ComplexTest.php @@ -4,16 +4,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ComplexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOMPLEX * @@ -21,7 +15,15 @@ class ComplexTest extends TestCase */ public function testCOMPLEX($expectedResult, ...$args): void { - $result = Engineering::COMPLEX(...$args); + if (count($args) === 0) { + $result = Engineering::COMPLEX(); + } elseif (count($args) === 1) { + $result = Engineering::COMPLEX($args[0]); + } elseif (count($args) === 2) { + $result = Engineering::COMPLEX($args[0], $args[1]); + } else { + $result = Engineering::COMPLEX($args[0], $args[1], $args[2]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php index cbb8e031..22d99eca 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarDeTest.php @@ -4,16 +4,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class DollarDeTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerDOLLARDE * @@ -21,7 +15,13 @@ class DollarDeTest extends TestCase */ public function testDOLLARDE($expectedResult, ...$args): void { - $result = Financial::DOLLARDE(...$args); + if (count($args) === 0) { + $result = Financial::DOLLARDE(); + } elseif (count($args) === 1) { + $result = Financial::DOLLARDE($args[0]); + } else { + $result = Financial::DOLLARDE($args[0], $args[1]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php index ce74595a..79e3d5ca 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/DollarFrTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class DollarFrTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerDOLLARFR * @@ -20,7 +14,13 @@ class DollarFrTest extends TestCase */ public function testDOLLARFR($expectedResult, ...$args): void { - $result = Financial::DOLLARFR(...$args); + if (count($args) === 0) { + $result = Financial::DOLLARFR(); + } elseif (count($args) === 1) { + $result = Financial::DOLLARFR($args[0]); + } else { + $result = Financial::DOLLARFR($args[0], $args[1]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php index 8d17d852..0e621766 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/FvTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class FvTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerFV * @@ -20,7 +14,19 @@ class FvTest extends TestCase */ public function testFV($expectedResult, array $args): void { - $result = Financial::FV(...$args); + if (count($args) === 0) { + $result = Financial::FV(); + } elseif (count($args) === 1) { + $result = Financial::FV($args[0]); + } elseif (count($args) === 2) { + $result = Financial::FV($args[0], $args[1]); + } elseif (count($args) === 3) { + $result = Financial::FV($args[0], $args[1], $args[2]); + } elseif (count($args) === 4) { + $result = Financial::FV($args[0], $args[1], $args[2], $args[3]); + } else { + $result = Financial::FV($args[0], $args[1], $args[2], $args[3], $args[4]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php index 87230eca..2d7ded04 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/NPerTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class NPerTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerNPER * @@ -20,7 +14,19 @@ class NPerTest extends TestCase */ public function testNPER($expectedResult, array $args): void { - $result = Financial::NPER(...$args); + if (count($args) === 0) { + $result = Financial::NPER(); + } elseif (count($args) === 1) { + $result = Financial::NPER($args[0]); + } elseif (count($args) === 2) { + $result = Financial::NPER($args[0], $args[1]); + } elseif (count($args) === 3) { + $result = Financial::NPER($args[0], $args[1], $args[2]); + } elseif (count($args) === 4) { + $result = Financial::NPER($args[0], $args[1], $args[2], $args[3]); + } else { + $result = Financial::NPER($args[0], $args[1], $args[2], $args[3], $args[4]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php index a8d3095a..dbe1b09a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PDurationTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class PDurationTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPDURATION * @@ -20,7 +14,15 @@ class PDurationTest extends TestCase */ public function testPDURATION($expectedResult, array $args): void { - $result = Financial::PDURATION(...$args); + if (count($args) === 0) { + $result = Financial::PDURATION(); + } elseif (count($args) === 1) { + $result = Financial::PDURATION($args[0]); + } elseif (count($args) === 2) { + $result = Financial::PDURATION($args[0], $args[1]); + } else { + $result = Financial::PDURATION($args[0], $args[1], $args[2]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php index d996db10..5c1f6556 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class PmtTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPMT * @@ -23,7 +17,13 @@ class PmtTest extends TestCase $interestRate = array_shift($args); $numberOfPeriods = array_shift($args); $presentValue = array_shift($args); - $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue, ...$args); + if (count($args) === 0) { + $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue); + } elseif (count($args) === 1) { + $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue, $args[0]); + } else { + $result = Financial::PMT($interestRate, $numberOfPeriods, $presentValue, $args[0], $args[1]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php index c1b40231..4e4a8f80 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PvTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class PvTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPV * @@ -20,7 +14,19 @@ class PvTest extends TestCase */ public function testPV($expectedResult, array $args): void { - $result = Financial::PV(...$args); + if (count($args) === 0) { + $result = Financial::PV(); + } elseif (count($args) === 1) { + $result = Financial::PV($args[0]); + } elseif (count($args) === 2) { + $result = Financial::PV($args[0], $args[1]); + } elseif (count($args) === 3) { + $result = Financial::PV($args[0], $args[1], $args[2]); + } elseif (count($args) === 4) { + $result = Financial::PV($args[0], $args[1], $args[2], $args[3]); + } else { + $result = Financial::PV($args[0], $args[1], $args[2], $args[3], $args[4]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php index e641b590..9107ba96 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/RriTest.php @@ -3,16 +3,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class RriTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerRRI * @@ -20,7 +14,15 @@ class RriTest extends TestCase */ public function testRRI($expectedResult, array $args): void { - $result = Financial::RRI(...$args); + if (count($args) === 0) { + $result = Financial::RRI(); + } elseif (count($args) === 1) { + $result = Financial::RRI($args[0]); + } elseif (count($args) === 2) { + $result = Financial::RRI($args[0], $args[1]); + } else { + $result = Financial::RRI($args[0], $args[1], $args[2]); + } self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php index 770b8ffa..db8fa112 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/UsDollarTest.php @@ -12,9 +12,13 @@ class UsDollarTest extends TestCase * * @param mixed $expectedResult */ - public function testUSDOLLAR($expectedResult, ...$args): void + public function testUSDOLLAR($expectedResult, float $amount, ?int $precision = null): void { - $result = Dollar::format(...$args); + if ($precision === null) { + $result = Dollar::format($amount); + } else { + $result = Dollar::format($amount, $precision); + } self::assertSame($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php index b3e21986..58997161 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfTest.php @@ -2,17 +2,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Logical; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Logical; use PHPUnit\Framework\TestCase; class IfTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerIF * @@ -20,7 +14,15 @@ class IfTest extends TestCase */ public function testIF($expectedResult, ...$args): void { - $result = Logical::statementIf(...$args); + if (count($args) === 0) { + $result = Logical::statementIf(); + } elseif (count($args) === 1) { + $result = Logical::statementIf($args[0]); + } elseif (count($args) === 2) { + $result = Logical::statementIf($args[0], $args[1]); + } else { + $result = Logical::statementIf($args[0], $args[1], $args[2]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php index 03eb15ba..6d8b2ca9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/NotTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Logical; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Logical; use PHPUnit\Framework\TestCase; class NotTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerNOT * @@ -21,7 +15,11 @@ class NotTest extends TestCase */ public function testNOT($expectedResult, ...$args): void { - $result = Logical::NOT(...$args); + if (count($args) === 0) { + $result = Logical::NOT(); + } else { + $result = Logical::NOT($args[0]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandArrayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandArrayTest.php index 6e63e5ca..6fffb31c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandArrayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandArrayTest.php @@ -17,7 +17,7 @@ class RandArrayTest extends AllSetupTeardown $result = MathTrig\Random::randArray($rows, $cols, $min, $max, true); self::assertIsArray($result); - self::assertCount($rows, $result); + self::assertCount($rows, /** @scrutinizer ignore-type */ $result); self::assertIsArray($result[0]); self::assertCount($cols, $result[0]); @@ -40,7 +40,7 @@ class RandArrayTest extends AllSetupTeardown $result = MathTrig\Random::randArray($rows, $cols, $min, $max, false); self::assertIsArray($result); - self::assertCount($rows, $result); + self::assertCount($rows, /** @scrutinizer ignore-type */ $result); self::assertIsArray($result[0]); self::assertCount($cols, $result[0]); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php index bd89d501..99cc1fa8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php @@ -62,7 +62,7 @@ class RandBetweenTest extends AllSetupTeardown self::assertIsArray($result); self::assertCount($expectedRows, $result); self::assertIsArray($result[0]); - self::assertCount($expectedColumns, $result[0]); + self::assertCount($expectedColumns, /** @scrutinizer ignore-type */ $result[0]); } public function providerRandBetweenArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SequenceTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SequenceTest.php index d2ba3345..9346a6cc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SequenceTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SequenceTest.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\MatrixFunctions; class SequenceTest extends AllSetupTeardown { @@ -14,7 +14,17 @@ class SequenceTest extends AllSetupTeardown */ public function testSEQUENCE(array $arguments, $expectedResult): void { - $result = MathTrig\MatrixFunctions::sequence(...$arguments); + if (count($arguments) === 0) { + $result = MatrixFunctions::sequence(); + } elseif (count($arguments) === 1) { + $result = MatrixFunctions::sequence($arguments[0]); + } elseif (count($arguments) === 2) { + $result = MatrixFunctions::sequence($arguments[0], $arguments[1]); + } elseif (count($arguments) === 3) { + $result = MatrixFunctions::sequence($arguments[0], $arguments[1], $arguments[2]); + } else { + $result = MatrixFunctions::sequence($arguments[0], $arguments[1], $arguments[2], $arguments[3]); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php index a5ed8223..a796c370 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php @@ -17,11 +17,16 @@ class GrowthTest extends TestCase * @dataProvider providerGROWTH * * @param mixed $expectedResult - * @param mixed $yValues */ - public function testGROWTH($expectedResult, $yValues, ...$args): void + public function testGROWTH($expectedResult, array $yValues, array $xValues, ?array $newValues = null, ?bool $const = null): void { - $result = Statistical::GROWTH($yValues, ...$args); + if ($newValues === null) { + $result = Statistical::GROWTH($yValues, $xValues); + } elseif ($const === null) { + $result = Statistical::GROWTH($yValues, $xValues, $newValues); + } else { + $result = Statistical::GROWTH($yValues, $xValues, $newValues, $const); + } self::assertEqualsWithDelta($expectedResult, $result[0], 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php index c367cb10..2cb06b77 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php @@ -17,11 +17,16 @@ class TrendTest extends TestCase * @dataProvider providerGROWTH * * @param mixed $expectedResult - * @param mixed $yValues */ - public function testTREND($expectedResult, $yValues, ...$args): void + public function testTREND($expectedResult, array $yValues, array $xValues, ?array $newValues = null, ?bool $const = null): void { - $result = Statistical::TREND($yValues, ...$args); + if ($newValues === null) { + $result = Statistical::TREND($yValues, $xValues); + } elseif ($const === null) { + $result = Statistical::TREND($yValues, $xValues, $newValues); + } else { + $result = Statistical::TREND($yValues, $xValues, $newValues, $const); + } self::assertEqualsWithDelta($expectedResult, $result[0], 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php index 2aff7b3d..c458fd22 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Web/WebServiceTest.php @@ -23,7 +23,7 @@ class WebServiceTest extends TestCase */ public function testWEBSERVICE(string $expectedResult, string $url, ?array $responseData): void { - if ($responseData) { + if (!empty($responseData)) { $body = $this->createMock(StreamInterface::class); $body->expects(self::atMost(1))->method('getContents')->willReturn($responseData[1]); diff --git a/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php index c12bf060..9ea62400 100644 --- a/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/FunctionsTest.php @@ -76,12 +76,10 @@ class FunctionsTest extends TestCase /** * @dataProvider providerIfCondition - * - * @param mixed $expectedResult */ - public function testIfCondition($expectedResult, ...$args): void + public function testIfCondition(string $expectedResult, string $args): void { - $result = Functions::ifCondition(...$args); + $result = Functions::ifCondition($args); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php b/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php index 58c696ef..f82a4ead 100644 --- a/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php @@ -32,15 +32,17 @@ class AddressHelperTest extends TestCase ?int $row = null, ?int $column = null ): void { - $args = []; - if ($row !== null) { - $args[] = $row; + if ($row === null) { + if ($column === null) { + $actualValue = AddressHelper::convertToA1($address); + } else { + $actualValue = AddressHelper::convertToA1($address, $column); + } + } elseif ($column === null) { + $actualValue = AddressHelper::convertToA1($address, $row); + } else { + $actualValue = AddressHelper::convertToA1($address, $row, $column); } - if ($column !== null) { - $args[] = $column; - } - - $actualValue = AddressHelper::convertToA1($address, ...$args); self::assertSame($expectedValue, $actualValue); } diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 9a0a2c21..8ed23427 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -265,7 +265,7 @@ class CoordinateTest extends TestCase $cellRange = null; // @phpstan-ignore-next-line - Coordinate::buildRange($cellRange); + Coordinate::buildRange(/** @scrutinizer ignore-type */ $cellRange); } public function testBuildRangeInvalid2(): void diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php index c539da09..e6c95cd4 100644 --- a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -155,11 +155,17 @@ class PropertiesTest extends TestCase * * @param mixed $expectedType * @param mixed $expectedValue - * @param mixed $propertyName + * @param string $propertyName + * @param mixed $propertyValue + * @param ?string $propertyType */ - public function testSetCustomProperties($expectedType, $expectedValue, $propertyName, ...$args): void + public function testSetCustomProperties($expectedType, $expectedValue, $propertyName, $propertyValue, $propertyType = null): void { - $this->properties->setCustomProperty($propertyName, ...$args); + if ($propertyType === null) { + $this->properties->setCustomProperty($propertyName, $propertyValue); + } else { + $this->properties->setCustomProperty($propertyName, $propertyValue, $propertyType); + } self::assertTrue($this->properties->isCustomPropertySet($propertyName)); self::assertSame($expectedType, $this->properties->getCustomPropertyType($propertyName)); $result = $this->properties->getCustomPropertyValue($propertyName); diff --git a/tests/PhpSpreadsheetTests/IOFactoryTest.php b/tests/PhpSpreadsheetTests/IOFactoryTest.php index 958c6eb0..b516a9f4 100644 --- a/tests/PhpSpreadsheetTests/IOFactoryTest.php +++ b/tests/PhpSpreadsheetTests/IOFactoryTest.php @@ -81,39 +81,13 @@ class IOFactoryTest extends TestCase /** * @dataProvider providerIdentify - * - * @param string $file - * @param string $expectedName - * @param string $expectedClass */ - public function testIdentify($file, $expectedName, $expectedClass): void + public function testIdentifyCreateLoad(string $file, string $expectedName, string $expectedClass): void { $actual = IOFactory::identify($file); self::assertSame($expectedName, $actual); - } - - /** - * @dataProvider providerIdentify - * - * @param string $file - * @param string $expectedName - * @param string $expectedClass - */ - public function testCreateReaderForFile($file, $expectedName, $expectedClass): void - { $actual = IOFactory::createReaderForFile($file); self::assertSame($expectedClass, get_class($actual)); - } - - /** - * @dataProvider providerIdentify - * - * @param string $file - * @param string $expectedName - * @param string $expectedClass - */ - public function testLoad($file, $expectedName, $expectedClass): void - { $actual = IOFactory::load($file); self::assertInstanceOf(Spreadsheet::class, $actual); } diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 9fb4d919..90ed5cc2 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -176,9 +176,9 @@ class DateTest extends TestCase * * @param mixed $expectedResult */ - public function testIsDateTimeFormatCode($expectedResult, ...$args): void + public function testIsDateTimeFormatCode($expectedResult, string $format): void { - $result = Date::isDateTimeFormatCode(...$args); + $result = Date::isDateTimeFormatCode($format); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Style/ColorTest.php b/tests/PhpSpreadsheetTests/Style/ColorTest.php index 6575c850..9767c537 100644 --- a/tests/PhpSpreadsheetTests/Style/ColorTest.php +++ b/tests/PhpSpreadsheetTests/Style/ColorTest.php @@ -78,11 +78,14 @@ class ColorTest extends TestCase * @dataProvider providerColorGetRed * * @param mixed $expectedResult - * @param mixed $color */ - public function testGetRed($expectedResult, $color, ...$args): void + public function testGetRed($expectedResult, string $color, ?bool $bool = null): void { - $result = Color::getRed($color, ...$args); + if ($bool === null) { + $result = Color::getRed($color); + } else { + $result = Color::getRed($color, $bool); + } self::assertEquals($expectedResult, $result); } @@ -95,11 +98,14 @@ class ColorTest extends TestCase * @dataProvider providerColorGetGreen * * @param mixed $expectedResult - * @param mixed $color */ - public function testGetGreen($expectedResult, $color, ...$args): void + public function testGetGreen($expectedResult, string $color, ?bool $bool = null): void { - $result = Color::getGreen($color, ...$args); + if ($bool === null) { + $result = Color::getGreen($color); + } else { + $result = Color::getGreen($color, $bool); + } self::assertEquals($expectedResult, $result); } @@ -112,11 +118,14 @@ class ColorTest extends TestCase * @dataProvider providerColorGetBlue * * @param mixed $expectedResult - * @param mixed $color */ - public function testGetBlue($expectedResult, $color, ...$args): void + public function testGetBlue($expectedResult, string $color, ?bool $bool = null): void { - $result = Color::getBlue($color, ...$args); + if ($bool === null) { + $result = Color::getBlue($color); + } else { + $result = Color::getBlue($color, $bool); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php index a81058af..b9e18e43 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php @@ -44,7 +44,7 @@ class ColumnCellIteratorTest extends TestCase self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); } - $transposed = array_map(null, ...self::CELL_VALUES); + $transposed = array_map(/** @scrutinizer ignore-type */ null, ...self::CELL_VALUES); self::assertSame($transposed[0], $values); $spreadsheet->disconnectWorksheets(); } From 6a349ccf5ab13851ec6457d253529caab3985111 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Mon, 21 Mar 2022 14:29:57 -0700 Subject: [PATCH 36/39] Scrutinizer Faulty Analysis (#2704) Despite typehint, Scrutinizer assigns union type to a variable, resulting in subsequent faulty analysis of a method call invoked on that variable. Adding a bogus routine that will never actually be called to one module eliminates 8 Scrutinizer errors. --- src/PhpSpreadsheet/Spreadsheet.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 2b8c8360..52d7fb55 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -1602,4 +1602,14 @@ class Spreadsheet } } } + + /** + * Silliness to mollify Scrutinizer. + * + * @codeCoverageIgnore + */ + public function getSharedComponent(): Style + { + return new Style(); + } } From 7f00049fe81c43d5f9a2e38cbbf1cbdcff3c4c39 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 22 Mar 2022 21:51:52 +0100 Subject: [PATCH 37/39] Initial work on the SORT() and SORTBY() Lookup/Reference functions The code could stil do with some cleaning up, and better optimisation for memory usage; but all tests are passing... that's for full multi-level sorting (including direction), and allowing for correct sorting of sting/numeric datatypes. --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 6 +- .../Calculation/LookupRef/Sort.php | 318 ++++++++++++++++++ .../Functions/LookupRef/SortByTest.php | 129 +++++++ .../Functions/LookupRef/SortTest.php | 210 ++++++++++++ 5 files changed, 661 insertions(+), 4 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Sort.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac8c9a9..c7ae1854 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 FILTER() and UNIQUE() Lookup/Reference (array) function +- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions - Implementation of the ISREF() Information function. - Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 0f94a7e7..b336920c 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2282,12 +2282,12 @@ class Calculation ], 'SORT' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '1+', + 'functionCall' => [LookupRef\Sort::class, 'sort'], + 'argumentCount' => '1-4', ], 'SORTBY' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Sort::class, 'sortBy'], 'argumentCount' => '2+', ], 'SQRT' => [ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php b/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php new file mode 100644 index 00000000..48dc3388 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php @@ -0,0 +1,318 @@ +getMessage(); + } + + // We want a simple, enumrated array of arrays where we can reference column by its index number. + $sortArray = array_values(array_map('array_values', $sortArray)); + + return ($byColumn === true) + ? self::sortByColumn($sortArray, $sortIndex, $sortOrder) + : self::sortByRow($sortArray, $sortIndex, $sortOrder); + } + + /** + * SORTBY + * The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array. + * The returned array is the same shape as the provided array argument. + * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting. + * + * @param mixed $sortArray The range of cells being sorted + * @param mixed $args + * + * @return mixed The sorted values from the sort range + */ + public static function sortBy($sortArray, ...$args) + { + if (!is_array($sortArray)) { + // Scalars are always returned "as is" + return $sortArray; + } + + $lookupArraySize = count($sortArray); + $argumentCount = count($args); + + try { + $sortBy = $sortOrder = []; + for ($i = 0; $i < $argumentCount; $i += 2) { + $sortBy[] = self::validateSortVector($args[$i], $lookupArraySize); + $sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::processSortBy($sortArray, $sortBy, $sortOrder); + } + + /** + * @param mixed $sortIndex + * @param mixed $sortOrder + */ + private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $lookupIndexSize): void + { + if (is_array($sortIndex) || is_array($sortOrder)) { + throw new Exception(ExcelError::VALUE()); + } + + $sortIndex = self::validatePositiveInt($sortIndex, false); + + if ($sortIndex > $lookupIndexSize) { + throw new Exception(ExcelError::VALUE()); + } + + $sortOrder = self::validateSortOrder($sortOrder); + } + + /** + * @param mixed $sortVector + */ + private static function validateSortVector($sortVector, int $lookupArraySize): array + { + if (!is_array($sortVector)) { + throw new Exception(ExcelError::VALUE()); + } + + // It doesn't matter if it's a row or a column vectors, it works either way + $sortVector = Functions::flattenArray($sortVector); + if (count($sortVector) !== $lookupArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortVector; + } + + /** + * @param mixed $sortOrder + */ + private static function validateSortOrder($sortOrder): int + { + $sortOrder = self::validateInt($sortOrder); + if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortOrder; + } + + /** + * @param array $sortIndex + * @param mixed $sortOrder + */ + private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $lookupIndexSize): void + { + // It doesn't matter if they're row or column vectors, it works either way + $sortIndex = Functions::flattenArray($sortIndex); + $sortOrder = Functions::flattenArray($sortOrder); + + if ( + count($sortOrder) === 0 || count($sortOrder) > $lookupIndexSize || + (count($sortOrder) > count($sortIndex)) + ) { + throw new Exception(ExcelError::VALUE()); + } + + if (count($sortIndex) > count($sortOrder)) { + // If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated. + $sortOrder = array_merge( + $sortOrder, + array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder)) + ); + } + + foreach ($sortIndex as $key => &$value) { + self::validateScalarArgumentsForSort($value, $sortOrder[$key], $lookupIndexSize); + } + } + + private static function prepareSortVectorValues(array $sortVector): array + { + // Strings should be sorted case-insensitive; with booleans converted to locale-strings + return array_map( + function ($value) { + if (is_bool($value)) { + return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + } elseif (is_string($value)) { + return StringHelper::strToLower($value); + } + + return $value; + }, + $sortVector + ); + } + + /** + * @param array[] $sortIndex + * @param int[] $sortOrder + */ + private static function processSortBy(array $lookupArray, array $sortIndex, $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortValues) { + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + $sortArguments = self::applyPHP7Patch($lookupArray, $sortArguments); + + $sortVector = self::executeVectorSortQuery($sortData, $sortArguments); + + return self::sortLookupArrayFromVector($lookupArray, $sortVector); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function sortByRow(array $lookupArray, array $sortIndex, array $sortOrder): array + { + $sortVector = self::buildVectorForSort($lookupArray, $sortIndex, $sortOrder); + + return self::sortLookupArrayFromVector($lookupArray, $sortVector); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function sortByColumn(array $lookupArray, array $sortIndex, array $sortOrder): array + { + $lookupArray = Matrix::transpose($lookupArray); + $result = self::sortByRow($lookupArray, $sortIndex, $sortOrder); + + return Matrix::transpose($result); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function buildVectorForSort(array $lookupArray, array $sortIndex, array $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortIndexValue) { + $sortValues = array_column($lookupArray, $sortIndexValue - 1); + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + $sortArguments = self::applyPHP7Patch($lookupArray, $sortArguments); + + $sortData = self::executeVectorSortQuery($sortData, $sortArguments); + + return $sortData; + } + + private static function executeVectorSortQuery(array $sortData, array $sortArguments): array + { + $sortData = Matrix::transpose($sortData); + + // We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys. + $sortDataIndexed = []; + foreach ($sortData as $key => $value) { + $sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value; + } + unset($sortData); + + $sortArguments[] = &$sortDataIndexed; + + array_multisort(...$sortArguments); + + // After the sort, we restore the numeric keys that will now be in the correct, sorted order + $sortedData = []; + foreach (array_keys($sortDataIndexed) as $key) { + $sortedData[] = Coordinate::columnIndexFromString($key) - 1; + } + + return $sortedData; + } + + private static function sortLookupArrayFromVector(array $lookupArray, array $sortVector): array + { + // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays + $sortedArray = []; + foreach ($sortVector as $index) { + $sortedArray[] = $lookupArray[$index]; + } + + return $sortedArray; + +// uksort( +// $lookupArray, +// function (int $a, int $b) use (array $sortVector) { +// return $sortVector[$a] <=> $sortVector[$b]; +// } +// ); +// +// return $lookupArray; + } + + /** + * Hack to handle PHP 7: + * From PHP 8.0.0, If two members compare as equal in a sort, they retain their original order; + * but prior to PHP 8.0.0, their relative order in the sorted array was undefined. + * MS Excel replicates the PHP 8.0.0 behaviour, retaining the original order of matching elements. + * To replicate that behaviour with PHP 7, we add an extra sort based on the row index. + */ + private static function applyPHP7Patch(array $lookupArray, array $sortArguments): array + { + if (PHP_VERSION_ID < 80000) { + $sortArguments[] = range(1, count($lookupArray)); + $sortArguments[] = SORT_ASC; + } + + return $sortArguments; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php new file mode 100644 index 00000000..3acc8b6a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php @@ -0,0 +1,129 @@ + ['A', 1], + 'Mismatched sortIndex count' => [[1, 2, 3, 4], 1], + 'Non-numeric sortOrder' => [[1, 2, 3], 'A'], + 'Invalid negative sortOrder' => [[1, 2, 3], -2], + 'Zero sortOrder' => [[1, 2, 3], 0], + 'Invalid positive sortOrder' => [[1, 2, 3], 2], + ]; + } + + /** + * @dataProvider providerSortByRow + */ + public function testSortByRow(array $expectedResult, array $matrix, array $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void + { + $result = Sort::sortBy($matrix, $sortIndex, $sortOrder); + self::assertSame($expectedResult, $result); + } + + public function providerSortByRow(): array + { + return [ + [ + [ + ['Fritz', 19], + ['Xi', 19], + ['Amy', 22], + ['Srivan', 39], + ['Tom', 52], + ['Fred', 65], + ['Hector', 66], + ['Sal', 73], + ], + $this->sampleDataForRow(), + array_column($this->sampleDataForRow(), 1), + ], + [ + [ + ['Amy', 22], + ['Fred', 65], + ['Fritz', 19], + ['Hector', 66], + ['Sal', 73], + ['Srivan', 39], + ['Tom', 52], + ['Xi', 19], + ], + $this->sampleDataForRow(), + array_column($this->sampleDataForRow(), 0), + ], + [ + [ + ['Amy', 22], + ['Fred', 65], + ['Fritz', 19], + ['Hector', 66], + ['Sal', 73], + ['Srivan', 39], + ['Tom', 52], + ['Xi', 19], + ], + $this->sampleDataForRow(), + ['Tom', 'Fred', 'Amy', 'Sal', 'Fritz', 'Srivan', 'Xi', 'Hector'], + ], + [ + [ + ['Amy', 22], + ['Fred', 65], + ['Fritz', 19], + ['Hector', 66], + ['Sal', 73], + ['Srivan', 39], + ['Tom', 52], + ['Xi', 19], + ], + $this->sampleDataForRow(), + [['Tom'], ['Fred'], ['Amy'], ['Sal'], ['Fritz'], ['Srivan'], ['Xi'], ['Hector']], + ], + ]; + } + + private function sampleDataForRow(): array + { + return [ + ['Tom', 52], + ['Fred', 65], + ['Amy', 22], + ['Sal', 73], + ['Fritz', 19], + ['Srivan', 39], + ['Xi', 19], + ['Hector', 66], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php new file mode 100644 index 00000000..bd120e30 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortTest.php @@ -0,0 +1,210 @@ + [-1, -1], + 'Non-numeric sortIndex' => ['A', -1], + 'Zero sortIndex' => [0, -1], + 'Too high sortIndex' => [3, -1], + 'Non-numeric sortOrder' => [1, 'A'], + 'Invalid negative sortOrder' => [1, -2], + 'Zero sortOrder' => [1, 0], + 'Invalid positive sortOrder' => [1, 2], + 'Too many sortOrders (scalar and array)' => [1, [-1, 1]], + 'Too many sortOrders (both array)' => [[1, 2], [1, 2, 3]], + 'Zero positive sortIndex in vector' => [[0, 1]], + 'Too high sortIndex in vector' => [[1, 3]], + 'Invalid sortOrder in vector' => [[1, 2], [1, -2]], + ]; + } + + /** + * @dataProvider providerSortByRow + */ + public function testSortByRow(array $expectedResult, array $matrix, int $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void + { + $result = Sort::sort($matrix, $sortIndex, $sortOrder); + self::assertSame($expectedResult, $result); + } + + public function providerSortByRow(): array + { + return [ + [ + [[142], [378], [404], [445], [483], [622], [650], [691], [783], [961]], + $this->sampleDataForRow(), + 1, + ], + [ + [[961], [783], [691], [650], [622], [483], [445], [404], [378], [142]], + $this->sampleDataForRow(), + 1, + Sort::ORDER_DESCENDING, + ], + [ + [['Peaches', 25], ['Cherries', 29], ['Grapes', 31], ['Lemons', 34], ['Oranges', 36], ['Apples', 38], ['Pears', 40]], + [['Apples', 38], ['Cherries', 29], ['Grapes', 31], ['Lemons', 34], ['Oranges', 36], ['Peaches', 25], ['Pears', 40]], + 2, + ], + ]; + } + + /** + * @dataProvider providerSortByRowMultiLevel + */ + public function testSortByRowMultiLevel(array $expectedResult, array $matrix, array $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void + { + $result = Sort::sort($matrix, $sortIndex, $sortOrder); + self::assertSame($expectedResult, $result); + } + + public function providerSortByRowMultiLevel(): array + { + return [ + [ + [ + ['East', 'Grapes', 31], + ['East', 'Lemons', 36], + ['North', 'Cherries', 29], + ['North', 'Grapes', 27], + ['North', 'Peaches', 25], + ['South', 'Apples', 38], + ['South', 'Cherries', 28], + ['South', 'Oranges', 36], + ['South', 'Pears', 40], + ['West', 'Apples', 30], + ['West', 'Lemons', 34], + ['West', 'Oranges', 25], + ], + $this->sampleDataForMultiRow(), + [1, 2], + ], + [ + [ + ['East', 'Grapes', 31], + ['East', 'Lemons', 36], + ['North', 'Peaches', 25], + ['North', 'Grapes', 27], + ['North', 'Cherries', 29], + ['South', 'Cherries', 28], + ['South', 'Oranges', 36], + ['South', 'Apples', 38], + ['South', 'Pears', 40], + ['West', 'Oranges', 25], + ['West', 'Apples', 30], + ['West', 'Lemons', 34], + ], + $this->sampleDataForMultiRow(), + [1, 3], + ], + [ + [ + ['West', 'Apples', 30], + ['South', 'Apples', 38], + ['South', 'Cherries', 28], + ['North', 'Cherries', 29], + ['North', 'Grapes', 27], + ['East', 'Grapes', 31], + ['West', 'Lemons', 34], + ['East', 'Lemons', 36], + ['West', 'Oranges', 25], + ['South', 'Oranges', 36], + ['North', 'Peaches', 25], + ['South', 'Pears', 40], + ], + $this->sampleDataForMultiRow(), + [2, 3], + ], + ]; + } + + /** + * @dataProvider providerSortByColumn + */ + public function testSortByColumn(array $expectedResult, array $matrix, int $sortIndex, int $sortOrder): void + { + $result = Sort::sort($matrix, $sortIndex, $sortOrder, true); + self::assertSame($expectedResult, $result); + } + + public function providerSortByColumn(): array + { + return [ + [ + [[142, 378, 404, 445, 483, 622, 650, 691, 783, 961]], + $this->sampleDataForColumn(), + 1, + Sort::ORDER_ASCENDING, + ], + [ + [[961, 783, 691, 650, 622, 483, 445, 404, 378, 142]], + $this->sampleDataForColumn(), + 1, + Sort::ORDER_DESCENDING, + ], + ]; + } + + public function sampleDataForRow(): array + { + return [ + [622], [961], [691], [445], [378], [483], [650], [783], [142], [404], + ]; + } + + public function sampleDataForMultiRow(): array + { + return [ + ['South', 'Pears', 40], + ['South', 'Apples', 38], + ['South', 'Oranges', 36], + ['East', 'Lemons', 36], + ['West', 'Lemons', 34], + ['East', 'Grapes', 31], + ['West', 'Apples', 30], + ['North', 'Cherries', 29], + ['South', 'Cherries', 28], + ['North', 'Grapes', 27], + ['North', 'Peaches', 25], + ['West', 'Oranges', 25], + ]; + } + + public function sampleDataForColumn(): array + { + return [ + [622, 961, 691, 445, 378, 483, 650, 783, 142, 404], + ]; + } +} From 9019523efc2151153776b0be689d09d040755471 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 24 Mar 2022 17:08:22 +0100 Subject: [PATCH 38/39] Additional unit testing And a quick bugfix for cell ranges applied to both sort functions and to FILTER() --- .../Calculation/LookupRef/Filter.php | 16 ++++ .../Calculation/LookupRef/Sort.php | 74 +++++++++++------- .../Functions/LookupRef/SortByTest.php | 75 +++++++++++++++---- 3 files changed, 126 insertions(+), 39 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php index a5e7dc17..6d201531 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -19,6 +19,8 @@ class Filter return ExcelError::VALUE(); } + $matchArray = self::enumerateArrayKeys($matchArray); + $result = (Matrix::isColumnVector($matchArray)) ? self::filterByRow($lookupArray, $matchArray) : self::filterByColumn($lookupArray, $matchArray); @@ -30,6 +32,20 @@ class Filter return array_values($result); } + private static function enumerateArrayKeys(array $sortArray): array + { + array_walk( + $sortArray, + function (&$columns): void { + if (is_array($columns)) { + $columns = array_values($columns); + } + } + ); + + return array_values($sortArray); + } + private static function filterByRow(array $lookupArray, array $matchArray): array { $matchArray = array_values(array_column($matchArray, 0)); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php b/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php index 48dc3388..ff78fbea 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php @@ -21,7 +21,7 @@ class Sort extends LookupRefValidations * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting. * * @param mixed $sortArray The range of cells being sorted - * @param mixed $sortIndex Whether the uniqueness should be determined by row (the default) or by column + * @param mixed $sortIndex The column or row number within the sortArray to sort on * @param mixed $sortOrder Flag indicating whether to sort ascending or descending * Ascending = 1 (self::ORDER_ASCENDING) * Descending = -1 (self::ORDER_DESCENDING) @@ -29,13 +29,15 @@ class Sort extends LookupRefValidations * * @return mixed The sorted values from the sort range */ - public static function sort($sortArray, $sortIndex = [1], $sortOrder = self::ORDER_ASCENDING, $byColumn = false) + public static function sort($sortArray, $sortIndex = 1, $sortOrder = self::ORDER_ASCENDING, $byColumn = false) { if (!is_array($sortArray)) { // Scalars are always returned "as is" return $sortArray; } + $sortArray = self::enumerateArrayKeys($sortArray); + $byColumn = (bool) $byColumn; $lookupIndexSize = $byColumn ? count($sortArray) : count($sortArray[0]); @@ -68,6 +70,12 @@ class Sort extends LookupRefValidations * * @param mixed $sortArray The range of cells being sorted * @param mixed $args + * At least one additional argument must be provided, The vector or range to sort on + * After that, arguments are passed as pairs: + * sort order: ascending or descending + * Ascending = 1 (self::ORDER_ASCENDING) + * Descending = -1 (self::ORDER_DESCENDING) + * additional arrays or ranges for multi-level sorting * * @return mixed The sorted values from the sort range */ @@ -78,6 +86,8 @@ class Sort extends LookupRefValidations return $sortArray; } + $sortArray = self::enumerateArrayKeys($sortArray); + $lookupArraySize = count($sortArray); $argumentCount = count($args); @@ -94,11 +104,25 @@ class Sort extends LookupRefValidations return self::processSortBy($sortArray, $sortBy, $sortOrder); } + private static function enumerateArrayKeys(array $sortArray): array + { + array_walk( + $sortArray, + function (&$columns): void { + if (is_array($columns)) { + $columns = array_values($columns); + } + } + ); + + return array_values($sortArray); + } + /** * @param mixed $sortIndex * @param mixed $sortOrder */ - private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $lookupIndexSize): void + private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void { if (is_array($sortIndex) || is_array($sortOrder)) { throw new Exception(ExcelError::VALUE()); @@ -106,7 +130,7 @@ class Sort extends LookupRefValidations $sortIndex = self::validatePositiveInt($sortIndex, false); - if ($sortIndex > $lookupIndexSize) { + if ($sortIndex > $sortArraySize) { throw new Exception(ExcelError::VALUE()); } @@ -116,7 +140,7 @@ class Sort extends LookupRefValidations /** * @param mixed $sortVector */ - private static function validateSortVector($sortVector, int $lookupArraySize): array + private static function validateSortVector($sortVector, int $sortArraySize): array { if (!is_array($sortVector)) { throw new Exception(ExcelError::VALUE()); @@ -124,7 +148,7 @@ class Sort extends LookupRefValidations // It doesn't matter if it's a row or a column vectors, it works either way $sortVector = Functions::flattenArray($sortVector); - if (count($sortVector) !== $lookupArraySize) { + if (count($sortVector) !== $sortArraySize) { throw new Exception(ExcelError::VALUE()); } @@ -148,14 +172,14 @@ class Sort extends LookupRefValidations * @param array $sortIndex * @param mixed $sortOrder */ - private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $lookupIndexSize): void + private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void { // It doesn't matter if they're row or column vectors, it works either way $sortIndex = Functions::flattenArray($sortIndex); $sortOrder = Functions::flattenArray($sortOrder); if ( - count($sortOrder) === 0 || count($sortOrder) > $lookupIndexSize || + count($sortOrder) === 0 || count($sortOrder) > $sortArraySize || (count($sortOrder) > count($sortIndex)) ) { throw new Exception(ExcelError::VALUE()); @@ -170,7 +194,7 @@ class Sort extends LookupRefValidations } foreach ($sortIndex as $key => &$value) { - self::validateScalarArgumentsForSort($value, $sortOrder[$key], $lookupIndexSize); + self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize); } } @@ -195,7 +219,7 @@ class Sort extends LookupRefValidations * @param array[] $sortIndex * @param int[] $sortOrder */ - private static function processSortBy(array $lookupArray, array $sortIndex, $sortOrder): array + private static function processSortBy(array $sortArray, array $sortIndex, $sortOrder): array { $sortArguments = []; $sortData = []; @@ -204,32 +228,32 @@ class Sort extends LookupRefValidations $sortArguments[] = self::prepareSortVectorValues($sortValues); $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; } - $sortArguments = self::applyPHP7Patch($lookupArray, $sortArguments); + $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments); $sortVector = self::executeVectorSortQuery($sortData, $sortArguments); - return self::sortLookupArrayFromVector($lookupArray, $sortVector); + return self::sortLookupArrayFromVector($sortArray, $sortVector); } /** * @param int[] $sortIndex * @param int[] $sortOrder */ - private static function sortByRow(array $lookupArray, array $sortIndex, array $sortOrder): array + private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array { - $sortVector = self::buildVectorForSort($lookupArray, $sortIndex, $sortOrder); + $sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder); - return self::sortLookupArrayFromVector($lookupArray, $sortVector); + return self::sortLookupArrayFromVector($sortArray, $sortVector); } /** * @param int[] $sortIndex * @param int[] $sortOrder */ - private static function sortByColumn(array $lookupArray, array $sortIndex, array $sortOrder): array + private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array { - $lookupArray = Matrix::transpose($lookupArray); - $result = self::sortByRow($lookupArray, $sortIndex, $sortOrder); + $sortArray = Matrix::transpose($sortArray); + $result = self::sortByRow($sortArray, $sortIndex, $sortOrder); return Matrix::transpose($result); } @@ -238,17 +262,17 @@ class Sort extends LookupRefValidations * @param int[] $sortIndex * @param int[] $sortOrder */ - private static function buildVectorForSort(array $lookupArray, array $sortIndex, array $sortOrder): array + private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array { $sortArguments = []; $sortData = []; foreach ($sortIndex as $index => $sortIndexValue) { - $sortValues = array_column($lookupArray, $sortIndexValue - 1); + $sortValues = array_column($sortArray, $sortIndexValue - 1); $sortData[] = $sortValues; $sortArguments[] = self::prepareSortVectorValues($sortValues); $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; } - $sortArguments = self::applyPHP7Patch($lookupArray, $sortArguments); + $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments); $sortData = self::executeVectorSortQuery($sortData, $sortArguments); @@ -279,12 +303,12 @@ class Sort extends LookupRefValidations return $sortedData; } - private static function sortLookupArrayFromVector(array $lookupArray, array $sortVector): array + private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array { // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays $sortedArray = []; foreach ($sortVector as $index) { - $sortedArray[] = $lookupArray[$index]; + $sortedArray[] = $sortArray[$index]; } return $sortedArray; @@ -306,10 +330,10 @@ class Sort extends LookupRefValidations * MS Excel replicates the PHP 8.0.0 behaviour, retaining the original order of matching elements. * To replicate that behaviour with PHP 7, we add an extra sort based on the row index. */ - private static function applyPHP7Patch(array $lookupArray, array $sortArguments): array + private static function applyPHP7Patch(array $sortArray, array $sortArguments): array { if (PHP_VERSION_ID < 80000) { - $sortArguments[] = range(1, count($lookupArray)); + $sortArguments[] = range(1, count($sortArray)); $sortArguments[] = SORT_ASC; } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php index 3acc8b6a..345f732b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/SortByTest.php @@ -12,7 +12,7 @@ class SortByTest extends TestCase { $value = 'NON-ARRAY'; - $result = Sort::sort($value, [1]); + $result = Sort::sortBy($value); self::assertSame($value, $result); } @@ -45,16 +45,16 @@ class SortByTest extends TestCase /** * @dataProvider providerSortByRow */ - public function testSortByRow(array $expectedResult, array $matrix, array $sortIndex, int $sortOrder = Sort::ORDER_ASCENDING): void + public function testSortByRow(array $expectedResult, array $matrix, ...$args): void { - $result = Sort::sortBy($matrix, $sortIndex, $sortOrder); + $result = Sort::sortBy($matrix, ...$args); self::assertSame($expectedResult, $result); } public function providerSortByRow(): array { return [ - [ + 'Simple sort by age' => [ [ ['Fritz', 19], ['Xi', 19], @@ -65,10 +65,10 @@ class SortByTest extends TestCase ['Hector', 66], ['Sal', 73], ], - $this->sampleDataForRow(), - array_column($this->sampleDataForRow(), 1), + $this->sampleDataForSimpleSort(), + array_column($this->sampleDataForSimpleSort(), 1), ], - [ + 'Simple sort by name' => [ [ ['Amy', 22], ['Fred', 65], @@ -79,10 +79,10 @@ class SortByTest extends TestCase ['Tom', 52], ['Xi', 19], ], - $this->sampleDataForRow(), - array_column($this->sampleDataForRow(), 0), + $this->sampleDataForSimpleSort(), + array_column($this->sampleDataForSimpleSort(), 0), ], - [ + 'Row vector' => [ [ ['Amy', 22], ['Fred', 65], @@ -93,10 +93,10 @@ class SortByTest extends TestCase ['Tom', 52], ['Xi', 19], ], - $this->sampleDataForRow(), + $this->sampleDataForSimpleSort(), ['Tom', 'Fred', 'Amy', 'Sal', 'Fritz', 'Srivan', 'Xi', 'Hector'], ], - [ + 'Column vector' => [ [ ['Amy', 22], ['Fred', 65], @@ -107,13 +107,46 @@ class SortByTest extends TestCase ['Tom', 52], ['Xi', 19], ], - $this->sampleDataForRow(), + $this->sampleDataForSimpleSort(), [['Tom'], ['Fred'], ['Amy'], ['Sal'], ['Fritz'], ['Srivan'], ['Xi'], ['Hector']], ], + 'Sort by region asc, name asc' => [ + [ + ['East', 'Fritz', 19], + ['East', 'Tom', 52], + ['North', 'Amy', 22], + ['North', 'Xi', 19], + ['South', 'Hector', 66], + ['South', 'Sal', 73], + ['West', 'Fred', 65], + ['West', 'Srivan', 39], + ], + $this->sampleDataForMultiSort(), + array_column($this->sampleDataForMultiSort(), 0), + Sort::ORDER_ASCENDING, + array_column($this->sampleDataForMultiSort(), 1), + ], + 'Sort by region asc, age desc' => [ + [ + ['East', 'Tom', 52], + ['East', 'Fritz', 19], + ['North', 'Amy', 22], + ['North', 'Xi', 19], + ['South', 'Sal', 73], + ['South', 'Hector', 66], + ['West', 'Fred', 65], + ['West', 'Srivan', 39], + ], + $this->sampleDataForMultiSort(), + array_column($this->sampleDataForMultiSort(), 0), + Sort::ORDER_ASCENDING, + array_column($this->sampleDataForMultiSort(), 2), + Sort::ORDER_DESCENDING, + ], ]; } - private function sampleDataForRow(): array + private function sampleDataForSimpleSort(): array { return [ ['Tom', 52], @@ -126,4 +159,18 @@ class SortByTest extends TestCase ['Hector', 66], ]; } + + private function sampleDataForMultiSort(): array + { + return [ + ['North', 'Amy', 22], + ['West', 'Fred', 65], + ['East', 'Fritz', 19], + ['South', 'Hector', 66], + ['South', 'Sal', 73], + ['West', 'Srivan', 39], + ['East', 'Tom', 52], + ['North', 'Xi', 19], + ]; + } } From 2ab582a707997a590f718d996d8b449bfbbeb20f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Thu, 24 Mar 2022 22:12:08 +0100 Subject: [PATCH 39/39] Bugfix to returned column indexes for FILTER() by row --- src/PhpSpreadsheet/Calculation/LookupRef/Filter.php | 2 +- .../Calculation/Functions/LookupRef/FilterTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php index 6d201531..74fa8321 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -29,7 +29,7 @@ class Filter return $ifEmpty ?? ExcelError::CALC(); } - return array_values($result); + return array_values(array_map('array_values', $result)); } private static function enumerateArrayKeys(array $sortArray): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php index 710a42e0..5a584c46 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FilterTest.php @@ -15,7 +15,7 @@ class FilterTest extends TestCase ['East', 'Tom', 'Apple', 6830], ['East', 'Fritz', 'Apple', 4394], ['South', 'Sal', 'Apple', 1310], - ['South', 'Hector', 'Apple', 98144], + ['South', 'Hector', 'Apple', 8144], ]; $result = Filter::filter($this->sampleDataForRow(), $criteria); self::assertSame($expectedResult, $result); @@ -70,7 +70,7 @@ class FilterTest extends TestCase ['East', 'Fritz', 'Banana', 6274], ['West', 'Sravan', 'Pear', 4894], ['North', 'Xi', 'Grape', 7580], - ['South', 'Hector', 'Apple', 98144], + ['South', 'Hector', 'Apple', 8144], ]; }