From d6b3514431fbccf941009f9bc55d8441b03c925b Mon Sep 17 00:00:00 2001 From: Christian WERNER Date: Tue, 19 Feb 2019 08:54:01 +0100 Subject: [PATCH 01/17] Cover `getSheetByName()` with tests for name with quote and spaces Fixes #739 Closes #893 --- CHANGELOG.md | 1 + tests/PhpSpreadsheetTests/SpreadsheetTest.php | 56 +++++++++++++++++++ .../Worksheet/WorksheetTest.php | 4 ++ 3 files changed, 61 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/SpreadsheetTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 20819876..d3ce9b5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [Issue #1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046) - COUPNUM should not return zero when settlement is in the last period - [Issue #1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [PR #1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021) - Fix handling of named ranges referencing sheets with spaces or "!" in their title +- Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) ## [1.8.2] - 2019-07-08 diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php new file mode 100644 index 00000000..5173bf22 --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -0,0 +1,56 @@ +object = new Spreadsheet(); + $sheet = $this->object->getActiveSheet(); + + $sheet->setTitle('someSheet1'); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet2'); + $this->object->addSheet($sheet); + $sheet = new Worksheet(); + $sheet->setTitle('someSheet 3'); + $this->object->addSheet($sheet); + } + + /** + * @return array + */ + public function dataProviderForSheetNames() + { + $array = [ + [0, 'someSheet1'], + [0, "'someSheet1'"], + [1, 'someSheet2'], + [1, "'someSheet2'"], + [2, 'someSheet 3'], + [2, "'someSheet 3'"], + ]; + + return $array; + } + + /** + * @param $index + * @param $sheetName + * + * @dataProvider dataProviderForSheetNames + */ + public function testGetSheetByName($index, $sheetName) + { + $this->assertEquals($this->object->getSheet($index), $this->object->getSheetByName($sheetName)); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index ce2df837..5e8bbcb2 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -138,6 +138,10 @@ class WorksheetTest extends TestCase ['testTitle!B2', 'testTitle', 'B2', 'B2'], ['test!Title!B2', 'test!Title', 'B2', 'B2'], ['test Title!B2', 'test Title', 'B2', 'B2'], + ['test!Title!B2', 'test!Title', 'B2', 'B2'], + ["'testSheet 1'!A3", "'testSheet 1'", 'A3', 'A3'], + ["'testSheet1'!A2", "'testSheet1'", 'A2', 'A2'], + ["'testSheet 2'!A1", "'testSheet 2'", 'A1', 'A1'], ]; } From 785705b71258c72f9678f3c2da5fd4f7a1d04c97 Mon Sep 17 00:00:00 2001 From: Mahmoud Abdo Date: Mon, 11 Feb 2019 15:06:39 +0300 Subject: [PATCH 02/17] Best effort to support invalid colspan values in HTML reader Closes #878 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Html.php | 8 ++++---- tests/PhpSpreadsheetTests/Reader/HtmlTest.php | 12 +++++++++++- tests/data/Reader/HTML/rowspan.html | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 tests/data/Reader/HTML/rowspan.html diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ce9b5b..1f08b073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - COUPNUM should not return zero when settlement is in the last period - [Issue #1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [PR #1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021) - Fix handling of named ranges referencing sheets with spaces or "!" in their title - Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) +- Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) ## [1.8.2] - 2019-07-08 diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index d599424d..ff2c909e 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -502,10 +502,10 @@ class Html extends BaseReader if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { //create merging rowspan and colspan $columnTo = $column; - for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) { + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { ++$columnTo; } - $range = $column . $row . ':' . $columnTo . ($row + $attributeArray['rowspan'] - 1); + $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1); foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { $this->rowspan[$value] = true; } @@ -513,7 +513,7 @@ class Html extends BaseReader $column = $columnTo; } elseif (isset($attributeArray['rowspan'])) { //create merging rowspan - $range = $column . $row . ':' . $column . ($row + $attributeArray['rowspan'] - 1); + $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1); foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { $this->rowspan[$value] = true; } @@ -521,7 +521,7 @@ class Html extends BaseReader } elseif (isset($attributeArray['colspan'])) { //create merging colspan $columnTo = $column; - for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) { + for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) { ++$columnTo; } $sheet->mergeCells($column . $row . ':' . $columnTo . $row); diff --git a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/HtmlTest.php index 415f562a..e8b00f1a 100644 --- a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/HtmlTest.php @@ -33,7 +33,7 @@ class HtmlTest extends TestCase /** * @dataProvider providerCanReadVerySmallFile * - * @param bool $expected + * @param bool $expected * @param string $content */ public function testCanReadVerySmallFile($expected, $content) @@ -321,4 +321,14 @@ class HtmlTest extends TestCase { return (new Html())->load($filename); } + + public function testRowspanInRendering() + { + $filename = './data/Reader/HTML/rowspan.html'; + $reader = new Html(); + $spreadsheet = $reader->load($filename); + + $actual = $spreadsheet->getActiveSheet()->getMergeCells(); + self::assertSame(['A2:C2' => 'A2:C2'], $actual); + } } diff --git a/tests/data/Reader/HTML/rowspan.html b/tests/data/Reader/HTML/rowspan.html new file mode 100644 index 00000000..c748c8e3 --- /dev/null +++ b/tests/data/Reader/HTML/rowspan.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + +
A1B1C1D1
A2 with invalid colspanD2 +
From 0b387e767e99a9042886ea673d9b9905c7951e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4ntz=20Miccoli?= Date: Tue, 27 Nov 2018 16:55:06 +0100 Subject: [PATCH 03/17] Branch pruning around IF function calls to avoid resolution of every branches Calculation engine was resolving every function by first resolving its arguments including IFs, this was causing significant over evaluation when IFs were used as it meant for every case to be evaluated. Introduce elements to identify ifs and enable better branch resolution (pruning). We tag parsed tokens to associate a branch identifier to them. Closes #844 --- CHANGELOG.md | 1 + .../Calculation/Calculation.php | 331 ++++++++++++++++-- .../Calculation/Token/Stack.php | 70 +++- .../Calculation/CalculationTest.php | 175 +++++++++ tests/data/Calculation/Calculation.php | 63 ++++ 5 files changed, 609 insertions(+), 31 deletions(-) create mode 100644 tests/data/Calculation/Calculation.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f08b073..60d0eede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - When <br> appears in a table cell, set the cell to wrap [Issue #1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [PR #1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070) - Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF - [Issue #1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056) - HLookup needs an ordered list even if range_lookup is set to false [Issue #1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [PR #1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076) +- Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844) ### Fixed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 63cd69cc..449553a4 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -66,6 +66,15 @@ class Calculation */ private $calculationCacheEnabled = true; + /** + * Used to generate unique store keys. + * + * @var int + */ + private $branchStoreKeyCounter = 0; + + private $branchPruningEnabled = true; + /** * List of operators that can be used within formulae * The true/false value indicates whether it is a binary operator or a unary operator. @@ -2256,6 +2265,7 @@ class Calculation public function flushInstance() { $this->clearCalculationCache(); + $this->clearBranchStore(); } /** @@ -2399,6 +2409,32 @@ class Calculation } } + /** + * Enable/disable calculation cache. + * + * @param bool $pValue + * @param mixed $enabled + */ + public function setBranchPruningEnabled($enabled) + { + $this->branchPruningEnabled = $enabled; + } + + public function enableBranchPruning() + { + $this->setBranchPruningEnabled(true); + } + + public function disableBranchPruning() + { + $this->setBranchPruningEnabled(false); + } + + public function clearBranchStore() + { + $this->branchStoreKeyCounter = 0; + } + /** * Get the currently defined locale code. * @@ -2867,6 +2903,7 @@ class Calculation if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) { $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache'); // Return the cached result + $cellValue = $this->calculationCache[$cellReference]; return true; @@ -3326,9 +3363,53 @@ class Calculation // - is a negation or + is a positive operator rather than an operation $expectingOperand = false; // We use this test in syntax-checking the expression to determine whether an operand // should be null in a function call + + // IF branch pruning + // currently pending storeKey (last item of the storeKeysStack + $pendingStoreKey = null; + // stores a list of storeKeys (string[]) + $pendingStoreKeysStack = []; + $expectingConditionMap = []; // ['storeKey' => true, ...] + $expectingThenMap = []; // ['storeKey' => true, ...] + $expectingElseMap = []; // ['storeKey' => true, ...] + $parenthesisDepthMap = []; // ['storeKey' => 4, ...] + // The guts of the lexical parser // Loop through the formula extracting each operator and operand in turn while (true) { + // Branch pruning: we adapt the output item to the context (it will + // be used to limit its computation) + $currentCondition = null; + $currentOnlyIf = null; + $currentOnlyIfNot = null; + $previousStoreKey = null; + $pendingStoreKey = end($pendingStoreKeysStack); + + if ($this->branchPruningEnabled) { + // this is a condition ? + if (isset($expectingConditionMap[$pendingStoreKey]) && $expectingConditionMap[$pendingStoreKey]) { + $currentCondition = $pendingStoreKey; + $stackDepth = count($pendingStoreKeysStack); + if ($stackDepth > 1) { // nested if + $previousStoreKey = $pendingStoreKeysStack[$stackDepth - 2]; + } + } + if (isset($expectingThenMap[$pendingStoreKey]) && $expectingThenMap[$pendingStoreKey]) { + $currentOnlyIf = $pendingStoreKey; + } elseif (isset($previousStoreKey)) { + if (isset($expectingThenMap[$previousStoreKey]) && $expectingThenMap[$previousStoreKey]) { + $currentOnlyIf = $previousStoreKey; + } + } + if (isset($expectingElseMap[$pendingStoreKey]) && $expectingElseMap[$pendingStoreKey]) { + $currentOnlyIfNot = $pendingStoreKey; + } elseif (isset($previousStoreKey)) { + if (isset($expectingElseMap[$previousStoreKey]) && $expectingElseMap[$previousStoreKey]) { + $currentOnlyIfNot = $previousStoreKey; + } + } + } + $opCharacter = $formula[$index]; // Get the first character of the value at the current index position if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) { $opCharacter .= $formula[++$index]; @@ -3338,10 +3419,12 @@ class Calculation $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match); if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus? - $stack->push('Unary Operator', '~'); // Put a negation on the stack + // Put a negation on the stack + $stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); ++$index; // and drop the negation symbol } elseif ($opCharacter == '%' && $expectingOperator) { - $stack->push('Unary Operator', '%'); // Put a percentage on the stack + // Put a percentage on the stack + $stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); ++$index; } elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded? ++$index; // Drop the redundant plus symbol @@ -3354,7 +3437,10 @@ class Calculation @(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) { $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output } - $stack->push('Binary Operator', $opCharacter); // Finally put our current operator onto the stack + + // Finally put our current operator onto the stack + $stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + ++$index; $expectingOperator = false; } elseif ($opCharacter == ')' && $expectingOperator) { // Are we expecting to close a parenthesis? @@ -3366,7 +3452,29 @@ class Calculation $output[] = $o2; } $d = $stack->last(2); + + // Branch pruning we decrease the depth whether is it a function + // call or a parenthesis + if (!empty($pendingStoreKey)) { + $parenthesisDepthMap[$pendingStoreKey] -= 1; + } + if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) { // Did this parenthesis just close a function? + if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) { + // we are closing an IF( + if ($d['value'] != 'IF(') { + return $this->raiseFormulaError('Parser bug we should be in an "IF("'); + } + if ($expectingConditionMap[$pendingStoreKey]) { + return $this->raiseFormulaError('We should not be expecting a condition'); + } + $expectingThenMap[$pendingStoreKey] = false; + $expectingElseMap[$pendingStoreKey] = false; + $parenthesisDepthMap[$pendingStoreKey] -= 1; + array_pop($pendingStoreKeysStack); + unset($pendingStoreKey); + } + $functionName = $matches[1]; // Get the function name $d = $stack->pop(); $argumentCount = $d['value']; // See how many arguments there were (argument count is the next value stored on the stack) @@ -3427,6 +3535,20 @@ class Calculation } ++$index; } elseif ($opCharacter == ',') { // Is this the separator for function arguments? + if (!empty($pendingStoreKey) && + $parenthesisDepthMap[$pendingStoreKey] == 0 + ) { + // We must go to the IF next argument + if ($expectingConditionMap[$pendingStoreKey]) { + $expectingConditionMap[$pendingStoreKey] = false; + $expectingThenMap[$pendingStoreKey] = true; + } elseif ($expectingThenMap[$pendingStoreKey]) { + $expectingThenMap[$pendingStoreKey] = false; + $expectingElseMap[$pendingStoreKey] = true; + } elseif ($expectingElseMap[$pendingStoreKey]) { + return $this->raiseFormulaError('Reaching fourth argument of an IF'); + } + } while (($o2 = $stack->pop()) && $o2['value'] != '(') { // Pop off the stack back to the last ( if ($o2 === null) { return $this->raiseFormulaError('Formula Error: Unexpected ,'); @@ -3444,13 +3566,19 @@ class Calculation return $this->raiseFormulaError('Formula Error: Unexpected ,'); } $d = $stack->pop(); - $stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count - $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again + $itemStoreKey = $d['storeKey'] ?? null; + $itemOnlyIf = $d['onlyIf'] ?? null; + $itemOnlyIfNot = $d['onlyIfNot'] ?? null; + $stack->push($d['type'], ++$d['value'], $d['reference'], $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // increment the argument count + $stack->push('Brace', '(', null, $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // put the ( back on, we'll need to pop back to it again $expectingOperator = false; $expectingOperand = true; ++$index; } elseif ($opCharacter == '(' && !$expectingOperator) { - $stack->push('Brace', '('); + if (!empty($pendingStoreKey)) { // Branch pruning: we go deeper + $parenthesisDepthMap[$pendingStoreKey] += 1; + } + $stack->push('Brace', '(', null, $currentCondition, $currentOnlyIf, $currentOnlyIf); ++$index; } elseif ($isOperandOrFunction && !$expectingOperator) { // do we now have a function/variable/number? $expectingOperator = true; @@ -3461,13 +3589,29 @@ class Calculation if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) { $val = preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function - $stack->push('Function', strtoupper($val)); + $valToUpper = strtoupper($val); + // here $matches[1] will contain values like "IF" + // and $val "IF(" + $injectStoreKey = null; + if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if + $pendingStoreKey = $this->getUnusedBranchStoreKey(); + $pendingStoreKeysStack[] = $pendingStoreKey; + $expectingConditionMap[$pendingStoreKey] = true; + $parenthesisDepthMap[$pendingStoreKey] = 0; + } else { // this is not a if but we good deeper + if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) { + $parenthesisDepthMap[$pendingStoreKey] += 1; + } + } + + $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + // tests if the function is closed right after opening $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch); if ($ax) { - $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0); + $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); $expectingOperator = true; } else { - $stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1); + $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); $expectingOperator = false; } $stack->push('Brace', '('); @@ -3495,7 +3639,9 @@ class Calculation } } - $output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val]; + $outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + + $output[] = $outputItem; } else { // it's a variable, constant, string, number or boolean // If the last entry on the stack was a : operator, then we may have a row or column range reference $testPrevOp = $stack->last(1); @@ -3542,7 +3688,7 @@ class Calculation } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { $val = self::$excelConstants[$localeConstant]; } - $details = ['type' => 'Value', 'value' => $val, 'reference' => null]; + $details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); if ($localeConstant) { $details['localeValue'] = $localeConstant; } @@ -3645,9 +3791,74 @@ class Calculation $pCellParent = ($pCell !== null) ? $pCell->getParent() : null; $stack = new Stack(); + // Stores branches that have been pruned + $fakedForBranchPruning = []; + // help us to know when pruning ['branchTestId' => true/false] + $branchStore = []; + // Loop through each token in turn foreach ($tokens as $tokenData) { $token = $tokenData['value']; + + // Branch pruning: skip useless resolutions + $storeKey = $tokenData['storeKey'] ?? null; + if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) { + $onlyIfStoreKey = $tokenData['onlyIf']; + $storeValue = $branchStore[$onlyIfStoreKey] ?? null; + if (is_array($storeValue)) { + $wrappedItem = end($storeValue); + $storeValue = end($wrappedItem); + } + + if (isset($storeValue) && (($storeValue !== true) + || ($storeValue === 'Pruned branch')) + ) { + // If branching value is not true, we don't need to compute + if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) { + $stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token); + $fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true; + } + + if (isset($storeKey)) { + // We are processing an if condition + // We cascade the pruning to the depending branches + $branchStore[$storeKey] = 'Pruned branch'; + $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true; + $fakedForBranchPruning['onlyIf-' . $storeKey] = true; + } + + continue; + } + } + + if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) { + $onlyIfNotStoreKey = $tokenData['onlyIfNot']; + $storeValue = $branchStore[$onlyIfNotStoreKey] ?? null; + if (is_array($storeValue)) { + $wrappedItem = end($storeValue); + $storeValue = end($wrappedItem); + } + if (isset($storeValue) && ($storeValue + || ($storeValue === 'Pruned branch')) + ) { + // If branching value is true, we don't need to compute + if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) { + $stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token); + $fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true; + } + + if (isset($storeKey)) { + // We are processing an if condition + // We cascade the pruning to the depending branches + $branchStore[$storeKey] = 'Pruned branch'; + $fakedForBranchPruning['onlyIfNot-' . $storeKey] = true; + $fakedForBranchPruning['onlyIf-' . $storeKey] = true; + } + + continue; + } + } + // if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack if (isset(self::$binaryOperators[$token])) { // We must have two operands, error if we don't @@ -3677,7 +3888,10 @@ class Calculation case '<=': // Less than or Equal to case '=': // Equality case '<>': // Inequality - $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack); + $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } break; // Binary Operators @@ -3733,23 +3947,38 @@ class Calculation break; case '+': // Addition - $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack); + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } break; case '-': // Subtraction - $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack); + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } break; case '*': // Multiplication - $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack); + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } break; case '/': // Division - $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack); + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } break; case '^': // Exponential - $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack); + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } break; case '&': // Concatenation @@ -3782,6 +4011,10 @@ class Calculation $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); $stack->push('Value', $result); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } + break; case '|': // Intersect $rowIntersect = array_intersect_key($operand1, $operand2); @@ -3826,6 +4059,9 @@ class Calculation } $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); $stack->push('Value', $result); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } } else { $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack); } @@ -3899,9 +4135,20 @@ class Calculation } } $stack->push('Value', $cellValue, $cellRef); + if (isset($storeKey)) { + $branchStore[$storeKey] = $cellValue; + } - // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) { + if (($cellID == 'AC99') || (isset($pCell) && $pCell->getCoordinate() == 'AC99')) { + if (defined('RESOLVING')) { + define('RESOLVING2', true); + } else { + define('RESOLVING', true); + } + } + $functionName = $matches[1]; $argCount = $stack->pop(); $argCount = $argCount['value']; @@ -3944,6 +4191,7 @@ class Calculation } } } + // Reverse the order of the arguments krsort($args); @@ -3974,16 +4222,25 @@ class Calculation $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result)); } $stack->push('Value', self::wrapResult($result)); + if (isset($storeKey)) { + $branchStore[$storeKey] = $result; + } } } else { // if the token is a number, boolean, string or an Excel error, push it onto the stack if (isset(self::$excelConstants[strtoupper($token)])) { $excelConstant = strtoupper($token); $stack->push('Constant Value', self::$excelConstants[$excelConstant]); + if (isset($storeKey)) { + $branchStore[$storeKey] = self::$excelConstants[$excelConstant]; + } $this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant])); } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) { $stack->push('Value', $token); - // if the token is a named range, push the named range name onto the stack + if (isset($storeKey)) { + $branchStore[$storeKey] = $token; + } + // if the token is a named range, push the named range name onto the stack } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) { $namedRange = $matches[6]; $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange); @@ -3992,6 +4249,9 @@ class Calculation $pCell->attach($pCellParent); $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue)); $stack->push('Named Range', $cellValue, $namedRange); + if (isset($storeKey)) { + $branchStore[$storeKey] = $cellValue; + } } else { return $this->raiseFormulaError("undefined variable '$token'"); } @@ -4053,7 +4313,7 @@ class Calculation * @param Stack $stack * @param bool $recursingArrays * - * @return bool + * @return mixed */ private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false) { @@ -4090,7 +4350,7 @@ class Calculation // And push the result onto the stack $stack->push('Array', $result); - return true; + return $result; } // Simple validate the two operands if they are string values @@ -4180,7 +4440,7 @@ class Calculation // And push the result onto the stack $stack->push('Value', $result); - return true; + return $result; } /** @@ -4206,7 +4466,7 @@ class Calculation * @param string $matrixFunction * @param mixed $stack * - * @return bool + * @return bool|mixed */ private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack) { @@ -4284,7 +4544,7 @@ class Calculation // And push the result onto the stack $stack->push('Value', $result); - return true; + return $result; } // trigger an error, but nicely, if need be @@ -4488,4 +4748,27 @@ class Calculation return $args; } + + private function getUnusedBranchStoreKey() + { + $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter; + ++$this->branchStoreKeyCounter; + + return $storeKeyValue; + } + + private function getTokensAsString($tokens) + { + $tokensStr = array_map(function ($token) { + $value = $token['value'] ?? 'no value'; + while (is_array($value)) { + $value = array_pop($value); + } + + return $value; + }, $tokens); + $str = '[ ' . implode(' | ', $tokensStr) . ' ]'; + + return $str; + } } diff --git a/src/PhpSpreadsheet/Calculation/Token/Stack.php b/src/PhpSpreadsheet/Calculation/Token/Stack.php index b8dccf95..341a0179 100644 --- a/src/PhpSpreadsheet/Calculation/Token/Stack.php +++ b/src/PhpSpreadsheet/Calculation/Token/Stack.php @@ -36,14 +36,24 @@ class Stack * @param mixed $type * @param mixed $value * @param mixed $reference + * @param null|string $storeKey will store the result under this alias + * @param null|string $onlyIf will only run computation if the matching + * store key is true + * @param null|string $onlyIfNot will only run computation if the matching + * store key is false */ - public function push($type, $value, $reference = null) - { - $this->stack[$this->count++] = [ - 'type' => $type, - 'value' => $value, - 'reference' => $reference, - ]; + public function push( + $type, + $value, + $reference = null, + $storeKey = null, + $onlyIf = null, + $onlyIfNot = null + ) { + $stackItem = $this->getStackItem($type, $value, $reference, $storeKey, $onlyIf, $onlyIfNot); + + $this->stack[$this->count++] = $stackItem; + if ($type == 'Function') { $localeFunction = Calculation::localeFunc($value); if ($localeFunction != $value) { @@ -52,6 +62,35 @@ class Stack } } + public function getStackItem( + $type, + $value, + $reference = null, + $storeKey = null, + $onlyIf = null, + $onlyIfNot = null + ) { + $stackItem = [ + 'type' => $type, + 'value' => $value, + 'reference' => $reference, + ]; + + if (isset($storeKey)) { + $stackItem['storeKey'] = $storeKey; + } + + if (isset($onlyIf)) { + $stackItem['onlyIf'] = $onlyIf; + } + + if (isset($onlyIfNot)) { + $stackItem['onlyIfNot'] = $onlyIfNot; + } + + return $stackItem; + } + /** * Pop the last entry from the stack. * @@ -90,4 +129,21 @@ class Stack $this->stack = []; $this->count = 0; } + + public function __toString() + { + $str = 'Stack: '; + foreach ($this->stack as $index => $item) { + if ($index > $this->count - 1) { + break; + } + $value = $item['value'] ?? 'no value'; + while (is_array($value)) { + $value = array_pop($value); + } + $str .= $value . ' |> '; + } + + return $str; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 4b81fbf4..6bceee0f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -163,4 +163,179 @@ class CalculationTest extends TestCase self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue()); } + + public function testBranchPruningFormulaParsingSimpleCase() + { + $calculation = Calculation::getInstance(); + $calculation->flushInstance(); // resets the ids + + // Very simple formula + $formula = '=IF(A1="please +",B1)'; + $tokens = $calculation->parseFormula($formula); + + $foundEqualAssociatedToStoreKey = false; + $foundConditionalOnB1 = false; + foreach ($tokens as $token) { + $isBinaryOperator = $token['type'] == 'Binary Operator'; + $isEqual = $token['value'] == '='; + $correctStoreKey = ($token['storeKey'] ?? '') == 'storeKey-0'; + $correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-0'; + $isB1Reference = ($token['reference'] ?? '') == 'B1'; + + $foundEqualAssociatedToStoreKey = $foundEqualAssociatedToStoreKey || + ($isBinaryOperator && $isEqual && $correctStoreKey); + + $foundConditionalOnB1 = $foundConditionalOnB1 || + ($isB1Reference && $correctOnlyIf); + } + $this->assertTrue($foundEqualAssociatedToStoreKey); + $this->assertTrue($foundConditionalOnB1); + } + + public function testBranchPruningFormulaParsingMultipleIfsCase() + { + $calculation = Calculation::getInstance(); + $calculation->flushInstance(); // resets the ids + + // + // Internal operation + $formula = '=IF(A1="please +",SUM(B1:B3))+IF(A2="please *",PRODUCT(C1:C3), C1)'; + $tokens = $calculation->parseFormula($formula); + + $plusGotTagged = false; + $productFunctionCorrectlyTagged = false; + foreach ($tokens as $token) { + $isBinaryOperator = $token['type'] == 'Binary Operator'; + $isPlus = $token['value'] == '+'; + $anyStoreKey = isset($token['storeKey']); + $anyOnlyIf = isset($token['onlyIf']); + $anyOnlyIfNot = isset($token['onlyIfNot']); + $plusGotTagged = $plusGotTagged || + ($isBinaryOperator && $isPlus && + ($anyStoreKey || $anyOnlyIfNot || $anyOnlyIf)); + + $isFunction = $token['type'] == 'Function'; + $isProductFunction = $token['value'] == 'PRODUCT('; + $correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-1'; + $productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isFunction && $isProductFunction && $correctOnlyIf); + } + $this->assertFalse($plusGotTagged, 'chaining IF( should not affect the external operators'); + $this->assertTrue($productFunctionCorrectlyTagged, 'function nested inside if should be tagged to be processed only if parent branching requires it'); + } + + public function testBranchPruningFormulaParingNestedIfCase() + { + $calculation = Calculation::getInstance(); + $calculation->flushInstance(); // resets the ids + + $formula = '=IF(A1="please +",SUM(B1:B3),1+IF(NOT(A2="please *"),C2-C1,PRODUCT(C1:C3)))'; + $tokens = $calculation->parseFormula($formula); + + $plusCorrectlyTagged = false; + $productFunctionCorrectlyTagged = false; + $notFunctionCorrectlyTagged = false; + $findOneOperandCountTagged = false; + foreach ($tokens as $token) { + $value = $token['value']; + $isPlus = $value == '+'; + $isProductFunction = $value == 'PRODUCT('; + $isNotFunction = $value == 'NOT('; + $isIfOperand = $token['type'] == 'Operand Count for Function IF()'; + $isOnlyIfNotDepth1 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-1'; + $isStoreKeyDepth1 = (array_key_exists('storeKey', $token) ? $token['storeKey'] : null) == 'storeKey-1'; + $isOnlyIfNotDepth0 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-0'; + + $plusCorrectlyTagged = $plusCorrectlyTagged || ($isPlus && $isOnlyIfNotDepth0); + $notFunctionCorrectlyTagged = $notFunctionCorrectlyTagged || ($isNotFunction && $isOnlyIfNotDepth0 && $isStoreKeyDepth1); + $productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isProductFunction && $isOnlyIfNotDepth1 && !$isStoreKeyDepth1 && !$isOnlyIfNotDepth0); + $findOneOperandCountTagged = $findOneOperandCountTagged || ($isIfOperand && $isOnlyIfNotDepth0); + } + $this->assertTrue($plusCorrectlyTagged); + $this->assertTrue($productFunctionCorrectlyTagged); + $this->assertTrue($notFunctionCorrectlyTagged); + } + + public function testBranchPruningFormulaParsingNoArgumentFunctionCase() + { + $calculation = Calculation::getInstance(); + $calculation->flushInstance(); // resets the ids + + $formula = '=IF(AND(TRUE(),A1="please +"),2,3)'; + // this used to raise a parser error, we keep it even though we don't + // test the output + $calculation->parseFormula($formula); + } + + public function testBranchPruningFormulaParsingInequalitiesConditionsCase() + { + $calculation = Calculation::getInstance(); + $calculation->flushInstance(); // resets the ids + + $formula = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))'; + $tokens = $calculation->parseFormula($formula); + $properlyTaggedPlus = false; + foreach ($tokens as $token) { + $isPlus = $token['value'] === '+'; + $hasOnlyIf = !empty($token['onlyIf']); + + $properlyTaggedPlus = $properlyTaggedPlus || + ($isPlus && $hasOnlyIf); + } + $this->assertTrue($properlyTaggedPlus); + } + + /** + * @param $expectedResult + * @param $dataArray + * @param string $formula + * @param string $cellCoordinates where to put the formula + * @param string[] $shouldBeSetInCacheCells coordinates of cells that must + * be set in cache + * @param string[] $shouldNotBeSetInCacheCells coordinates of cells that must + * not be set in cache because of pruning + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @dataProvider dataProviderBranchPruningFullExecution + */ + public function testFullExecution( + $expectedResult, + $dataArray, + $formula, + $cellCoordinates, + $shouldBeSetInCacheCells = [], + $shouldNotBeSetInCacheCells = [] + ) { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->fromArray($dataArray); + $cell = $sheet->getCell($cellCoordinates); + $calculation = Calculation::getInstance($cell->getWorksheet()->getParent()); + + $cell->setValue($formula); + $calculated = $cell->getCalculatedValue(); + $this->assertEquals($expectedResult, $calculated); + + // this mostly to ensure that at least some cells are cached + foreach ($shouldBeSetInCacheCells as $setCell) { + unset($inCache); + $calculation->getValueFromCache('Worksheet!' . $setCell, $inCache); + $this->assertNotEmpty($inCache); + } + + foreach ($shouldNotBeSetInCacheCells as $notSetCell) { + unset($inCache); + $calculation->getValueFromCache('Worksheet!' . $notSetCell, $inCache); + $this->assertEmpty($inCache); + } + + $calculation->disableBranchPruning(); + $calculated = $cell->getCalculatedValue(); + $this->assertEquals($expectedResult, $calculated); + } + + public function dataProviderBranchPruningFullExecution() + { + return require 'data/Calculation/Calculation.php'; + } } diff --git a/tests/data/Calculation/Calculation.php b/tests/data/Calculation/Calculation.php new file mode 100644 index 00000000..09538c3c --- /dev/null +++ b/tests/data/Calculation/Calculation.php @@ -0,0 +1,63 @@ +3,C1,0)', 'E5']; + + $dataArray4 = [ + ['noflag', 0, 0], + [127000, 0, 0], + [ 10000, 0.03, 0], + [ 20000, 0.06, 0], + [ 40000, 0.09, 0], + [ 70000, 0.12, 0], + [ 90000, 0.03, 0] + ]; + $formula2 = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))'; + $set7 = [false, $dataArray4, $formula2, 'E5']; + + $dataArray5 = [ + [1, 2], + [3, 4], + ['=A1+A2', '=SUM(B1:B2)'], + [ 'take A', 0] + ]; + $formula3 = '=IF(A4="take A", A3, B3)'; + $set8 = [4, $dataArray5, $formula3, 'E5', ['A3'], ['B3']]; + + return [$set0, $set1, $set2, $set3, $set4, $set5, $set6, $set7, $set8]; +}; + +return calculationTestDataGenerator(); From 5fe0a796c7a6e4db5f76567a2639eba9f4fe3eb2 Mon Sep 17 00:00:00 2001 From: Alex Pravdin Date: Thu, 31 Jan 2019 19:38:31 +0900 Subject: [PATCH 04/17] Fix incorrect cache clearance on row deletion Fixes #868 Closes #871 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Worksheet/Worksheet.php | 4 ++++ .../Worksheet/WorksheetTest.php | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d0eede..b28a0c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix handling of named ranges referencing sheets with spaces or "!" in their title - Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) - Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) +- Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868) ## [1.8.2] - 2019-07-08 diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 689c547e..a485f405 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2115,6 +2115,10 @@ class Worksheet implements IComparable public function removeRow($pRow, $pNumRows = 1) { if ($pRow >= 1) { + for ($r = 0; $r < $pNumRows; ++$r) { + $this->getCellCollection()->removeRow($pRow + $r); + } + $highestRow = $this->getHighestDataRow(); $objReferenceHelper = ReferenceHelper::getInstance(); $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this); diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 5e8bbcb2..eb2aa200 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -161,4 +161,24 @@ class WorksheetTest extends TestCase self::assertSame($expectTitle, $arRange[0]); self::assertSame($expectCell2, $arRange[1]); } + + /** + * Fix https://github.com/PHPOffice/PhpSpreadsheet/issues/868 when cells are not removed correctly + * on row deletion. + */ + public function testRemoveCellsCorrectlyWhenRemovingRow() + { + $workbook = new Spreadsheet(); + $worksheet = $workbook->getActiveSheet(); + $worksheet->getCell('A2')->setValue('A2'); + $worksheet->getCell('C1')->setValue('C1'); + $worksheet->removeRow(1); + $this->assertEquals( + 'A2', + $worksheet->getCell('A1')->getValue() + ); + $this->assertNull( + $worksheet->getCell('C1')->getValue() + ); + } } From 1e1118d9e0fa8e8004b13b260b53d0eaaf60b4ba Mon Sep 17 00:00:00 2001 From: rumbleh Date: Mon, 18 Mar 2019 12:32:07 -0300 Subject: [PATCH 05/17] Update NumberFormat.php Fix FORMAT_DATE_DDMMYYYY from 'dd/mm/yy' to 'dd/mm/yyyy' --- src/PhpSpreadsheet/Style/NumberFormat.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index ac64b20e..5f23edcc 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -24,7 +24,7 @@ class NumberFormat extends Supervisor const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'; const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd'; - const FORMAT_DATE_DDMMYYYY = 'dd/mm/yy'; + const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy'; const FORMAT_DATE_DMYSLASH = 'd/m/yy'; const FORMAT_DATE_DMYMINUS = 'd-m-yy'; const FORMAT_DATE_DMMINUS = 'd-m'; From 7cca8d2bae8d4e5e0b918c44817e254e0c564cf3 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 11 Aug 2019 19:14:25 -0700 Subject: [PATCH 06/17] Consistent format name and value --- src/PhpSpreadsheet/Style/NumberFormat.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index 5f23edcc..6577aeff 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -23,7 +23,7 @@ class NumberFormat extends Supervisor const FORMAT_PERCENTAGE_00 = '0.00%'; const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'; - const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd'; + const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd'; const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy'; const FORMAT_DATE_DMYSLASH = 'd/m/yy'; const FORMAT_DATE_DMYMINUS = 'd-m-yy'; @@ -43,7 +43,7 @@ class NumberFormat extends Supervisor const FORMAT_DATE_TIME6 = 'h:mm:ss'; const FORMAT_DATE_TIME7 = 'i:s.S'; const FORMAT_DATE_TIME8 = 'h:mm:ss;@'; - const FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd;@'; + const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@'; const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-'; const FORMAT_CURRENCY_USD = '$#,##0_-'; From 2166458de39137dfb81d4350a6c1c60da6ad1a05 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sun, 11 Aug 2019 19:40:48 -0700 Subject: [PATCH 07/17] Duplicated call of strtoupper --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index a485f405..7da5f693 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1441,7 +1441,7 @@ class Worksheet implements IComparable $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); // set cell coordinate as active - $this->setSelectedCells(strtoupper($pCellCoordinate)); + $this->setSelectedCells($pCellCoordinate); return $this->parent->getCellXfSupervisor(); } From 9df68f12e2ebe2987ceee8bdfe1a04b8c3fe5691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolands=20Us=C4=81ns?= Date: Mon, 12 Aug 2019 06:11:36 +0300 Subject: [PATCH 08/17] MATCH function fix - fix boolean search - add support for excel expressions `*?~` Fixes #1116 Closes #1122 --- CHANGELOG.md | 2 + src/PhpSpreadsheet/Calculation/LookupRef.php | 88 ++++++++---- tests/data/Calculation/LookupRef/MATCH.php | 142 ++++++++++++++++++- 3 files changed, 207 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b28a0c99..20d26f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF - [Issue #1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056) - HLookup needs an ordered list even if range_lookup is set to false [Issue #1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [PR #1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076) - Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844) +- MATCH function supports `*?~` Excel functionality, when match_type=0 - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) ### Fixed @@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) - Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) - Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868) +- MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) ## [1.8.2] - 2019-07-08 diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 5bc0a435..48434300 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -464,9 +464,10 @@ class LookupRef * * @param mixed $lookupValue The value that you want to match in lookup_array * @param mixed $lookupArray The range of cells being searched - * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below. If match_type is 1 or -1, the list has to be ordered. + * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below. + * If match_type is 1 or -1, the list has to be ordered. * - * @return int The relative position of the found item + * @return int|string The relative position of the found item */ public static function MATCH($lookupValue, $lookupArray, $matchType = 1) { @@ -474,9 +475,10 @@ class LookupRef $lookupValue = Functions::flattenSingleValue($lookupValue); $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType); - $initialLookupValue = $lookupValue; - // MATCH is not case sensitive - $lookupValue = StringHelper::strToLower($lookupValue); + // MATCH is not case sensitive, so we convert lookup value to be lower cased in case it's string type. + if (is_string($lookupValue)) { + $lookupValue = StringHelper::strToLower($lookupValue); + } // Lookup_value type has to be number, text, or logical values if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) { @@ -522,16 +524,54 @@ class LookupRef // find the match // ** - if ($matchType == 0 || $matchType == 1) { + if ($matchType === 0 || $matchType === 1) { foreach ($lookupArray as $i => $lookupArrayValue) { - $onlyNumeric = is_numeric($lookupArrayValue) && is_numeric($lookupValue); - $onlyNumericExactMatch = $onlyNumeric && $lookupArrayValue == $lookupValue; - $nonOnlyNumericExactMatch = !$onlyNumeric && $lookupArrayValue === $lookupValue; - $exactMatch = $onlyNumericExactMatch || $nonOnlyNumericExactMatch; - if (($matchType == 0) && $exactMatch) { - // exact match - return $i + 1; - } elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) { + $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue; + $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue; + $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch; + + if ($matchType === 0) { + if ($typeMatch && is_string($lookupValue) && (bool) preg_match('/([\?\*])/', $lookupValue)) { + $splitString = $lookupValue; + $chars = array_map(function ($i) use ($splitString) { + return mb_substr($splitString, $i, 1); + }, range(0, mb_strlen($splitString) - 1)); + + $length = count($chars); + $pattern = '/^'; + for ($j = 0; $j < $length; ++$j) { + if ($chars[$j] === '~') { + if (isset($chars[$j + 1])) { + if ($chars[$j + 1] === '*') { + $pattern .= preg_quote($chars[$j + 1], '/'); + ++$j; + } elseif ($chars[$j + 1] === '?') { + $pattern .= preg_quote($chars[$j + 1], '/'); + ++$j; + } + } else { + $pattern .= preg_quote($chars[$j], '/'); + } + } elseif ($chars[$j] === '*') { + $pattern .= '.*'; + } elseif ($chars[$j] === '?') { + $pattern .= '.{1}'; + } else { + $pattern .= preg_quote($chars[$j], '/'); + } + } + + $pattern .= '$/'; + if ((bool) preg_match($pattern, $lookupArrayValue)) { + // exact match + return $i + 1; + } + } elseif ($exactMatch) { + // exact match + return $i + 1; + } + } elseif (($matchType === 1) && $typeMatch && ($lookupArrayValue <= $lookupValue)) { $i = array_search($i, $keySet); // The current value is the (first) match @@ -539,26 +579,26 @@ class LookupRef } } } else { - // matchType = -1 - - // "Special" case: since the array it's supposed to be ordered in descending order, the - // Excel algorithm gives up immediately if the first element is smaller than the searched value - if ($lookupArray[0] < $lookupValue) { - return Functions::NA(); - } - $maxValueKey = null; // The basic algorithm is: // Iterate and keep the highest match until the next element is smaller than the searched value. // Return immediately if perfect match is found foreach ($lookupArray as $i => $lookupArrayValue) { - if ($lookupArrayValue == $lookupValue) { + $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue; + $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue; + $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch; + + if ($exactMatch) { // Another "special" case. If a perfect match is found, // the algorithm gives up immediately return $i + 1; - } elseif ($lookupArrayValue >= $lookupValue) { + } elseif ($typeMatch & $lookupArrayValue >= $lookupValue) { $maxValueKey = $i + 1; + } elseif ($typeMatch & $lookupArrayValue < $lookupValue) { + //Excel algorithm gives up immediately if the first element is smaller than the searched value + break; } } diff --git a/tests/data/Calculation/LookupRef/MATCH.php b/tests/data/Calculation/LookupRef/MATCH.php index 84644949..54ff9f1f 100644 --- a/tests/data/Calculation/LookupRef/MATCH.php +++ b/tests/data/Calculation/LookupRef/MATCH.php @@ -104,5 +104,145 @@ return [ [[0], [0], ['x'], ['x'], ['x']], 0 ], - + [ + 2, + 'a', + [false, 'a',1], + -1 + ], + [ + '#N/A', // Expected + 0, + ['x', true, false], + -1 + ], + [ + '#N/A', // Expected + true, + ['a', 'b', 'c'], + -1 + ], + [ + '#N/A', // Expected + true, + [0,1,2], + -1 + ], + [ + '#N/A', // Expected + true, + [0,1,2], + 0 + ], + [ + '#N/A', // Expected + true, + [0,1,2], + 1 + ], + [ + 1, // Expected + true, + [true,true,true], + -1 + ], + [ + 1, // Expected + true, + [true,true,true], + 0 + ], + [ + 3, // Expected + true, + [true,true,true], + 1 + ], + // lookup stops when value < searched one + [ + 5, // Expected + 6, + [true, false, 'a', 'z', 222222, 2, 99999999], + -1 + ], + // if element of same data type met and it is < than searched one #N/A - no further processing + [ + '#N/A', // Expected + 6, + [true, false, 'a', 'z', 2, 888 ], + -1 + ], + [ + '#N/A', // Expected + 6, + ['6'], + -1 + ], + // expression match + [ + 2, // Expected + 'a?b', + ['a', 'abb', 'axc'], + 0 + ], + [ + 1, // Expected + 'a*', + ['aAAAAAAA', 'as', 'az'], + 0 + ], + [ + 3, // Expected + '1*11*1', + ['abc', 'efh', '1a11b1'], + 0 + ], + [ + 3, // Expected + '1*11*1', + ['abc', 'efh', '1a11b1'], + 0 + ], + [ + 2, // Expected + 'a*~*c', + ['aAAAAA', 'a123456*c', 'az'], + 0 + ], + [ + 3, // Expected + 'a*123*b', + ['aAAAAA', 'a123456*c', 'a99999123b'], + 0 + ], + [ + 1, // Expected + '*', + ['aAAAAA', 'a111123456*c', 'qq'], + 0 + ], + [ + 2, // Expected + '?', + ['aAAAAA', 'a', 'a99999123b'], + 0 + ], + [ + '#N/A', // Expected + '?', + [1, 22,333], + 0 + ], + [ + 3, // Expected + '???', + [1, 22,'aaa'], + 0 + ], + [ + 3, // Expected + '*', + [1, 22,'aaa'], + 0 + ], ]; From 34675bdf5d736ee796abf552e75a1660cfab4b66 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Mon, 12 Aug 2019 10:03:41 -0700 Subject: [PATCH 09/17] Remove dead code --- src/PhpSpreadsheet/Calculation/Calculation.php | 1 - tests/data/Calculation/Calculation.php | 16 +++++++--------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 449553a4..a131fca9 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3592,7 +3592,6 @@ class Calculation $valToUpper = strtoupper($val); // here $matches[1] will contain values like "IF" // and $val "IF(" - $injectStoreKey = null; if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if $pendingStoreKey = $this->getUnusedBranchStoreKey(); $pendingStoreKeysStack[] = $pendingStoreKey; diff --git a/tests/data/Calculation/Calculation.php b/tests/data/Calculation/Calculation.php index 09538c3c..201df47a 100644 --- a/tests/data/Calculation/Calculation.php +++ b/tests/data/Calculation/Calculation.php @@ -14,8 +14,6 @@ function calculationTestDataGenerator() $formula1 = '=IF(A1="please +",SUM(A2:C2),7 + IF(B1="please *", 4, 2))'; $set2 = [3, $dataArray1, $formula1, 'E5']; - $originalDataArray = $dataArray1; - $dataArray1[0][0] = 'not please + something else'; $set3 = [11, $dataArray1, $formula1, 'E5']; @@ -39,11 +37,11 @@ function calculationTestDataGenerator() $dataArray4 = [ ['noflag', 0, 0], [127000, 0, 0], - [ 10000, 0.03, 0], - [ 20000, 0.06, 0], - [ 40000, 0.09, 0], - [ 70000, 0.12, 0], - [ 90000, 0.03, 0] + [10000, 0.03, 0], + [20000, 0.06, 0], + [40000, 0.09, 0], + [70000, 0.12, 0], + [90000, 0.03, 0], ]; $formula2 = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))'; $set7 = [false, $dataArray4, $formula2, 'E5']; @@ -52,12 +50,12 @@ function calculationTestDataGenerator() [1, 2], [3, 4], ['=A1+A2', '=SUM(B1:B2)'], - [ 'take A', 0] + ['take A', 0], ]; $formula3 = '=IF(A4="take A", A3, B3)'; $set8 = [4, $dataArray5, $formula3, 'E5', ['A3'], ['B3']]; return [$set0, $set1, $set2, $set3, $set4, $set5, $set6, $set7, $set8]; -}; +} return calculationTestDataGenerator(); From 95c8bb9918b43a1f088cab54c4684e1ef40caaaa Mon Sep 17 00:00:00 2001 From: Nathanael Noblet Date: Wed, 14 Aug 2019 10:04:21 -0600 Subject: [PATCH 10/17] Allow HTML Reader to load from string We often want to export a table as an excel sheet. The system renders the html and it seems like a waste of time to write it to the file system to use the reader. This allows us to render the html and then just pass it to a reader Closes #1136 --- CHANGELOG.md | 1 + docs/topics/reading-and-writing-to-file.md | 28 +++++++++ src/PhpSpreadsheet/Reader/Html.php | 58 +++++++++++++++---- tests/PhpSpreadsheetTests/Reader/HtmlTest.php | 30 ++++++++++ 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20d26f9e..3891842e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - HLookup needs an ordered list even if range_lookup is set to false [Issue #1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [PR #1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076) - Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844) - MATCH function supports `*?~` Excel functionality, when match_type=0 - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) +- Allow HTML Reader to accept HTML as a string [Issue #1136](https://github.com/PHPOffice/PhpSpreadsheet/pull/1136) ### Fixed diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 0b27f8c1..b26cc6a9 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -875,3 +875,31 @@ $writer->save('write.xls'); ``` Notice that it is ok to load an xlsx file and generate an xls file. + +## Generating Excel files from HTML content + +If you are generating an Excel file from pre-rendered HTML content you can do so +automatically using the HTML Reader. This is most useful when you are generating +Excel files from web application content that would be downloaded/sent to a user. + +For example: + +```php +$htmlString = ' + + + + + + + + + +
Hello World
Hello
World
Hello
World
'; + +$reader = new \PhpOffice\PhpSpreadsheet\Reader\Html(); +$spreadsheet = $reader->loadFromString($htmlString); + +$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls'); +$writer->save('write.xls'); +``` diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index ff2c909e..bf9c6038 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -592,28 +592,64 @@ class Html extends BaseReader throw new Exception($pFilename . ' is an Invalid HTML file.'); } - // Create new sheet - while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { - $spreadsheet->createSheet(); - } - $spreadsheet->setActiveSheetIndex($this->sheetIndex); - - // Create a new DOM object + // Create a new DOM object $dom = new DOMDocument(); - // Reload the HTML file into the DOM object + // Reload the HTML file into the DOM object $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scanFile($pFilename), 'HTML-ENTITIES', 'UTF-8')); if ($loaded === false) { throw new Exception('Failed to load ' . $pFilename . ' as a DOM Document'); } - // Discard white space - $dom->preserveWhiteSpace = false; + return $this->loadDocument($dom, $spreadsheet); + } + + /** + * Spreadsheet from content. + * + * @param string $content + * + * @throws Exception + * + * @return Spreadsheet + */ + public function loadFromString($content): Spreadsheet + { + // Create a new DOM object + $dom = new DOMDocument(); + // Reload the HTML file into the DOM object + $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8')); + if ($loaded === false) { + throw new Exception('Failed to load content as a DOM Document'); + } + + return $this->loadDocument($dom, new Spreadsheet()); + } + + /** + * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance. + * + * @param DOMDocument $document + * @param Spreadsheet $spreadsheet + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * + * @return Spreadsheet + */ + private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet + { + while ($spreadsheet->getSheetCount() <= $this->sheetIndex) { + $spreadsheet->createSheet(); + } + $spreadsheet->setActiveSheetIndex($this->sheetIndex); + + // Discard white space + $document->preserveWhiteSpace = false; $row = 0; $column = 'A'; $content = ''; $this->rowspan = []; - $this->processDomElement($dom, $spreadsheet->getActiveSheet(), $row, $column, $content); + $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content); // Return return $spreadsheet; diff --git a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/HtmlTest.php index e8b00f1a..e9dd207f 100644 --- a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/HtmlTest.php @@ -299,6 +299,36 @@ class HtmlTest extends TestCase unlink($filename); } + public function testCanLoadFromString() + { + $html = ' + + + + + + + + + +
Hello World
Hello
World
Hello
World
'; + $spreadsheet = (new Html())->loadFromString($html); + $firstSheet = $spreadsheet->getSheet(0); + + $cellStyle = $firstSheet->getStyle('A1'); + self::assertFalse($cellStyle->getAlignment()->getWrapText()); + + $cellStyle = $firstSheet->getStyle('A2'); + self::assertTrue($cellStyle->getAlignment()->getWrapText()); + $cellValue = $firstSheet->getCell('A2')->getValue(); + $this->assertContains("\n", $cellValue); + + $cellStyle = $firstSheet->getStyle('A3'); + self::assertTrue($cellStyle->getAlignment()->getWrapText()); + $cellValue = $firstSheet->getCell('A3')->getValue(); + $this->assertContains("\n", $cellValue); + } + /** * @param string $html * From bbbfdb86a0762e7893e28c1bda45165198d18fdb Mon Sep 17 00:00:00 2001 From: yunjusu Date: Tue, 30 Jul 2019 17:04:15 +0800 Subject: [PATCH 11/17] Fix `getCalculatedValue()` error with more than two INDIRECT Closes #1115 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Calculation/Calculation.php | 4 ++++ .../Calculation/CalculationTest.php | 16 ++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3891842e..8a8894fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) - Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868) - MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) +- Fix `getCalculatedValue()` error with more than two INDIRECT [#1115](https://github.com/PHPOffice/PhpSpreadsheet/pull/1115) ## [1.8.2] - 2019-07-08 diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index a131fca9..f2e6647f 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4140,6 +4140,9 @@ class Calculation // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) { + if ($pCellParent) { + $pCell->attach($pCellParent); + } if (($cellID == 'AC99') || (isset($pCell) && $pCell->getCoordinate() == 'AC99')) { if (defined('RESOLVING')) { define('RESOLVING2', true); @@ -4215,6 +4218,7 @@ class Calculation } unset($arg); } + $result = call_user_func_array($functionCall, $args); if ($functionName != 'MKMATRIX') { diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 6bceee0f..4cdfe5cb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -164,6 +164,22 @@ class CalculationTest extends TestCase self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue()); } + public function testCellWithFormulaTwoIndirect() + { + $spreadsheet = new Spreadsheet(); + $workSheet = $spreadsheet->getActiveSheet(); + $cell1 = $workSheet->getCell('A1'); + $cell1->setValue('2'); + $cell2 = $workSheet->getCell('B1'); + $cell2->setValue('3'); + $cell2 = $workSheet->getCell('C1'); + $cell2->setValue('4'); + $cell3 = $workSheet->getCell('D1'); + $cell3->setValue('=SUM(INDIRECT("A"&ROW()),INDIRECT("B"&ROW()),INDIRECT("C"&ROW()))'); + + self::assertEquals('9', $cell3->getCalculatedValue()); + } + public function testBranchPruningFormulaParsingSimpleCase() { $calculation = Calculation::getInstance(); From ed253655315c29a577032b9f0d74b07d01546194 Mon Sep 17 00:00:00 2001 From: Andrey Dovbyshko Date: Tue, 14 May 2019 18:37:51 +0300 Subject: [PATCH 12/17] Fix Writer\Html did not hide columns Closes #985 --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Writer/Html.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8894fd..aad9bb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868) - MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) - Fix `getCalculatedValue()` error with more than two INDIRECT [#1115](https://github.com/PHPOffice/PhpSpreadsheet/pull/1115) +- Writer\Html did not hide columns [#985](https://github.com/PHPOffice/PhpSpreadsheet/pull/985) ## [1.8.2] - 2019-07-08 diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index ebda5c33..d626e9c4 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -891,8 +891,8 @@ class Html extends BaseWriter $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt'; if ($columnDimension->getVisible() === false) { - $css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse'; - $css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7 + $css['table.sheet' . $sheetIndex . ' .column' . $column]['visibility'] = 'collapse'; + $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none'; // target IE6+7 } } } From 05081c4acfd654694f6613f8e0a1b861813a75eb Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 17 Aug 2019 13:31:40 -0700 Subject: [PATCH 13/17] Standardize changelog to the simplest format --- CHANGELOG.md | 202 +++++++++++++++++++++++++-------------------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aad9bb99..6f445fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,26 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- When <br> appears in a table cell, set the cell to wrap [Issue #1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [PR #1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070) -- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF - [Issue #1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056) -- HLookup needs an ordered list even if range_lookup is set to false [Issue #1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [PR #1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076) +- When <br> appears in a table cell, set the cell to wrap [#1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [#1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070) +- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF [#1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056) +- HLookup needs an ordered list even if range_lookup is set to false [#1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [#1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076) - Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844) -- MATCH function supports `*?~` Excel functionality, when match_type=0 - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) -- Allow HTML Reader to accept HTML as a string [Issue #1136](https://github.com/PHPOffice/PhpSpreadsheet/pull/1136) +- MATCH function supports `*?~` Excel functionality, when match_type=0 [#1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) +- Allow HTML Reader to accept HTML as a string [#1136](https://github.com/PHPOffice/PhpSpreadsheet/pull/1136) ### Fixed - Fix to AVERAGEIF() function when called with a third argument -- Eliminate duplicate fill none style entries [Issue #1066](https://github.com/PHPOffice/PhpSpreadsheet/issues/1066) -- Fix number format masks containing literal (non-decimal point) dots [Issue #1079](https://github.com/PHPOffice/PhpSpreadsheet/issues/1079) -- Fix number format masks containing named colours that were being misinterpreted as date formats; and add support for masks that fully replace the value with a full text string [Issue #1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009) -- Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [Issue #1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046) -- COUPNUM should not return zero when settlement is in the last period - [Issue #1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [PR #1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021) +- Eliminate duplicate fill none style entries [#1066](https://github.com/PHPOffice/PhpSpreadsheet/issues/1066) +- Fix number format masks containing literal (non-decimal point) dots [#1079](https://github.com/PHPOffice/PhpSpreadsheet/issues/1079) +- Fix number format masks containing named colours that were being misinterpreted as date formats; and add support for masks that fully replace the value with a full text string [#1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009) +- Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [#1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046) +- COUPNUM should not return zero when settlement is in the last period [#1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [#1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021) - Fix handling of named ranges referencing sheets with spaces or "!" in their title -- Cover `getSheetByName()` with tests for name with quote and spaces - [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) -- Best effort to support invalid colspan values in HTML reader - [878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) +- Cover `getSheetByName()` with tests for name with quote and spaces [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739) +- Best effort to support invalid colspan values in HTML reader - [#878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878) - Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868) -- MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) - [Issue #1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) +- MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) [#1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116) - Fix `getCalculatedValue()` error with more than two INDIRECT [#1115](https://github.com/PHPOffice/PhpSpreadsheet/pull/1115) - Writer\Html did not hide columns [#985](https://github.com/PHPOffice/PhpSpreadsheet/pull/985) @@ -36,14 +36,14 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Uncaught error when opening ods file and properties aren't defined - [Issue #1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047) -- Xlsx Reader Cell datavalidations bug - [PR #1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052) +- Uncaught error when opening ods file and properties aren't defined [#1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047) +- Xlsx Reader Cell datavalidations bug [#1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052) ## [1.8.1] - 2019-07-02 ### Fixed -- Allow nullable theme for Xlsx Style Reader class - [Issue #1043](https://github.com/PHPOffice/PhpSpreadsheet/issues/1043) +- Allow nullable theme for Xlsx Style Reader class [#1043](https://github.com/PHPOffice/PhpSpreadsheet/issues/1043) ## [1.8.0] - 2019-07-01 @@ -56,10 +56,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). `XmlScanner::threadSafeLibxmlDisableEntityLoaderAvailability()` - Provide an option to disable the libxml_disable_entity_loader call through settings. This is not recommended as it reduces the security of the XML-based readers, and should only be used if you understand the consequences and have no other choice. - + ### Added -- Added support for the SWITCH function - [Issue #963](https://github.com/PHPOffice/PhpSpreadsheet/issues/963) and [PR #983](https://github.com/PHPOffice/PhpSpreadsheet/pull/983) +- Added support for the SWITCH function [#963](https://github.com/PHPOffice/PhpSpreadsheet/issues/963) and [#983](https://github.com/PHPOffice/PhpSpreadsheet/pull/983) - Add accounting number format style [#974](https://github.com/PHPOffice/PhpSpreadsheet/pull/974) ### Fixed @@ -79,7 +79,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix handling for escaped enclosures and new lines in CSV Separator Inference - Fix MATCH an error was appearing when comparing strings against 0 (always true) - Fix wrong calculation of highest column with specified row [#700](https://github.com/PHPOffice/PhpSpreadsheet/issues/700) -- Fix VLOOKUP +- Fix VLOOKUP - Fix return type hint ## [1.6.0] - 2019-01-02 @@ -87,24 +87,24 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Refactored Matrix Functions to use external Matrix library -- Possibility to specify custom colors of values for pie and donut charts - [#768](https://github.com/PHPOffice/PhpSpreadsheet/pull/768) +- Possibility to specify custom colors of values for pie and donut charts [#768](https://github.com/PHPOffice/PhpSpreadsheet/pull/768) ### Fixed -- Improve XLSX parsing speed if no readFilter is applied - [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772) -- Fix column names if read filter calls in XLSX reader skip columns - [#777](https://github.com/PHPOffice/PhpSpreadsheet/pull/777) -- XLSX reader can now ignore blank cells, using the setReadEmptyCells(false) method. - [#810](https://github.com/PHPOffice/PhpSpreadsheet/issues/810) -- Fix LOOKUP function which was breaking on edge cases - [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796) -- Fix VLOOKUP with exact matches - [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809) -- Support COUNTIFS multiple arguments - [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830) -- Change `libxml_disable_entity_loader()` as shortly as possible - [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819) -- Improved memory usage and performance when loading large spreadsheets - [#822](https://github.com/PHPOffice/PhpSpreadsheet/pull/822) -- Improved performance when loading large spreadsheets - [#825](https://github.com/PHPOffice/PhpSpreadsheet/pull/825) -- Improved performance when loading large spreadsheets - [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824) -- Fix color from CSS when reading from HTML - [#831](https://github.com/PHPOffice/PhpSpreadsheet/pull/831) -- Fix infinite loop when reading invalid ODS files - [#832](https://github.com/PHPOffice/PhpSpreadsheet/pull/832) -- Fix time format for duration is incorrect - [#666](https://github.com/PHPOffice/PhpSpreadsheet/pull/666) -- Fix iconv unsupported `//IGNORE//TRANSLIT` on IBM i - [#791](https://github.com/PHPOffice/PhpSpreadsheet/issues/791) +- Improve XLSX parsing speed if no readFilter is applied [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772) +- Fix column names if read filter calls in XLSX reader skip columns [#777](https://github.com/PHPOffice/PhpSpreadsheet/pull/777) +- XLSX reader can now ignore blank cells, using the setReadEmptyCells(false) method. [#810](https://github.com/PHPOffice/PhpSpreadsheet/issues/810) +- Fix LOOKUP function which was breaking on edge cases [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796) +- Fix VLOOKUP with exact matches [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809) +- Support COUNTIFS multiple arguments [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830) +- Change `libxml_disable_entity_loader()` as shortly as possible [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819) +- Improved memory usage and performance when loading large spreadsheets [#822](https://github.com/PHPOffice/PhpSpreadsheet/pull/822) +- Improved performance when loading large spreadsheets [#825](https://github.com/PHPOffice/PhpSpreadsheet/pull/825) +- Improved performance when loading large spreadsheets [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824) +- Fix color from CSS when reading from HTML [#831](https://github.com/PHPOffice/PhpSpreadsheet/pull/831) +- Fix infinite loop when reading invalid ODS files [#832](https://github.com/PHPOffice/PhpSpreadsheet/pull/832) +- Fix time format for duration is incorrect [#666](https://github.com/PHPOffice/PhpSpreadsheet/pull/666) +- Fix iconv unsupported `//IGNORE//TRANSLIT` on IBM i [#791](https://github.com/PHPOffice/PhpSpreadsheet/issues/791) ### Changed @@ -114,59 +114,59 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Security -- Improvements to the design of the XML Security Scanner - [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771) +- Improvements to the design of the XML Security Scanner [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771) ## [1.5.1] - 2018-11-20 ### Security -- Fix and improve XXE security scanning for XML-based and HTML Readers - [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771) +- Fix and improve XXE security scanning for XML-based and HTML Readers [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771) ### Added -- Support page margin in mPDF - [#750](https://github.com/PHPOffice/PhpSpreadsheet/issues/750) +- Support page margin in mPDF [#750](https://github.com/PHPOffice/PhpSpreadsheet/issues/750) ### Fixed -- Support numeric condition in SUMIF, SUMIFS, AVERAGEIF, COUNTIF, MAXIF and MINIF - [#683](https://github.com/PHPOffice/PhpSpreadsheet/issues/683) -- SUMIFS containing multiple conditions - [#704](https://github.com/PHPOffice/PhpSpreadsheet/issues/704) -- Csv reader avoid notice when the file is empty - [#743](https://github.com/PHPOffice/PhpSpreadsheet/pull/743) -- Fix print area parser for XLSX reader - [#734](https://github.com/PHPOffice/PhpSpreadsheet/pull/734) -- Support overriding `DefaultValueBinder::dataTypeForValue()` without overriding `DefaultValueBinder::bindValue()` - [#735](https://github.com/PHPOffice/PhpSpreadsheet/pull/735) -- Mpdf export can exceed pcre.backtrack_limit - [#637](https://github.com/PHPOffice/PhpSpreadsheet/issues/637) -- Fix index overflow on data values array - [#748](https://github.com/PHPOffice/PhpSpreadsheet/pull/748) +- Support numeric condition in SUMIF, SUMIFS, AVERAGEIF, COUNTIF, MAXIF and MINIF [#683](https://github.com/PHPOffice/PhpSpreadsheet/issues/683) +- SUMIFS containing multiple conditions [#704](https://github.com/PHPOffice/PhpSpreadsheet/issues/704) +- Csv reader avoid notice when the file is empty [#743](https://github.com/PHPOffice/PhpSpreadsheet/pull/743) +- Fix print area parser for XLSX reader [#734](https://github.com/PHPOffice/PhpSpreadsheet/pull/734) +- Support overriding `DefaultValueBinder::dataTypeForValue()` without overriding `DefaultValueBinder::bindValue()` [#735](https://github.com/PHPOffice/PhpSpreadsheet/pull/735) +- Mpdf export can exceed pcre.backtrack_limit [#637](https://github.com/PHPOffice/PhpSpreadsheet/issues/637) +- Fix index overflow on data values array [#748](https://github.com/PHPOffice/PhpSpreadsheet/pull/748) ## [1.5.0] - 2018-10-21 ### Added - PHP 7.3 support -- Add the DAYS() function - [#594](https://github.com/PHPOffice/PhpSpreadsheet/pull/594) +- Add the DAYS() function [#594](https://github.com/PHPOffice/PhpSpreadsheet/pull/594) ### Fixed -- Sheet title can contain exclamation mark - [#325](https://github.com/PHPOffice/PhpSpreadsheet/issues/325) -- Xls file cause the exception during open by Xls reader - [#402](https://github.com/PHPOffice/PhpSpreadsheet/issues/402) -- Skip non numeric value in SUMIF - [#618](https://github.com/PHPOffice/PhpSpreadsheet/pull/618) -- OFFSET should allow omitted height and width - [#561](https://github.com/PHPOffice/PhpSpreadsheet/issues/561) -- Correctly determine delimiter when CSV contains line breaks inside enclosures - [#716](https://github.com/PHPOffice/PhpSpreadsheet/issues/716) +- Sheet title can contain exclamation mark [#325](https://github.com/PHPOffice/PhpSpreadsheet/issues/325) +- Xls file cause the exception during open by Xls reader [#402](https://github.com/PHPOffice/PhpSpreadsheet/issues/402) +- Skip non numeric value in SUMIF [#618](https://github.com/PHPOffice/PhpSpreadsheet/pull/618) +- OFFSET should allow omitted height and width [#561](https://github.com/PHPOffice/PhpSpreadsheet/issues/561) +- Correctly determine delimiter when CSV contains line breaks inside enclosures [#716](https://github.com/PHPOffice/PhpSpreadsheet/issues/716) ## [1.4.1] - 2018-09-30 ### Fixed -- Remove locale from formatting string - [#644](https://github.com/PHPOffice/PhpSpreadsheet/pull/644) -- Allow iterators to go out of bounds with prev - [#587](https://github.com/PHPOffice/PhpSpreadsheet/issues/587) -- Fix warning when reading xlsx without styles - [#631](https://github.com/PHPOffice/PhpSpreadsheet/pull/631) -- Fix broken sample links on windows due to $baseDir having backslash - [#653](https://github.com/PHPOffice/PhpSpreadsheet/pull/653) +- Remove locale from formatting string [#644](https://github.com/PHPOffice/PhpSpreadsheet/pull/644) +- Allow iterators to go out of bounds with prev [#587](https://github.com/PHPOffice/PhpSpreadsheet/issues/587) +- Fix warning when reading xlsx without styles [#631](https://github.com/PHPOffice/PhpSpreadsheet/pull/631) +- Fix broken sample links on windows due to $baseDir having backslash [#653](https://github.com/PHPOffice/PhpSpreadsheet/pull/653) ## [1.4.0] - 2018-08-06 ### Added -- Add excel function EXACT(value1, value2) support - [#595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595) -- Support workbook view attributes for Xlsx format - [#523](https://github.com/PHPOffice/PhpSpreadsheet/issues/523) -- Read and write hyperlink for drawing image - [#490](https://github.com/PHPOffice/PhpSpreadsheet/pull/490) +- Add excel function EXACT(value1, value2) support [#595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595) +- Support workbook view attributes for Xlsx format [#523](https://github.com/PHPOffice/PhpSpreadsheet/issues/523) +- Read and write hyperlink for drawing image [#490](https://github.com/PHPOffice/PhpSpreadsheet/pull/490) - Added calculation engine support for the new bitwise functions that were added in MS Excel 2013 - BITAND() Returns a Bitwise 'And' of two numbers - BITOR() Returns a Bitwise 'Or' of two number @@ -210,15 +210,15 @@ and this project adheres to [Semantic Versioning](https://semver.org). - IMSEC() Returns the secant of a complex number - IMSECH() Returns the hyperbolic secant of a complex number - IMSINH() Returns the hyperbolic sine of a complex number - - IMTAN() Returns the tangent of a complex number + - IMTAN() Returns the tangent of a complex number ### Fixed - Fix ISFORMULA() function to work with a cell reference to another worksheet -- Xlsx reader crashed when reading a file with workbook protection - [#553](https://github.com/PHPOffice/PhpSpreadsheet/pull/553) -- Cell formats with escaped spaces were causing incorrect date formatting - [#557](https://github.com/PHPOffice/PhpSpreadsheet/issues/557) -- Could not open CSV file containing HTML fragment - [#564](https://github.com/PHPOffice/PhpSpreadsheet/issues/564) -- Exclude the vendor folder in migration - [#481](https://github.com/PHPOffice/PhpSpreadsheet/issues/481) +- Xlsx reader crashed when reading a file with workbook protection [#553](https://github.com/PHPOffice/PhpSpreadsheet/pull/553) +- Cell formats with escaped spaces were causing incorrect date formatting [#557](https://github.com/PHPOffice/PhpSpreadsheet/issues/557) +- Could not open CSV file containing HTML fragment [#564](https://github.com/PHPOffice/PhpSpreadsheet/issues/564) +- Exclude the vendor folder in migration [#481](https://github.com/PHPOffice/PhpSpreadsheet/issues/481) - Chained operations on cell ranges involving borders operated on last cell only [#428](https://github.com/PHPOffice/PhpSpreadsheet/issues/428) - Avoid memory exhaustion when cloning worksheet with a drawing [#437](https://github.com/PHPOffice/PhpSpreadsheet/issues/437) - Migration tool keep variables containing $PHPExcel untouched [#598](https://github.com/PHPOffice/PhpSpreadsheet/issues/598) @@ -228,83 +228,83 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Ranges across Z and AA columns incorrectly threw an exception - [#545](https://github.com/PHPOffice/PhpSpreadsheet/issues/545) +- Ranges across Z and AA columns incorrectly threw an exception [#545](https://github.com/PHPOffice/PhpSpreadsheet/issues/545) ## [1.3.0] - 2018-06-10 ### Added -- Support to read Xlsm templates with form elements, macros, printer settings, protected elements and back compatibility drawing, and save result without losing important elements of document - [#435](https://github.com/PHPOffice/PhpSpreadsheet/issues/435) -- Expose sheet title maximum length as `Worksheet::SHEET_TITLE_MAXIMUM_LENGTH` - [#482](https://github.com/PHPOffice/PhpSpreadsheet/issues/482) -- Allow escape character to be set in CSV reader – [#492](https://github.com/PHPOffice/PhpSpreadsheet/issues/492) +- Support to read Xlsm templates with form elements, macros, printer settings, protected elements and back compatibility drawing, and save result without losing important elements of document [#435](https://github.com/PHPOffice/PhpSpreadsheet/issues/435) +- Expose sheet title maximum length as `Worksheet::SHEET_TITLE_MAXIMUM_LENGTH` [#482](https://github.com/PHPOffice/PhpSpreadsheet/issues/482) +- Allow escape character to be set in CSV reader [#492](https://github.com/PHPOffice/PhpSpreadsheet/issues/492) ### Fixed -- Subtotal 9 in a group that has other subtotals 9 exclude the totals of the other subtotals in the range - [#332](https://github.com/PHPOffice/PhpSpreadsheet/issues/332) -- `Helper\Html` support UTF-8 HTML input - [#444](https://github.com/PHPOffice/PhpSpreadsheet/issues/444) -- Xlsx loaded an extra empty comment for each real comment - [#375](https://github.com/PHPOffice/PhpSpreadsheet/issues/375) -- Xlsx reader do not read rows and columns filtered out in readFilter at all - [#370](https://github.com/PHPOffice/PhpSpreadsheet/issues/370) -- Make newer Excel versions properly recalculate formulas on document open - [#456](https://github.com/PHPOffice/PhpSpreadsheet/issues/456) -- `Coordinate::extractAllCellReferencesInRange()` throws an exception for an invalid range – [#519](https://github.com/PHPOffice/PhpSpreadsheet/issues/519) -- Fixed parsing of conditionals in COUNTIF functions - [#526](https://github.com/PHPOffice/PhpSpreadsheet/issues/526) -- Corruption errors for saved Xlsx docs with frozen panes - [#532](https://github.com/PHPOffice/PhpSpreadsheet/issues/532) +- Subtotal 9 in a group that has other subtotals 9 exclude the totals of the other subtotals in the range [#332](https://github.com/PHPOffice/PhpSpreadsheet/issues/332) +- `Helper\Html` support UTF-8 HTML input [#444](https://github.com/PHPOffice/PhpSpreadsheet/issues/444) +- Xlsx loaded an extra empty comment for each real comment [#375](https://github.com/PHPOffice/PhpSpreadsheet/issues/375) +- Xlsx reader do not read rows and columns filtered out in readFilter at all [#370](https://github.com/PHPOffice/PhpSpreadsheet/issues/370) +- Make newer Excel versions properly recalculate formulas on document open [#456](https://github.com/PHPOffice/PhpSpreadsheet/issues/456) +- `Coordinate::extractAllCellReferencesInRange()` throws an exception for an invalid range [#519](https://github.com/PHPOffice/PhpSpreadsheet/issues/519) +- Fixed parsing of conditionals in COUNTIF functions [#526](https://github.com/PHPOffice/PhpSpreadsheet/issues/526) +- Corruption errors for saved Xlsx docs with frozen panes [#532](https://github.com/PHPOffice/PhpSpreadsheet/issues/532) ## [1.2.1] - 2018-04-10 ### Fixed -- Plain text and richtext mixed in same cell can be read - [#442](https://github.com/PHPOffice/PhpSpreadsheet/issues/442) +- Plain text and richtext mixed in same cell can be read [#442](https://github.com/PHPOffice/PhpSpreadsheet/issues/442) ## [1.2.0] - 2018-03-04 ### Added -- HTML writer creates a generator meta tag - [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312) -- Support invalid zoom value in XLSX format - [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350) -- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` - [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390) +- HTML writer creates a generator meta tag [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312) +- Support invalid zoom value in XLSX format [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350) +- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390) ### Fixed -- Avoid potentially unsupported PSR-16 cache keys - [#354](https://github.com/PHPOffice/PhpSpreadsheet/issues/354) -- Check for MIME type to know if CSV reader can read a file - [#167](https://github.com/PHPOffice/PhpSpreadsheet/issues/167) -- Use proper € symbol for currency format - [#379](https://github.com/PHPOffice/PhpSpreadsheet/pull/379) -- Read printing area correctly when skipping some sheets - [#371](https://github.com/PHPOffice/PhpSpreadsheet/issues/371) -- Avoid incorrectly overwriting calculated value type - [#394](https://github.com/PHPOffice/PhpSpreadsheet/issues/394) -- Select correct cell when calling freezePane - [#389](https://github.com/PHPOffice/PhpSpreadsheet/issues/389) -- `setStrikethrough()` did not set the font - [#403](https://github.com/PHPOffice/PhpSpreadsheet/issues/403) +- Avoid potentially unsupported PSR-16 cache keys [#354](https://github.com/PHPOffice/PhpSpreadsheet/issues/354) +- Check for MIME type to know if CSV reader can read a file [#167](https://github.com/PHPOffice/PhpSpreadsheet/issues/167) +- Use proper € symbol for currency format [#379](https://github.com/PHPOffice/PhpSpreadsheet/pull/379) +- Read printing area correctly when skipping some sheets [#371](https://github.com/PHPOffice/PhpSpreadsheet/issues/371) +- Avoid incorrectly overwriting calculated value type [#394](https://github.com/PHPOffice/PhpSpreadsheet/issues/394) +- Select correct cell when calling freezePane [#389](https://github.com/PHPOffice/PhpSpreadsheet/issues/389) +- `setStrikethrough()` did not set the font [#403](https://github.com/PHPOffice/PhpSpreadsheet/issues/403) ## [1.1.0] - 2018-01-28 ### Added - Support for PHP 7.2 -- Support cell comments in HTML writer and reader - [#308](https://github.com/PHPOffice/PhpSpreadsheet/issues/308) -- Option to stop at a conditional styling, if it matches (only XLSX format) - [#292](https://github.com/PHPOffice/PhpSpreadsheet/pull/292) -- Support for line width for data series when rendering Xlsx - [#329](https://github.com/PHPOffice/PhpSpreadsheet/pull/329) +- Support cell comments in HTML writer and reader [#308](https://github.com/PHPOffice/PhpSpreadsheet/issues/308) +- Option to stop at a conditional styling, if it matches (only XLSX format) [#292](https://github.com/PHPOffice/PhpSpreadsheet/pull/292) +- Support for line width for data series when rendering Xlsx [#329](https://github.com/PHPOffice/PhpSpreadsheet/pull/329) ### Fixed -- Better auto-detection of CSV separators - [#305](https://github.com/PHPOffice/PhpSpreadsheet/issues/305) -- Support for shape style ending with `;` - [#304](https://github.com/PHPOffice/PhpSpreadsheet/issues/304) -- Freeze Panes takes wrong coordinates for XLSX - [#322](https://github.com/PHPOffice/PhpSpreadsheet/issues/322) -- `COLUMNS` and `ROWS` functions crashed in some cases - [#336](https://github.com/PHPOffice/PhpSpreadsheet/issues/336) -- Support XML file without styles - [#331](https://github.com/PHPOffice/PhpSpreadsheet/pull/331) +- Better auto-detection of CSV separators [#305](https://github.com/PHPOffice/PhpSpreadsheet/issues/305) +- Support for shape style ending with `;` [#304](https://github.com/PHPOffice/PhpSpreadsheet/issues/304) +- Freeze Panes takes wrong coordinates for XLSX [#322](https://github.com/PHPOffice/PhpSpreadsheet/issues/322) +- `COLUMNS` and `ROWS` functions crashed in some cases [#336](https://github.com/PHPOffice/PhpSpreadsheet/issues/336) +- Support XML file without styles [#331](https://github.com/PHPOffice/PhpSpreadsheet/pull/331) - Cell coordinates which are already a range cause an exception [#319](https://github.com/PHPOffice/PhpSpreadsheet/issues/319) ## [1.0.0] - 2017-12-25 ### Added -- Support to write merged cells in ODS format - [#287](https://github.com/PHPOffice/PhpSpreadsheet/issues/287) -- Able to set the `topLeftCell` in freeze panes - [#261](https://github.com/PHPOffice/PhpSpreadsheet/pull/261) +- Support to write merged cells in ODS format [#287](https://github.com/PHPOffice/PhpSpreadsheet/issues/287) +- Able to set the `topLeftCell` in freeze panes [#261](https://github.com/PHPOffice/PhpSpreadsheet/pull/261) - Support `DateTimeImmutable` as cell value - Support migration of prefixed classes ### Fixed -- Can read very small HTML files - [#194](https://github.com/PHPOffice/PhpSpreadsheet/issues/194) -- Written DataValidation was corrupted - [#290](https://github.com/PHPOffice/PhpSpreadsheet/issues/290) -- Date format compatible with both LibreOffice and Excel - [#298](https://github.com/PHPOffice/PhpSpreadsheet/issues/298) +- Can read very small HTML files [#194](https://github.com/PHPOffice/PhpSpreadsheet/issues/194) +- Written DataValidation was corrupted [#290](https://github.com/PHPOffice/PhpSpreadsheet/issues/290) +- Date format compatible with both LibreOffice and Excel [#298](https://github.com/PHPOffice/PhpSpreadsheet/issues/298) ### BREAKING CHANGE @@ -323,13 +323,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Merge data-validations to reduce written worksheet size - @billblume [#131](https://github.com/PHPOffice/PhpSpreadSheet/issues/131) - Throws exception if a XML file is invalid - @GreatHumorist [#222](https://github.com/PHPOffice/PhpSpreadsheet/pull/222) -- Upgrade to mPDF 7.0+ - [#144](https://github.com/PHPOffice/PhpSpreadsheet/issues/144) +- Upgrade to mPDF 7.0+ [#144](https://github.com/PHPOffice/PhpSpreadsheet/issues/144) ### Fixed -- Control characters in cell values are automatically escaped - [#212](https://github.com/PHPOffice/PhpSpreadsheet/issues/212) +- Control characters in cell values are automatically escaped [#212](https://github.com/PHPOffice/PhpSpreadsheet/issues/212) - Prevent color changing when copy/pasting xls files written by PhpSpreadsheet to another file - @al-lala [#218](https://github.com/PHPOffice/PhpSpreadsheet/issues/218) -- Add cell reference automatic when there is no cell reference('r' attribute) in Xlsx file. - @GreatHumorist [#225](https://github.com/PHPOffice/PhpSpreadsheet/pull/225) Refer to [issue#201](https://github.com/PHPOffice/PhpSpreadsheet/issues/201) +- Add cell reference automatic when there is no cell reference('r' attribute) in Xlsx file. - @GreatHumorist [#225](https://github.com/PHPOffice/PhpSpreadsheet/pull/225) Refer to [#201](https://github.com/PHPOffice/PhpSpreadsheet/issues/201) - `Reader\Xlsx::getFromZipArchive()` function return false if the zip entry could not be located. - @anton-harvey [#268](https://github.com/PHPOffice/PhpSpreadsheet/pull/268) ### BREAKING CHANGE From 74bc0b826d47991fa2fee9fce375515a3499b963 Mon Sep 17 00:00:00 2001 From: Claudio Galdiolo Date: Wed, 7 Aug 2019 17:28:44 +0200 Subject: [PATCH 14/17] reword repeated text --- docs/topics/reading-and-writing-to-file.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index b26cc6a9..66455356 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -173,10 +173,9 @@ code: $writer->setOffice2003Compatibility(true); $writer->save("05featuredemo.xlsx"); -**Office2003 compatibility should only be used when needed** Office2003 -compatibility option should only be used when needed. This option -disables several Office2007 file format options, resulting in a -lower-featured Office2007 spreadsheet when this option is used. +**Office2003 compatibility option should only be used when needed** because +it disables several Office2007 file format options, resulting in a +lower-featured Office2007 spreadsheet. ## Excel 5 (BIFF) file format From 8dea03eaf60a349b6097e4bcad11f894668280df Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 17 Aug 2019 15:24:35 -0700 Subject: [PATCH 15/17] 1.9.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f445fe6..f894c764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## [Unreleased] +## [1.9.0] - 2019-08-17 ### Added From 48ccdc271674070528f7252bfbb9c32a58df2813 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 17 Aug 2019 15:28:41 -0700 Subject: [PATCH 16/17] Prepare for next version --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f894c764..d69ee8e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## [Unreleased] + +### Added + +- ... + +### Fixed + +- ... + ## [1.9.0] - 2019-08-17 ### Added From a972943ac56895905d9cbaf103d17e9e25fb8b50 Mon Sep 17 00:00:00 2001 From: Adrien Crivelli Date: Sat, 24 Aug 2019 12:42:22 -0700 Subject: [PATCH 17/17] Mention PHP version change in changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d69ee8e4..153b1acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). ## [1.9.0] - 2019-08-17 +### Changed + +- Drop support for PHP 5.6 and 7.0, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support + ### Added - When <br> appears in a table cell, set the cell to wrap [#1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [#1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070)