From 60358f26afaacda9cf49dc8354cec4fd83edf2ea Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sun, 3 May 2020 07:06:12 -0700 Subject: [PATCH 1/2] Save Excel 2010+ Functions Properly For functions introduced in Excel 2010 and beyond, Excel saves them in formulas with the xlfn_ prefix. PhpSpreadsheet does not do this; as a result, when a spreadsheet so created is opened, the cells which use the new functions display a #NAME? error. This the cause of bug report 1246: https://github.com/PHPOffice/PhpSpreadsheet/issues/1246 This change corrects that problem when the Xlsx writer encounters a 2010+ formula for a cell or a conditional style. A new class Writer/Xlsx/Xlfn, with 2 static methods, is introduced to facilitate this change. As part of the testing for this, I found some additional problems. When an unknown function name is used, Excel generates a #NAME? error. However, when an unknown function is used in PhpSpreadsheet: - if there are no parameters, it returns #VALUE!, which is wrong - if there are parameters, it throws an exception, which is horrible Both of these situations will now return #NAME? Tests have been added for these situations. The MODE (and MODE.SNGL) function is not quite in alignment with Excel. MODE(3, 3, 4, 4) returns 3 in both Excel and PhpSpreadsheet. However, MODE(4, 3, 3, 4) returns 4 in Excel, but 3 in PhpSpreadsheet. Both situations will now match Excel's result. Also, Excel allows its parameters for MODE to be an array, but PhpSpreadsheet did not; it now will. There had not been any tests for MODE. Now there are. The SHEET and SHEETS functions were introduced in Excel 2013, but were not introduced in PhpSpreadsheet. They are now introduced as DUMMY functions so that they can be parsed appropriately. Finally, in common with the "rate" changes for which I am creating a pull request at the same time as this one: samples/Basic/13_CalculationCyclicFormulae PhpUnit started reporting an error like "too much regression". The test deals with an infinite cyclic formula, and allowed the calculation engine to run for 100 cycles. The actual number of cycles seems irrelevant for the purpose of this test. I changed it to 15, and PhpUnit no longer complains. --- .../Basic/13_CalculationCyclicFormulae.php | 2 +- .../Calculation/Calculation.php | 62 ++-- .../Calculation/Statistical.php | 27 +- .../Calculation/functionlist.txt | 2 + src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 275 ++++++++++-------- src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php | 167 +++++++++++ .../Calculation/CalculationTest.php | 14 + .../Functions/Statistical/ModeTest.php | 43 +++ .../Calculation/XlfnFunctionsTest.php | 99 +++++++ .../Functional/ConditionalTextTest.php | 107 +++++++ tests/data/Calculation/Statistical/MODE.php | 11 + 11 files changed, 655 insertions(+), 154 deletions(-) create mode 100644 src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ModeTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php create mode 100644 tests/PhpSpreadsheetTests/Functional/ConditionalTextTest.php create mode 100644 tests/data/Calculation/Statistical/MODE.php diff --git a/samples/Basic/13_CalculationCyclicFormulae.php b/samples/Basic/13_CalculationCyclicFormulae.php index 26e9784db4..a446e56e08 100644 --- a/samples/Basic/13_CalculationCyclicFormulae.php +++ b/samples/Basic/13_CalculationCyclicFormulae.php @@ -16,7 +16,7 @@ ->setCellValue('B1', '=A1+1') ->setCellValue('B2', '=A2'); -Calculation::getInstance($spreadsheet)->cyclicFormulaCount = 100; +Calculation::getInstance($spreadsheet)->cyclicFormulaCount = 15; // Calculated data $helper->log('Calculated data'); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 69f72033a3..d64f53b234 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1853,6 +1853,16 @@ class Calculation 'functionCall' => [MathTrig::class, 'SERIESSUM'], 'argumentCount' => '4', ], + 'SHEET' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '0,1', + ], + 'SHEETS' => [ + 'category' => Category::CATEGORY_INFORMATION, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '0,1', + ], 'SIGN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, 'functionCall' => [MathTrig::class, 'SIGN'], @@ -2247,6 +2257,10 @@ class Calculation 'argumentCount' => '*', 'functionCall' => [__CLASS__, 'mkMatrix'], ], + 'NAME.ERROR' => [ + 'argumentCount' => '*', + 'functionCall' => [Functions::class, 'NAME'], + ], ]; public function __construct(Spreadsheet $spreadsheet = null) @@ -3625,33 +3639,33 @@ private function _parseFormula($formula, Cell $pCell = null) $val = preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function $valToUpper = strtoupper($val); - // here $matches[1] will contain values like "IF" - // and $val "IF(" - 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; - } + } else { + $valToUpper = 'NAME.ERROR('; + } + // here $matches[1] will contain values like "IF" + // and $val "IF(" + 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 an if but we go 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 ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); - $expectingOperator = true; - } else { - $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); - $expectingOperator = false; - } - $stack->push('Brace', '('); - } else { // it's a var w/ implicit multiplication - $output[] = ['type' => 'Value', 'value' => $matches[1], 'reference' => null]; + $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + // tests if the function is closed right after opening + $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length)); + if ($ax) { + $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $expectingOperator = true; + } else { + $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $expectingOperator = false; } + $stack->push('Brace', '('); } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) { // Watch for this case-change when modifying to allow cell references in different worksheets... // Should only be applied to the actual cell column, not the worksheet name diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index b1c7fb020e..2b7fd8c92d 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -2468,11 +2468,27 @@ public static function MINIFS(...$args) private static function modeCalc($data) { $frequencyArray = []; + $index = 0; + $maxfreq = 0; + $maxfreqkey = ''; + $maxfreqdatum = ''; foreach ($data as $datum) { $found = false; + ++$index; foreach ($frequencyArray as $key => $value) { if ((string) $value['value'] == (string) $datum) { ++$frequencyArray[$key]['frequency']; + $freq = $frequencyArray[$key]['frequency']; + if ($freq > $maxfreq) { + $maxfreq = $freq; + $maxfreqkey = $key; + $maxfreqdatum = $datum; + } elseif ($freq == $maxfreq) { + if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { + $maxfreqkey = $key; + $maxfreqdatum = $datum; + } + } $found = true; break; @@ -2482,21 +2498,16 @@ private static function modeCalc($data) $frequencyArray[] = [ 'value' => $datum, 'frequency' => 1, + 'index' => $index, ]; } } - foreach ($frequencyArray as $key => $value) { - $frequencyList[$key] = $value['frequency']; - $valueList[$key] = $value['value']; - } - array_multisort($frequencyList, SORT_DESC, $valueList, SORT_ASC, SORT_NUMERIC, $frequencyArray); - - if ($frequencyArray[0]['frequency'] == 1) { + if ($maxfreq <= 1) { return Functions::NA(); } - return $frequencyArray[0]['value']; + return $maxfreqdatum; } /** diff --git a/src/PhpSpreadsheet/Calculation/functionlist.txt b/src/PhpSpreadsheet/Calculation/functionlist.txt index 77fd4ee0e4..2556ec9028 100644 --- a/src/PhpSpreadsheet/Calculation/functionlist.txt +++ b/src/PhpSpreadsheet/Calculation/functionlist.txt @@ -316,6 +316,8 @@ SEC SECH SECOND SERIESSUM +SHEET +SHEETS SIGN SIN SINH diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 1d5a995a8b..e8f66a792b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -454,6 +454,55 @@ private function writeSheetProtection(XMLWriter $objWriter, PhpspreadsheetWorksh $objWriter->endElement(); } + private static function writeAttributeIf($cond, $objWriter, $attr, $val) + { + if ($cond) { + $objWriter->writeAttribute($attr, $val); + } + } + + private static function writeElementIf($cond, $objWriter, $attr, $val) + { + if ($cond) { + $objWriter->writeElement($attr, $val); + } + } + + private static function writeOtherCondElements($objWriter, $conditional, $cellCoordinate) + { + if ($conditional->getConditionType() == Conditional::CONDITION_CELLIS + || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT + || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { + foreach ($conditional->getConditions() as $formula) { + // Formula + $objWriter->writeElement('formula', Xlfn::addXlfn($formula)); + } + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) { + // formula copied from ms xlsx xml source file + $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) { + // formula copied from ms xlsx xml source file + $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0'); + } + } + + private static function writeTextCondElements($objWriter, $conditional, $cellCoordinate) + { + $txt = $conditional->getText(); + if ($txt !== null) { + $objWriter->writeAttribute('text', $txt); + if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) { + $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))'); + } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) { + $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"'); + } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) { + $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"'); + } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) { + $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))'); + } + } + } + /** * Write ConditionalFormatting. * @@ -485,49 +534,20 @@ private function writeConditionalFormatting(XMLWriter $objWriter, Phpspreadsheet $objWriter->writeAttribute('dxfId', $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode())); $objWriter->writeAttribute('priority', $id++); - if (($conditional->getConditionType() == Conditional::CONDITION_CELLIS || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT) - && $conditional->getOperatorType() != Conditional::OPERATOR_NONE) { - $objWriter->writeAttribute('operator', $conditional->getOperatorType()); - } + self::writeAttributeif( + ($conditional->getConditionType() == Conditional::CONDITION_CELLIS || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT) + && $conditional->getOperatorType() != Conditional::OPERATOR_NONE, + $objWriter, + 'operator', + $conditional->getOperatorType() + ); - if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - && $conditional->getText() !== null) { - $objWriter->writeAttribute('text', $conditional->getText()); - } + self::writeAttributeIf($conditional->getStopIfTrue(), $objWriter, 'stopIfTrue', '1'); - if ($conditional->getStopIfTrue()) { - $objWriter->writeAttribute('stopIfTrue', '1'); - } - - if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - && $conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT - && $conditional->getText() !== null) { - $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $conditional->getText() . '",' . $cellCoordinate . ')))'); - } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - && $conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH - && $conditional->getText() !== null) { - $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',' . strlen($conditional->getText()) . ')="' . $conditional->getText() . '"'); - } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - && $conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH - && $conditional->getText() !== null) { - $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',' . strlen($conditional->getText()) . ')="' . $conditional->getText() . '"'); - } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - && $conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS - && $conditional->getText() !== null) { - $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $conditional->getText() . '",' . $cellCoordinate . '))'); - } elseif ($conditional->getConditionType() == Conditional::CONDITION_CELLIS - || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { - foreach ($conditional->getConditions() as $formula) { - // Formula - $objWriter->writeElement('formula', $formula); - } - } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) { - // formula copied from ms xlsx xml source file - $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0'); - } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) { - // formula copied from ms xlsx xml source file - $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0'); + if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT) { + self::writeTextCondElements($objWriter, $conditional, $cellCoordinate); + } else { + self::writeOtherCondElements($objWriter, $conditional, $cellCoordinate); } $objWriter->endElement(); @@ -1037,6 +1057,91 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $p $objWriter->endElement(); } + private function writeCellInlineStr($mappedType, $objWriter, $cellValue) + { + $objWriter->writeAttribute('t', $mappedType); + if (!$cellValue instanceof RichText) { + $objWriter->writeElement('t', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue))); + } elseif ($cellValue instanceof RichText) { + $objWriter->startElement('is'); + $this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $cellValue); + $objWriter->endElement(); + } + } + + private function writeCellString($mappedType, $objWriter, $cellValue, $pFlippedStringTable) + { + $objWriter->writeAttribute('t', $mappedType); + if (!$cellValue instanceof RichText) { + self::writeElementIf(isset($pFlippedStringTable[$cellValue]), $objWriter, 'v', $pFlippedStringTable[$cellValue]); + } else { + $objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]); + } + } + + private function writeCellNumeric($objWriter, $cellValue) + { + //force a decimal to be written if the type is float + if (is_float($cellValue)) { + // force point as decimal separator in case current locale uses comma + $cellValue = str_replace(',', '.', (string) $cellValue); + if (strpos($cellValue, '.') === false) { + $cellValue = $cellValue . '.0'; + } + } + $objWriter->writeElement('v', $cellValue); + } + + private function writeCellBoolean($mappedType, $objWriter, $cellValue) + { + $objWriter->writeAttribute('t', $mappedType); + $objWriter->writeElement('v', ($cellValue ? '1' : '0')); + } + + private function writeCellError($mappedType, $objWriter, $cellValue, $formulaerr = '#NULL!') + { + $objWriter->writeAttribute('t', $mappedType); + $cellIsFormula = substr($cellValue, 0, 1) === '='; + self::writeElementIf($cellIsFormula, $objWriter, 'f', Xlfn::addXlfnStripEquals($cellValue)); + $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue); + } + + private function writeCellFormula($objWriter, $cellValue, $pCell) + { + $calculatedValue = ($this->getParentWriter()->getPreCalculateFormulas()) ? $pCell->getCalculatedValue() : $cellValue; + if (is_string($calculatedValue)) { + if (substr($calculatedValue, 0, 1) === '#') { + $this->writeCellError('e', $objWriter, $cellValue, $calculatedValue); + + return; + } + $objWriter->writeAttribute('t', 'str'); + } elseif (is_bool($calculatedValue)) { + $objWriter->writeAttribute('t', 'b'); + } + // array values are not yet supported + //$attributes = $pCell->getFormulaAttributes(); + //if (($attributes['t'] ?? null) === 'array') { + // $objWriter->startElement('f'); + // $objWriter->writeAttribute('t', 'array'); + // $objWriter->writeAttribute('ref', $pCellAddress); + // $objWriter->writeAttribute('aca', '1'); + // $objWriter->writeAttribute('ca', '1'); + // $objWriter->text(substr($cellValue, 1)); + // $objWriter->endElement(); + //} else { + // $objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue)); + //} + $objWriter->writeElement('f', Xlfn::addXlfnStripEquals($cellValue)); + self::writeElementIf( + $this->getParentWriter()->getOffice2003Compatibility() === false, + $objWriter, + 'v', + ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#') + ? StringHelper::formatNumber($calculatedValue) : '0' + ); + } + /** * Write Cell. * @@ -1055,9 +1160,8 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet $objWriter->writeAttribute('r', $pCellAddress); // Sheet styles - if ($pCell->getXfIndex() != '') { - $objWriter->writeAttribute('s', $pCell->getXfIndex()); - } + $xfi = $pCell->getXfIndex(); + self::writeAttributeIf($xfi, $objWriter, 's', $xfi); // If cell value is supplied, write cell value $cellValue = $pCell->getValue(); @@ -1065,101 +1169,30 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet // Map type $mappedType = $pCell->getDataType(); - // Write data type depending on its type - switch (strtolower($mappedType)) { - case 'inlinestr': // Inline string - case 's': // String - case 'b': // Boolean - $objWriter->writeAttribute('t', $mappedType); - - break; - case 'f': // Formula - $calculatedValue = ($this->getParentWriter()->getPreCalculateFormulas()) ? - $pCell->getCalculatedValue() : $cellValue; - if (is_string($calculatedValue)) { - $objWriter->writeAttribute('t', 'str'); - } elseif (is_bool($calculatedValue)) { - $objWriter->writeAttribute('t', 'b'); - } - - break; - case 'e': // Error - $objWriter->writeAttribute('t', $mappedType); - } - // Write data depending on its type switch (strtolower($mappedType)) { case 'inlinestr': // Inline string - if (!$cellValue instanceof RichText) { - $objWriter->writeElement('t', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue))); - } elseif ($cellValue instanceof RichText) { - $objWriter->startElement('is'); - $this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $cellValue); - $objWriter->endElement(); - } + $this->writeCellInlineStr($mappedType, $objWriter, $cellValue); break; case 's': // String - if (!$cellValue instanceof RichText) { - if (isset($pFlippedStringTable[$cellValue])) { - $objWriter->writeElement('v', $pFlippedStringTable[$cellValue]); - } - } elseif ($cellValue instanceof RichText) { - $objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]); - } + $this->writeCellString($mappedType, $objWriter, $cellValue, $pFlippedStringTable); break; case 'f': // Formula - $attributes = $pCell->getFormulaAttributes(); - if (($attributes['t'] ?? null) === 'array') { - $objWriter->startElement('f'); - $objWriter->writeAttribute('t', 'array'); - $objWriter->writeAttribute('ref', $pCellAddress); - $objWriter->writeAttribute('aca', '1'); - $objWriter->writeAttribute('ca', '1'); - $objWriter->text(substr($cellValue, 1)); - $objWriter->endElement(); - } else { - $objWriter->writeElement('f', substr($cellValue, 1)); - } - if ($this->getParentWriter()->getOffice2003Compatibility() === false) { - if ($this->getParentWriter()->getPreCalculateFormulas()) { - if (!is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#') { - $objWriter->writeElement('v', StringHelper::formatNumber($calculatedValue)); - } else { - $objWriter->writeElement('v', '0'); - } - } else { - $objWriter->writeElement('v', '0'); - } - } + self::writeCellFormula($objWriter, $cellValue, $pCell); break; case 'n': // Numeric - //force a decimal to be written if the type is float - if (is_float($cellValue)) { - // force point as decimal separator in case current locale uses comma - $cellValue = str_replace(',', '.', (string) $cellValue); - if (strpos($cellValue, '.') === false) { - $cellValue = $cellValue . '.0'; - } - } - $objWriter->writeElement('v', $cellValue); + $this->writeCellNumeric($objWriter, $cellValue); break; case 'b': // Boolean - $objWriter->writeElement('v', ($cellValue ? '1' : '0')); + $this->writeCellBoolean($mappedType, $objWriter, $cellValue); break; case 'e': // Error - if (substr($cellValue, 0, 1) === '=') { - $objWriter->writeElement('f', substr($cellValue, 1)); - $objWriter->writeElement('v', substr($cellValue, 1)); - } else { - $objWriter->writeElement('v', $cellValue); - } - - break; + $this->writeCellError($mappedType, $objWriter, $cellValue); } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php new file mode 100644 index 0000000000..9e889ace0f --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -0,0 +1,167 @@ +getActiveSheet(); + $sheet->setCellValue('A1', '=gzorg()'); + $sheet->setCellValue('A2', '=mode.gzorg(1)'); + $sheet->setCellValue('A3', '=gzorg(1,2)'); + $sheet->setCellValue('A4', '=3+IF(gzorg(),1,2)'); + self::assertEquals('#NAME?', $sheet->getCell('A1')->getCalculatedValue()); + self::assertEquals('#NAME?', $sheet->getCell('A2')->getCalculatedValue()); + self::assertEquals('#NAME?', $sheet->getCell('A3')->getCalculatedValue()); + self::assertEquals('#NAME?', $sheet->getCell('A4')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ModeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ModeTest.php new file mode 100644 index 0000000000..00dee00da5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ModeTest.php @@ -0,0 +1,43 @@ +getActiveSheet(); + + $row = 1; + $sheet->setCellValue("B$row", "=MODE($str)"); + $sheet->setCellValue("C$row", "=MODE.SNGL($str)"); + self::assertEquals($expectedResult, $sheet->getCell("B$row")->getCalculatedValue()); + self::assertEquals($expectedResult, $sheet->getCell("C$row")->getCalculatedValue()); + } + + public function providerMODE() + { + return require 'data/Calculation/Statistical/MODE.php'; + } + + public function testMODENoArgs() + { + $this->expectException(\PhpOffice\PhpSpreadsheet\Calculation\Exception::class); + + $workbook = new Spreadsheet(); + $sheet = $workbook->getActiveSheet(); + + $sheet->setCellValue('B1', '=MODE()'); + self::assertEquals('#N/A', $sheet->getCell('B1')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php new file mode 100644 index 0000000000..efe0c9343b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php @@ -0,0 +1,99 @@ +getActiveSheet(); + $sheet->setTitle('2010'); + $sheet = $workbook->createSheet(); + $sheet->setTitle('2013'); + $sheet = $workbook->createSheet(); + $sheet->setTitle('2016'); + $sheet = $workbook->createSheet(); + $sheet->setTitle('2019'); + + foreach ($formulas as $values) { + $sheet = $workbook->setActiveSheetIndexByName($values[0]); + $sheet->setCellValue($values[1], $values[2]); + } + + $sheet = $workbook->setActiveSheetIndexByName('2013'); + $sheet->getStyle('A3:A5')->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $sheet->getColumnDimension('A')->setAutoSize(true); + $condition0 = new Conditional(); + $condition0->setConditionType(Conditional::CONDITION_EXPRESSION); + $condition0->addCondition('ABS(B3)<2'); + $condition0->getStyle()->getFill()->setFillType(Fill::FILL_SOLID); + $condition0->getStyle()->getFill()->getEndColor()->setARGB(Color::COLOR_RED); + $condition1 = new Conditional(); + $condition1->setConditionType(Conditional::CONDITION_EXPRESSION); + $condition1->addCondition('ABS(B3)>2'); + $condition1->getStyle()->getFill()->setFillType(Fill::FILL_SOLID); + $condition1->getStyle()->getFill()->getEndColor()->setARGB(Color::COLOR_GREEN); + $cond = [$condition0, $condition1]; + $sheet->getStyle('B3:B5')->setConditionalStyles($cond); + $condition0 = new Conditional(); + $condition0->setConditionType(Conditional::CONDITION_EXPRESSION); + $condition0->addCondition('ISOWEEKNUM(A3)<10'); + $condition0->getStyle()->getFill()->setFillType(Fill::FILL_SOLID); + $condition0->getStyle()->getFill()->getEndColor()->setARGB(Color::COLOR_RED); + $condition1 = new Conditional(); + $condition1->setConditionType(Conditional::CONDITION_EXPRESSION); + $condition1->addCondition('ISOWEEKNUM(A3)>40'); + $condition1->getStyle()->getFill()->setFillType(Fill::FILL_SOLID); + $condition1->getStyle()->getFill()->getEndColor()->setARGB(Color::COLOR_GREEN); + $cond = [$condition0, $condition1]; + $sheet->getStyle('A3:A5')->setConditionalStyles($cond); + $sheet->setSelectedCell('B1'); + + $writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($workbook, 'Xlsx'); + $oufil = tempnam(File::sysGetTempDir(), 'phpspreadsheet-test'); + $writer->save($oufil); + + $reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx'); + $rdobj = $reader->load($oufil); + unlink($oufil); + foreach ($formulas as $values) { + $sheet = $rdobj->setActiveSheetIndexByName($values[0]); + self::assertEquals($values[3], $sheet->getCell($values[1])->getValue()); + if ($values[4] !== null) { + self::assertEquals($values[4], $sheet->getCell($values[1])->getCalculatedValue()); + } + } + $sheet = $rdobj->setActiveSheetIndexByName('2013'); + $cond = $sheet->getConditionalStyles('A3:A5'); + self::assertEquals('_xlfn.ISOWEEKNUM(A3)<10', $cond[0]->getConditions()[0]); + self::assertEquals('_xlfn.ISOWEEKNUM(A3)>40', $cond[1]->getConditions()[0]); + $cond = $sheet->getConditionalStyles('B3:B5'); + self::assertEquals('ABS(B3)<2', $cond[0]->getConditions()[0]); + self::assertEquals('ABS(B3)>2', $cond[1]->getConditions()[0]); + } +} diff --git a/tests/PhpSpreadsheetTests/Functional/ConditionalTextTest.php b/tests/PhpSpreadsheetTests/Functional/ConditionalTextTest.php new file mode 100644 index 0000000000..7cf1ef1142 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Functional/ConditionalTextTest.php @@ -0,0 +1,107 @@ +setConditionType(Conditional::CONDITION_CONTAINSTEXT); + $condition0->setOperatorType(Conditional::CONDITION_CONTAINSTEXT); + $condition0->setText('anywhere'); + $condition0->getStyle()->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(self::COLOR_RED); + array_push($conditionalStyles, $condition0); + + // if text contains 'Left' on left - green background + $condition1 = new Conditional(); + $condition1->setConditionType(Conditional::CONDITION_CONTAINSTEXT); + $condition1->setOperatorType(Conditional::OPERATOR_BEGINSWITH); + $condition1->setText('Left'); + $condition1->getStyle()->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(self::COLOR_GREEN); + array_push($conditionalStyles, $condition1); + + // if text contains 'right' on right - blue background + $condition2 = new Conditional(); + $condition2->setConditionType(Conditional::CONDITION_CONTAINSTEXT); + $condition2->setOperatorType(Conditional::OPERATOR_ENDSWITH); + $condition2->setText('right'); + $condition2->getStyle()->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(self::COLOR_BLUE); + array_push($conditionalStyles, $condition2); + + // if text contains no spaces - yellow background + $condition3 = new Conditional(); + $condition3->setConditionType(Conditional::CONDITION_CONTAINSTEXT); + $condition3->setOperatorType(Conditional::OPERATOR_NOTCONTAINS); + $condition3->setText(' '); + $condition3->getStyle()->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getEndColor()->setARGB(self::COLOR_YELLOW); + array_push($conditionalStyles, $condition3); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B1', 'This should match anywhere, right?'); + $sheet->setCellValue('B2', 'This should match nowhere, right?'); + $sheet->setCellValue('B3', 'Left match'); + $sheet->setCellValue('B4', 'Match on right'); + $sheet->setCellValue('B5', 'nospaces'); + $xpCoordinate = 'B1:B5'; + + $spreadsheet->getActiveSheet()->setConditionalStyles($xpCoordinate, $conditionalStyles); + $sheet->getColumnDimension('B')->setAutoSize(true); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + + // see if we successfully written conditional text elements + $newConditionalStyles = $reloadedSpreadsheet->getActiveSheet()->getConditionalStyles($xpCoordinate); + $cnt = count($conditionalStyles); + for ($i = 0; $i < $cnt; ++$i) { + self::assertEquals( + $conditionalStyles[$i]->getConditionType(), + $newConditionalStyles[$i]->getConditionType(), + "Failure on condition type $i" + ); + self::assertEquals( + $conditionalStyles[$i]->getOperatorType(), + $newConditionalStyles[$i]->getOperatorType(), + "Failure on operator type $i" + ); + self::assertEquals( + $conditionalStyles[$i]->getText(), + $newConditionalStyles[$i]->getText(), + "Failure on text $i" + ); + $filCond = $conditionalStyles[$i]->getStyle()->getFill(); + $newCond = $newConditionalStyles[$i]->getStyle()->getFill(); + self::assertEquals( + $filCond->getFillType(), + $newCond->getFillType(), + "Failure on fill type $i" + ); + self::assertEquals( + $filCond->getEndColor()->getARGB(), + $newCond->getEndColor()->getARGB(), + "Failure on end color $i" + ); + } + } +} diff --git a/tests/data/Calculation/Statistical/MODE.php b/tests/data/Calculation/Statistical/MODE.php new file mode 100644 index 0000000000..7d27b33e5e --- /dev/null +++ b/tests/data/Calculation/Statistical/MODE.php @@ -0,0 +1,11 @@ + Date: Sun, 3 May 2020 07:26:39 -0700 Subject: [PATCH 2/2] Scrutinizer-suggested Change One call which was static should be object call. --- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index e8f66a792b..60e4cbd207 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1180,7 +1180,7 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet break; case 'f': // Formula - self::writeCellFormula($objWriter, $cellValue, $pCell); + $this->writeCellFormula($objWriter, $cellValue, $pCell); break; case 'n': // Numeric