diff --git a/CHANGELOG.md b/CHANGELOG.md index fc9946b0..bbc39092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix Chart samples by updating chart parameter from 0 to DataSeries::EMPTY_AS_GAP [#1448](https://github.com/PHPOffice/PhpSpreadsheet/pull/1448) - Fix return type in docblock for the Cells::get() [#1398](https://github.com/PHPOffice/PhpSpreadsheet/pull/1398) - Fix RATE, PRICE, XIRR, and XNPV Functions [#1456](https://github.com/PHPOffice/PhpSpreadsheet/pull/1456) +- Save Excel 2010+ functions properly in XLSX [#1461](https://github.com/PHPOffice/PhpSpreadsheet/pull/1461) ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index e1469b1f..447ae588 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) @@ -3615,33 +3629,33 @@ class Calculation $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; - } - } - - $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]; + } 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*\)/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 b1c7fb02..2b7fd8c9 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -2468,11 +2468,27 @@ class Statistical 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 @@ class Statistical $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 77fd4ee0..2556ec90 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 0c042fe1..febee54d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -449,6 +449,55 @@ class Worksheet extends WriterPart $objWriter->endElement(); } + private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + { + if ($condition) { + $objWriter->writeAttribute($attr, $val); + } + } + + private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + { + if ($condition) { + $objWriter->writeElement($attr, $val); + } + } + + private static function writeOtherCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void + { + 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(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void + { + $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. * @@ -478,49 +527,20 @@ class Worksheet extends WriterPart $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( + $objWriter, + ($conditional->getConditionType() == Conditional::CONDITION_CELLIS || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT) + && $conditional->getOperatorType() != Conditional::OPERATOR_NONE, + 'operator', + $conditional->getOperatorType() + ); - if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT - && $conditional->getText() !== null) { - $objWriter->writeAttribute('text', $conditional->getText()); - } + self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), '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(); @@ -1028,15 +1048,115 @@ class Worksheet extends WriterPart $objWriter->endElement(); } + /** + * @param XMLWriter $objWriter + * @param string $mappedType + * @param RichText|string $cellValue + */ + private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, $cellValue): void + { + $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(); + } + } + + /** + * @param XMLWriter $objWriter + * @param string $mappedType + * @param RichText|string $cellValue + * @param string[] $pFlippedStringTable + */ + private function writeCellString(XMLWriter $objWriter, string $mappedType, $cellValue, array $pFlippedStringTable): void + { + $objWriter->writeAttribute('t', $mappedType); + if (!$cellValue instanceof RichText) { + self::writeElementIf($objWriter, isset($pFlippedStringTable[$cellValue]), 'v', $pFlippedStringTable[$cellValue]); + } else { + $objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]); + } + } + + /** + * @param XMLWriter $objWriter + * @param float|int $cellValue + */ + private function writeCellNumeric(XMLWriter $objWriter, $cellValue): void + { + //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(XMLWriter $objWriter, string $mappedType, bool $cellValue): void + { + $objWriter->writeAttribute('t', $mappedType); + $objWriter->writeElement('v', $cellValue ? '1' : '0'); + } + + private function writeCellError(XMLWriter $objWriter, string $mappedType, string $cellValue, string $formulaerr = '#NULL!'): void + { + $objWriter->writeAttribute('t', $mappedType); + $cellIsFormula = substr($cellValue, 0, 1) === '='; + self::writeElementIf($objWriter, $cellIsFormula, 'f', Xlfn::addXlfnStripEquals($cellValue)); + $objWriter->writeElement('v', $cellIsFormula ? $formulaerr : $cellValue); + } + + private function writeCellFormula(XMLWriter $objWriter, string $cellValue, Cell $pCell): void + { + $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $pCell->getCalculatedValue() : $cellValue; + if (is_string($calculatedValue)) { + if (substr($calculatedValue, 0, 1) === '#') { + $this->writeCellError($objWriter, 'e', $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( + $objWriter, + $this->getParentWriter()->getOffice2003Compatibility() === false, + 'v', + ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#') + ? StringHelper::formatNumber($calculatedValue) : '0' + ); + } + /** * Write Cell. * * @param XMLWriter $objWriter XML Writer * @param PhpspreadsheetWorksheet $pSheet Worksheet - * @param Cell $pCellAddress Cell Address + * @param string $pCellAddress Cell Address * @param string[] $pFlippedStringTable String table (flipped), for faster index searching */ - private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, $pCellAddress, array $pFlippedStringTable) + private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, string $pCellAddress, array $pFlippedStringTable) { // Cell $pCell = $pSheet->getCell($pCellAddress); @@ -1044,9 +1164,8 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('r', $pCellAddress); // Sheet styles - if ($pCell->getXfIndex() != '') { - $objWriter->writeAttribute('s', $pCell->getXfIndex()); - } + $xfi = $pCell->getXfIndex(); + self::writeAttributeIf($objWriter, $xfi, 's', $xfi); // If cell value is supplied, write cell value $cellValue = $pCell->getValue(); @@ -1054,101 +1173,30 @@ class Worksheet extends WriterPart // 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($objWriter, $mappedType, $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($objWriter, $mappedType, $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'); - } - } + $this->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($objWriter, $mappedType, $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($objWriter, $mappedType, $cellValue); } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php new file mode 100644 index 00000000..8f7c07e8 --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -0,0 +1,159 @@ +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 00000000..d3eb6f32 --- /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(): array + { + return require 'tests/data/Calculation/Statistical/MODE.php'; + } + + public function testMODENoArgs(): void + { + $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 00000000..ea20fbf3 --- /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 00000000..c8db3bf0 --- /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 00000000..7d27b33e --- /dev/null +++ b/tests/data/Calculation/Statistical/MODE.php @@ -0,0 +1,11 @@ +