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.
This commit is contained in:
parent
414e5695ef
commit
4f6d4af396
|
@ -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 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 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)
|
- 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
|
### Changed
|
||||||
|
|
||||||
|
|
|
@ -1853,6 +1853,16 @@ class Calculation
|
||||||
'functionCall' => [MathTrig::class, 'SERIESSUM'],
|
'functionCall' => [MathTrig::class, 'SERIESSUM'],
|
||||||
'argumentCount' => '4',
|
'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' => [
|
'SIGN' => [
|
||||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||||
'functionCall' => [MathTrig::class, 'SIGN'],
|
'functionCall' => [MathTrig::class, 'SIGN'],
|
||||||
|
@ -2247,6 +2257,10 @@ class Calculation
|
||||||
'argumentCount' => '*',
|
'argumentCount' => '*',
|
||||||
'functionCall' => [__CLASS__, 'mkMatrix'],
|
'functionCall' => [__CLASS__, 'mkMatrix'],
|
||||||
],
|
],
|
||||||
|
'NAME.ERROR' => [
|
||||||
|
'argumentCount' => '*',
|
||||||
|
'functionCall' => [Functions::class, 'NAME'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(Spreadsheet $spreadsheet = null)
|
public function __construct(Spreadsheet $spreadsheet = null)
|
||||||
|
@ -3615,33 +3629,33 @@ class Calculation
|
||||||
$val = preg_replace('/\s/u', '', $val);
|
$val = preg_replace('/\s/u', '', $val);
|
||||||
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
||||||
$valToUpper = strtoupper($val);
|
$valToUpper = strtoupper($val);
|
||||||
// here $matches[1] will contain values like "IF"
|
} else {
|
||||||
// and $val "IF("
|
$valToUpper = 'NAME.ERROR(';
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
// 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)) {
|
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) {
|
||||||
// Watch for this case-change when modifying to allow cell references in different worksheets...
|
// 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
|
// Should only be applied to the actual cell column, not the worksheet name
|
||||||
|
|
|
@ -2468,11 +2468,27 @@ class Statistical
|
||||||
private static function modeCalc($data)
|
private static function modeCalc($data)
|
||||||
{
|
{
|
||||||
$frequencyArray = [];
|
$frequencyArray = [];
|
||||||
|
$index = 0;
|
||||||
|
$maxfreq = 0;
|
||||||
|
$maxfreqkey = '';
|
||||||
|
$maxfreqdatum = '';
|
||||||
foreach ($data as $datum) {
|
foreach ($data as $datum) {
|
||||||
$found = false;
|
$found = false;
|
||||||
|
++$index;
|
||||||
foreach ($frequencyArray as $key => $value) {
|
foreach ($frequencyArray as $key => $value) {
|
||||||
if ((string) $value['value'] == (string) $datum) {
|
if ((string) $value['value'] == (string) $datum) {
|
||||||
++$frequencyArray[$key]['frequency'];
|
++$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;
|
$found = true;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -2482,21 +2498,16 @@ class Statistical
|
||||||
$frequencyArray[] = [
|
$frequencyArray[] = [
|
||||||
'value' => $datum,
|
'value' => $datum,
|
||||||
'frequency' => 1,
|
'frequency' => 1,
|
||||||
|
'index' => $index,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($frequencyArray as $key => $value) {
|
if ($maxfreq <= 1) {
|
||||||
$frequencyList[$key] = $value['frequency'];
|
|
||||||
$valueList[$key] = $value['value'];
|
|
||||||
}
|
|
||||||
array_multisort($frequencyList, SORT_DESC, $valueList, SORT_ASC, SORT_NUMERIC, $frequencyArray);
|
|
||||||
|
|
||||||
if ($frequencyArray[0]['frequency'] == 1) {
|
|
||||||
return Functions::NA();
|
return Functions::NA();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $frequencyArray[0]['value'];
|
return $maxfreqdatum;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -316,6 +316,8 @@ SEC
|
||||||
SECH
|
SECH
|
||||||
SECOND
|
SECOND
|
||||||
SERIESSUM
|
SERIESSUM
|
||||||
|
SHEET
|
||||||
|
SHEETS
|
||||||
SIGN
|
SIGN
|
||||||
SIN
|
SIN
|
||||||
SINH
|
SINH
|
||||||
|
|
|
@ -449,6 +449,55 @@ class Worksheet extends WriterPart
|
||||||
$objWriter->endElement();
|
$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.
|
* Write ConditionalFormatting.
|
||||||
*
|
*
|
||||||
|
@ -478,49 +527,20 @@ class Worksheet extends WriterPart
|
||||||
$objWriter->writeAttribute('dxfId', $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()));
|
$objWriter->writeAttribute('dxfId', $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()));
|
||||||
$objWriter->writeAttribute('priority', $id++);
|
$objWriter->writeAttribute('priority', $id++);
|
||||||
|
|
||||||
if (($conditional->getConditionType() == Conditional::CONDITION_CELLIS || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT)
|
self::writeAttributeif(
|
||||||
&& $conditional->getOperatorType() != Conditional::OPERATOR_NONE) {
|
$objWriter,
|
||||||
$objWriter->writeAttribute('operator', $conditional->getOperatorType());
|
($conditional->getConditionType() == Conditional::CONDITION_CELLIS || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT)
|
||||||
}
|
&& $conditional->getOperatorType() != Conditional::OPERATOR_NONE,
|
||||||
|
'operator',
|
||||||
|
$conditional->getOperatorType()
|
||||||
|
);
|
||||||
|
|
||||||
if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT
|
self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1');
|
||||||
&& $conditional->getText() !== null) {
|
|
||||||
$objWriter->writeAttribute('text', $conditional->getText());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($conditional->getStopIfTrue()) {
|
if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT) {
|
||||||
$objWriter->writeAttribute('stopIfTrue', '1');
|
self::writeTextCondElements($objWriter, $conditional, $cellCoordinate);
|
||||||
}
|
} else {
|
||||||
|
self::writeOtherCondElements($objWriter, $conditional, $cellCoordinate);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$objWriter->endElement();
|
$objWriter->endElement();
|
||||||
|
@ -1028,15 +1048,115 @@ class Worksheet extends WriterPart
|
||||||
$objWriter->endElement();
|
$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.
|
* Write Cell.
|
||||||
*
|
*
|
||||||
* @param XMLWriter $objWriter XML Writer
|
* @param XMLWriter $objWriter XML Writer
|
||||||
* @param PhpspreadsheetWorksheet $pSheet Worksheet
|
* @param PhpspreadsheetWorksheet $pSheet Worksheet
|
||||||
* @param Cell $pCellAddress Cell Address
|
* @param string $pCellAddress Cell Address
|
||||||
* @param string[] $pFlippedStringTable String table (flipped), for faster index searching
|
* @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
|
// Cell
|
||||||
$pCell = $pSheet->getCell($pCellAddress);
|
$pCell = $pSheet->getCell($pCellAddress);
|
||||||
|
@ -1044,9 +1164,8 @@ class Worksheet extends WriterPart
|
||||||
$objWriter->writeAttribute('r', $pCellAddress);
|
$objWriter->writeAttribute('r', $pCellAddress);
|
||||||
|
|
||||||
// Sheet styles
|
// Sheet styles
|
||||||
if ($pCell->getXfIndex() != '') {
|
$xfi = $pCell->getXfIndex();
|
||||||
$objWriter->writeAttribute('s', $pCell->getXfIndex());
|
self::writeAttributeIf($objWriter, $xfi, 's', $xfi);
|
||||||
}
|
|
||||||
|
|
||||||
// If cell value is supplied, write cell value
|
// If cell value is supplied, write cell value
|
||||||
$cellValue = $pCell->getValue();
|
$cellValue = $pCell->getValue();
|
||||||
|
@ -1054,101 +1173,30 @@ class Worksheet extends WriterPart
|
||||||
// Map type
|
// Map type
|
||||||
$mappedType = $pCell->getDataType();
|
$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
|
// Write data depending on its type
|
||||||
switch (strtolower($mappedType)) {
|
switch (strtolower($mappedType)) {
|
||||||
case 'inlinestr': // Inline string
|
case 'inlinestr': // Inline string
|
||||||
if (!$cellValue instanceof RichText) {
|
$this->writeCellInlineStr($objWriter, $mappedType, $cellValue);
|
||||||
$objWriter->writeElement('t', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue)));
|
|
||||||
} elseif ($cellValue instanceof RichText) {
|
|
||||||
$objWriter->startElement('is');
|
|
||||||
$this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $cellValue);
|
|
||||||
$objWriter->endElement();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 's': // String
|
case 's': // String
|
||||||
if (!$cellValue instanceof RichText) {
|
$this->writeCellString($objWriter, $mappedType, $cellValue, $pFlippedStringTable);
|
||||||
if (isset($pFlippedStringTable[$cellValue])) {
|
|
||||||
$objWriter->writeElement('v', $pFlippedStringTable[$cellValue]);
|
|
||||||
}
|
|
||||||
} elseif ($cellValue instanceof RichText) {
|
|
||||||
$objWriter->writeElement('v', $pFlippedStringTable[$cellValue->getHashCode()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'f': // Formula
|
case 'f': // Formula
|
||||||
$attributes = $pCell->getFormulaAttributes();
|
$this->writeCellFormula($objWriter, $cellValue, $pCell);
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'n': // Numeric
|
case 'n': // Numeric
|
||||||
//force a decimal to be written if the type is float
|
$this->writeCellNumeric($objWriter, $cellValue);
|
||||||
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);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'b': // Boolean
|
case 'b': // Boolean
|
||||||
$objWriter->writeElement('v', ($cellValue ? '1' : '0'));
|
$this->writeCellBoolean($objWriter, $mappedType, $cellValue);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'e': // Error
|
case 'e': // Error
|
||||||
if (substr($cellValue, 0, 1) === '=') {
|
$this->writeCellError($objWriter, $mappedType, $cellValue);
|
||||||
$objWriter->writeElement('f', substr($cellValue, 1));
|
|
||||||
$objWriter->writeElement('v', substr($cellValue, 1));
|
|
||||||
} else {
|
|
||||||
$objWriter->writeElement('v', $cellValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
|
||||||
|
class Xlfn
|
||||||
|
{
|
||||||
|
const XLFNREGEXP = '/(?<!_xlfn[.])\\b('
|
||||||
|
// functions added with Excel 2010
|
||||||
|
. 'beta[.]dist'
|
||||||
|
. '|beta[.]inv'
|
||||||
|
. '|binom[.]dist'
|
||||||
|
. '|binom[.]inv'
|
||||||
|
. '|chisq[.]dist'
|
||||||
|
. '|chisq[.]dist[.]rt'
|
||||||
|
. '|chisq[.]inv'
|
||||||
|
. '|chisq[.]inv[.]rt'
|
||||||
|
. '|chisq[.]test'
|
||||||
|
. '|confidence[.]norm'
|
||||||
|
. '|confidence[.]t'
|
||||||
|
. '|covariance[.]p'
|
||||||
|
. '|covariance[.]s'
|
||||||
|
. '|erf[.]precise'
|
||||||
|
. '|erfc[.]precise'
|
||||||
|
. '|expon[.]dist'
|
||||||
|
. '|f[.]dist'
|
||||||
|
. '|f[.]dist[.]rt'
|
||||||
|
. '|f[.]inv'
|
||||||
|
. '|f[.]inv[.]rt'
|
||||||
|
. '|f[.]test'
|
||||||
|
. '|gamma[.]dist'
|
||||||
|
. '|gamma[.]inv'
|
||||||
|
. '|gammaln[.]precise'
|
||||||
|
. '|lognorm[.]dist'
|
||||||
|
. '|lognorm[.]inv'
|
||||||
|
. '|mode[.]mult'
|
||||||
|
. '|mode[.]sngl'
|
||||||
|
. '|negbinom[.]dist'
|
||||||
|
. '|networkdays[.]intl'
|
||||||
|
. '|norm[.]dist'
|
||||||
|
. '|norm[.]inv'
|
||||||
|
. '|norm[.]s[.]dist'
|
||||||
|
. '|norm[.]s[.]inv'
|
||||||
|
. '|percentile[.]exc'
|
||||||
|
. '|percentile[.]inc'
|
||||||
|
. '|percentrank[.]exc'
|
||||||
|
. '|percentrank[.]inc'
|
||||||
|
. '|poisson[.]dist'
|
||||||
|
. '|quartile[.]exc'
|
||||||
|
. '|quartile[.]inc'
|
||||||
|
. '|rank[.]avg'
|
||||||
|
. '|rank[.]eq'
|
||||||
|
. '|stdev[.]p'
|
||||||
|
. '|stdev[.]s'
|
||||||
|
. '|t[.]dist'
|
||||||
|
. '|t[.]dist[.]2t'
|
||||||
|
. '|t[.]dist[.]rt'
|
||||||
|
. '|t[.]inv'
|
||||||
|
. '|t[.]inv[.]2t'
|
||||||
|
. '|t[.]test'
|
||||||
|
. '|var[.]p'
|
||||||
|
. '|var[.]s'
|
||||||
|
. '|weibull[.]dist'
|
||||||
|
. '|z[.]test'
|
||||||
|
// functions added with Excel 2013
|
||||||
|
. '|acot'
|
||||||
|
. '|acoth'
|
||||||
|
. '|arabic'
|
||||||
|
. '|averageifs'
|
||||||
|
. '|binom[.]dist[.]range'
|
||||||
|
. '|bitand'
|
||||||
|
. '|bitlshift'
|
||||||
|
. '|bitor'
|
||||||
|
. '|bitrshift'
|
||||||
|
. '|bitxor'
|
||||||
|
. '|ceiling[.]math'
|
||||||
|
. '|combina'
|
||||||
|
. '|cot'
|
||||||
|
. '|coth'
|
||||||
|
. '|csc'
|
||||||
|
. '|csch'
|
||||||
|
. '|days'
|
||||||
|
. '|dbcs'
|
||||||
|
. '|decimal'
|
||||||
|
. '|encodeurl'
|
||||||
|
. '|filterxml'
|
||||||
|
. '|floor[.]math'
|
||||||
|
. '|formulatext'
|
||||||
|
. '|gamma'
|
||||||
|
. '|gauss'
|
||||||
|
. '|ifna'
|
||||||
|
. '|imcosh'
|
||||||
|
. '|imcot'
|
||||||
|
. '|imcsc'
|
||||||
|
. '|imcsch'
|
||||||
|
. '|imsec'
|
||||||
|
. '|imsech'
|
||||||
|
. '|imsinh'
|
||||||
|
. '|imtan'
|
||||||
|
. '|isformula'
|
||||||
|
. '|iso[.]ceiling'
|
||||||
|
. '|isoweeknum'
|
||||||
|
. '|munit'
|
||||||
|
. '|numbervalue'
|
||||||
|
. '|pduration'
|
||||||
|
. '|permutationa'
|
||||||
|
. '|phi'
|
||||||
|
. '|rri'
|
||||||
|
. '|sec'
|
||||||
|
. '|sech'
|
||||||
|
. '|sheet'
|
||||||
|
. '|sheets'
|
||||||
|
. '|skew[.]p'
|
||||||
|
. '|unichar'
|
||||||
|
. '|unicode'
|
||||||
|
. '|webservice'
|
||||||
|
. '|xor'
|
||||||
|
// functions added with Excel 2016
|
||||||
|
. '|forecast[.]et2'
|
||||||
|
. '|forecast[.]ets[.]confint'
|
||||||
|
. '|forecast[.]ets[.]seasonality'
|
||||||
|
. '|forecast[.]ets[.]stat'
|
||||||
|
. '|forecast[.]linear'
|
||||||
|
. '|switch'
|
||||||
|
// functions added with Excel 2019
|
||||||
|
. '|concat'
|
||||||
|
. '|countifs'
|
||||||
|
. '|ifs'
|
||||||
|
. '|maxifs'
|
||||||
|
. '|minifs'
|
||||||
|
. '|sumifs'
|
||||||
|
. '|textjoin'
|
||||||
|
// functions added with Excel 365
|
||||||
|
. '|filter'
|
||||||
|
. '|randarray'
|
||||||
|
. '|sequence'
|
||||||
|
. '|sort'
|
||||||
|
. '|sortby'
|
||||||
|
. '|unique'
|
||||||
|
. '|xlookup'
|
||||||
|
. '|xmatch'
|
||||||
|
. ')(?=\\s*[(])/i';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix function name in string with _xlfn. where required.
|
||||||
|
*/
|
||||||
|
public static function addXlfn(string $funcstring): string
|
||||||
|
{
|
||||||
|
return preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefix function name in string with _xlfn. where required.
|
||||||
|
* Leading character, expected to be equals sign, is stripped.
|
||||||
|
*/
|
||||||
|
public static function addXlfnStripEquals(string $funcstring): string
|
||||||
|
{
|
||||||
|
return self::addXlfn(substr($funcstring, 1));
|
||||||
|
}
|
||||||
|
}
|
|
@ -310,6 +310,7 @@ class CalculationTest extends TestCase
|
||||||
* be set in cache
|
* be set in cache
|
||||||
* @param string[] $shouldNotBeSetInCacheCells coordinates of cells that must
|
* @param string[] $shouldNotBeSetInCacheCells coordinates of cells that must
|
||||||
* not be set in cache because of pruning
|
* not be set in cache because of pruning
|
||||||
|
*
|
||||||
* @dataProvider dataProviderBranchPruningFullExecution
|
* @dataProvider dataProviderBranchPruningFullExecution
|
||||||
*/
|
*/
|
||||||
public function testFullExecution(
|
public function testFullExecution(
|
||||||
|
@ -353,4 +354,18 @@ class CalculationTest extends TestCase
|
||||||
{
|
{
|
||||||
return require 'tests/data/Calculation/Calculation.php';
|
return require 'tests/data/Calculation/Calculation.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testUnknownFunction(): void
|
||||||
|
{
|
||||||
|
$workbook = new Spreadsheet();
|
||||||
|
$sheet = $workbook->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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ModeTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider providerMODE
|
||||||
|
*
|
||||||
|
* @param mixed $expectedResult
|
||||||
|
* @param string $str
|
||||||
|
*/
|
||||||
|
public function testMODE($expectedResult, string $str): void
|
||||||
|
{
|
||||||
|
$workbook = new Spreadsheet();
|
||||||
|
$sheet = $workbook->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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Color;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Conditional;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
|
||||||
|
class XlfnFunctionsTest extends \PHPUnit\Framework\TestCase
|
||||||
|
{
|
||||||
|
public function testXlfn(): void
|
||||||
|
{
|
||||||
|
$formulas = [
|
||||||
|
// null indicates function not implemented in Calculation engine
|
||||||
|
['2010', 'A1', '=MODE.SNGL({5.6,4,4,3,2,4})', '=_xlfn.MODE.SNGL({5.6,4,4,3,2,4})', 4],
|
||||||
|
['2010', 'A2', '=MODE.SNGL({"x","y"})', '=_xlfn.MODE.SNGL({"x","y"})', '#N/A'],
|
||||||
|
['2013', 'A1', '=ISOWEEKNUM("2019-12-19")', '=_xlfn.ISOWEEKNUM("2019-12-19")', 51],
|
||||||
|
['2013', 'A2', '=SHEET("2019")', '=_xlfn.SHEET("2019")', null],
|
||||||
|
['2013', 'A3', '2019-01-04', '2019-01-04', null],
|
||||||
|
['2013', 'A4', '2019-07-04', '2019-07-04', null],
|
||||||
|
['2013', 'A5', '2019-12-04', '2019-12-04', null],
|
||||||
|
['2013', 'B3', 1, 1, null],
|
||||||
|
['2013', 'B4', 2, 2, null],
|
||||||
|
['2013', 'B5', -3, -3, null],
|
||||||
|
// multiple xlfn functions interleaved with non-xlfn
|
||||||
|
['2013', 'C3', '=ISOWEEKNUM(A3)+WEEKNUM(A4)+ISOWEEKNUM(A5)', '=_xlfn.ISOWEEKNUM(A3)+WEEKNUM(A4)+_xlfn.ISOWEEKNUM(A5)', 77],
|
||||||
|
['2016', 'A1', '=SWITCH(WEEKDAY("2019-12-22",1),1,"Sunday",2,"Monday","No Match")', '=_xlfn.SWITCH(WEEKDAY("2019-12-22",1),1,"Sunday",2,"Monday","No Match")', 'Sunday'],
|
||||||
|
['2016', 'B1', '=SWITCH(WEEKDAY("2019-12-20",1),1,"Sunday",2,"Monday","No Match")', '=_xlfn.SWITCH(WEEKDAY("2019-12-20",1),1,"Sunday",2,"Monday","No Match")', 'No Match'],
|
||||||
|
['2019', 'A1', '=CONCAT("The"," ","sun"," ","will"," ","come"," ","up"," ","tomorrow.")', '=_xlfn.CONCAT("The"," ","sun"," ","will"," ","come"," ","up"," ","tomorrow.")', 'The sun will come up tomorrow.'],
|
||||||
|
];
|
||||||
|
$workbook = new Spreadsheet();
|
||||||
|
$sheet = $workbook->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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpOffice\PhpSpreadsheetTests\Functional;
|
||||||
|
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Conditional;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||||
|
|
||||||
|
class ConditionalTextTest extends AbstractFunctional
|
||||||
|
{
|
||||||
|
const COLOR_GREEN = 'FF99FF66';
|
||||||
|
const COLOR_RED = 'FFFF5050';
|
||||||
|
const COLOR_BLUE = 'FF5050FF';
|
||||||
|
const COLOR_YELLOW = 'FFFFFF50';
|
||||||
|
|
||||||
|
public function testConditionalText(): void
|
||||||
|
{
|
||||||
|
$format = 'Xlsx';
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
|
||||||
|
$conditionalStyles = [];
|
||||||
|
// if text contains 'anywhere' - red background
|
||||||
|
$condition0 = new Conditional();
|
||||||
|
$condition0->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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
[4.1, '{5.6,4.1,4.1,3,2,4.1}'], // Calculated value was #N/A
|
||||||
|
[4.1, '5.6,4.1,4.1,3,2,4.1'],
|
||||||
|
[3, '3,3,4,4'],
|
||||||
|
[4, '4,3,3,4'], // Calculated value was 3
|
||||||
|
['#N/A', '1,2,3,4'],
|
||||||
|
[2, '1,2,2,"3","3","3"'],
|
||||||
|
['#N/A', '"3","3","3"'],
|
||||||
|
];
|
Loading…
Reference in New Issue