diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b595933..6044ff12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## TBD - 2.2.0 +## TBD - 3.0.0 + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Moved + +- Nothing + +### Fixed + +- Add Sheet may leave Active Sheet uninitialized. [Issue #4112](https://github.com/PHPOffice/PhpSpreadsheet/issues/4112) [PR #4114](https://github.com/PHPOffice/PhpSpreadsheet/pull/4114) + +## 2024-07-24 - 2.2.0 ### Added - Xlsx Reader Optionally Ignore Rows With No Cells. [Issue #3982](https://github.com/PHPOffice/PhpSpreadsheet/issues/3982) [PR #4035](https://github.com/PHPOffice/PhpSpreadsheet/pull/4035) +- Means to change style without affecting current cell/sheet. [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073) - Option for CSV output file to have varying numbers of columns for each row. [Issue #1415](https://github.com/PHPOffice/PhpSpreadsheet/issues/1415) [PR #4076](https://github.com/PHPOffice/PhpSpreadsheet/pull/4076) ### Changed @@ -34,10 +57,19 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Mpdf and Tcpdf Borders on Merged Cells. [Issue #3557](https://github.com/PHPOffice/PhpSpreadsheet/issues/3557) [PR #4047](https://github.com/PHPOffice/PhpSpreadsheet/pull/4047) - Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033) - Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042) +- Ods comments with newlines. [Issue #4081](https://github.com/PHPOffice/PhpSpreadsheet/issues/4081) [PR #4086](https://github.com/PHPOffice/PhpSpreadsheet/pull/4086) +- Propagate errors in Text functions. [Issue #2581](https://github.com/PHPOffice/PhpSpreadsheet/issues/2581) [PR #4080](https://github.com/PHPOffice/PhpSpreadsheet/pull/4080) - Csv Reader allow use of html mimetype. [Issue #4036](https://github.com/PHPOffice/PhpSpreadsheet/issues/4036) [PR #4040](https://github.com/PHPOffice/PhpSpreadsheet/pull/4040) - Problem rendering line chart with missing plot label. [PR #4074](https://github.com/PHPOffice/PhpSpreadsheet/pull/4074) - More RTL in Xlsx/Html Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4065](https://github.com/PHPOffice/PhpSpreadsheet/pull/4065) - Empty String in sharedStrings. [Issue #4063](https://github.com/PHPOffice/PhpSpreadsheet/issues/4063) [PR #4064](https://github.com/PHPOffice/PhpSpreadsheet/pull/4064) +- Xlsx Writer RichText and TYPE_STRING. [Issue #476](https://github.com/PHPOffice/PhpSpreadsheet/issues/476) [PR #4094](https://github.com/PHPOffice/PhpSpreadsheet/pull/4094) +- Ods boolean data. [Issue #460](https://github.com/PHPOffice/PhpSpreadsheet/issues/460) [PR #4093](https://github.com/PHPOffice/PhpSpreadsheet/pull/4093) +- Html Writer Minor Fixes. [PR #4089](https://github.com/PHPOffice/PhpSpreadsheet/pull/4089) +- Changes to INDEX function. [Issue #64](https://github.com/PHPOffice/PhpSpreadsheet/issues/64) [PR #4088](https://github.com/PHPOffice/PhpSpreadsheet/pull/4088) +- Ods Reader and Whitespace Text Nodes. [Issue #804](https://github.com/PHPOffice/PhpSpreadsheet/issues/804) [PR #4087](https://github.com/PHPOffice/PhpSpreadsheet/pull/4087) +- Ods Xml Reader and Whitespace Text Nodes. [Issue #804](https://github.com/PHPOffice/PhpSpreadsheet/issues/804) [PR #4087](https://github.com/PHPOffice/PhpSpreadsheet/pull/4087) +- Treat invalid formulas as strings. [Issue #1310](https://github.com/PHPOffice/PhpSpreadsheet/issues/1310) [PR #4073](https://github.com/PHPOffice/PhpSpreadsheet/pull/4073) ## 2024-05-11 - 2.1.0 diff --git a/docs/index.md b/docs/index.md index 9505181b7..d577d8982 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,9 @@ allow you to read and write various spreadsheet file formats such as Excel and L |CSV | ✓ | ✓ | |PDF (using either the TCPDF, Dompdf or mPDF libraries, which need to be installed separately)| | ✓ | +Note - reading or writing certain aspects of a spreadsheet may not be supported in all formats. For more details, please consult +[Features Cross-reference](./references/features-cross-reference.md). + # Getting started ## Software requirements diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 37668ba82..746357894 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -28,7 +28,7 @@ ✔ ✔ ● - ● + ● 6 ✔ N/A N/A @@ -389,7 +389,7 @@ Conditional Formatting - ● + ✔ ✔ ✖ ✖ @@ -414,7 +414,7 @@ ✔ ✔ ✔ - ✖ + ✔ ✔ N/A N/A @@ -495,14 +495,14 @@ ✔ N/A ● - ● + ● 7 Number Format Mask ✔ ✔ ✔ - ✔ + ● ✔ N/A ✔ @@ -517,7 +517,7 @@ ✔ N/A ✖ - ● + ● 7 Horizontal @@ -583,7 +583,7 @@ ✔ N/A ✔ - ✔ + ● 7 Patterned @@ -605,7 +605,7 @@ ✔ N/A ✔ - ✔ + ● 7 Font Face @@ -704,7 +704,7 @@ ✔ N/A ● - ✔ + ● 7 Line Style @@ -797,13 +797,13 @@ Alignment ✖ 3 - ✖ + ✔ ✖ ✖ ✖ N/A ✖ - ✖ + ✔ Background Image @@ -929,7 +929,7 @@ Macros ✖ - ✔ + ● 5 ✖ ✖ ✖ @@ -940,7 +940,7 @@ Form Controls ✖ - ✖ + ● 4 ✖ ✖ ✖ @@ -1001,6 +1001,10 @@ 1. Only text contents 2. Only BIFF8 files support Rich Text. Prior to that, comments could only be plain text 3. Only BIFF8 files support alignment and rotation. Prior to that, comments could only be unformatted text +4. Xlsx forms and controls can be read and written but not otherwise manipulated +5. Xlsx macros can be read and written; their values can be retrieved and changed, but only in a binary form which is unlikely to be useful +6. There is very limited support for reading styles from an Ods spreadsheet. Writing styles has better support, although Number Format is incomplete. +7. In most cases, Html reader processes only inline styles; styles provided by Css classes may be ignored. ## Writers @@ -1184,7 +1188,7 @@ Row Height/Column Width ✔ ✔ - ✖ + ✔ N/A ✔ ✔ @@ -1256,7 +1260,7 @@ Number Format Mask ✔ ✔ - ✔ + ● N/A ✔ ✔ @@ -1472,10 +1476,10 @@ Merged Cells ✔ ✔ - ✖ + ✔ N/A ✔ - ✖ + ✔ Cell Comments @@ -1498,10 +1502,10 @@ Alignment ✖ - ✖ + ✔ ✖ N/A - ✖ + ✔ N/A @@ -1606,7 +1610,7 @@ Macros ✖ - ✔ + ● 5 ✖ N/A ✖ @@ -1615,7 +1619,7 @@ Form Controls ✖ - ✖ + ● 4 ✖ N/A ✖ @@ -1803,8 +1807,8 @@ Macros - $spreadsheet->getMacrosCode(); - $spreadsheet->setMacrosCode(); + $spreadsheet->getMacrosCode();5 + $spreadsheet->setMacrosCode();5 Security diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2f13a1634..d372005a2 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -4065,7 +4065,7 @@ class Calculation $opCharacter = $formula[$index]; // Get the first character of the value at the current index position // Check for two-character operators (e.g. >=, <=, <>) - if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) { + if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && isset($formula[$index + 1], self::$comparisonOperators[$formula[$index + 1]])) { $opCharacter .= $formula[++$index]; } // Find out if we're currently at the beginning of a number, variable, cell/row/column reference, @@ -4792,13 +4792,20 @@ class Calculation for ($row = 0; $row < $rows; ++$row) { for ($column = 0; $column < $columns; ++$column) { - $operand1[$row][$column] - = Shared\StringHelper::substring( - self::boolToString($operand1[$row][$column]) - . self::boolToString($operand2[$row][$column]), - 0, - DataType::MAX_STRING_LENGTH - ); + $op1x = self::boolToString($operand1[$row][$column]); + $op2x = self::boolToString($operand2[$row][$column]); + if (Information\ErrorValue::isError($op1x)) { + // no need to do anything + } elseif (Information\ErrorValue::isError($op2x)) { + $operand1[$row][$column] = $op2x; + } else { + $operand1[$row][$column] + = Shared\StringHelper::substring( + $op1x . $op2x, + 0, + DataType::MAX_STRING_LENGTH + ); + } } } $result = $operand1; @@ -4808,7 +4815,13 @@ class Calculation // using the concatenation operator // with literals that fits in 32K, // so I don't think we can overflow here. - $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; + if (Information\ErrorValue::isError($operand1)) { + $result = $operand1; + } elseif (Information\ErrorValue::isError($operand2)) { + $result = $operand2; + } else { + $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; + } } $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); $stack->push('Value', $result); @@ -5074,9 +5087,10 @@ class Calculation if ($cell === null || $pCellWorksheet === null) { return $this->raiseFormulaError("undefined name '$token'"); } + $specifiedWorksheet = trim($matches[2], "'"); $this->debugLog->writeDebugLog('Evaluating Defined Name %s', $definedName); - $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet); + $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet, $specifiedWorksheet); // If not Defined Name, try as Table. if ($namedRange === null && $this->spreadsheet !== null) { $table = $this->spreadsheet->getTableByName($definedName); @@ -5101,7 +5115,7 @@ class Calculation return $this->raiseFormulaError("undefined name '$definedName'"); } - $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack); + $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== ''); if (isset($storeKey)) { $branchStore[$storeKey] = $result; } @@ -5580,10 +5594,10 @@ class Calculation return $args; } - private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksheet $cellWorksheet, Stack $stack): mixed + private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksheet $cellWorksheet, Stack $stack, bool $ignoreScope = false): mixed { $definedNameScope = $namedRange->getScope(); - if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet) { + if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet && !$ignoreScope) { // The defined name isn't in our current scope, so #REF $result = ExcelError::REF(); $stack->push('Error', $result, $namedRange->getName()); diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 6b7450069..77f8317ae 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -26,6 +26,8 @@ class Functions const RETURNDATE_PHP_DATETIME_OBJECT = 'O'; const RETURNDATE_EXCEL = 'E'; + public const NOT_YET_IMPLEMENTED = '#Not Yet Implemented'; + /** * Compatibility mode to use for error checking and responses. */ @@ -123,7 +125,7 @@ class Functions */ public static function DUMMY(): string { - return '#Not Yet Implemented'; + return self::NOT_YET_IMPLEMENTED; } public static function isMatrixValue(mixed $idx): bool diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index dcef43990..f3a746273 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Information; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; class ErrorValue { @@ -35,7 +36,7 @@ class ErrorValue * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array * with the same dimensions */ - public static function isError(mixed $value = ''): array|bool + public static function isError(mixed $value = '', bool $tryNotImplemented = false): array|bool { if (is_array($value)) { return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); @@ -44,6 +45,9 @@ class ErrorValue if (!is_string($value)) { return false; } + if ($tryNotImplemented && $value === Functions::NOT_YET_IMPLEMENTED) { + return true; + } return in_array($value, ExcelError::ERROR_CODES, true); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index d578854de..228b46448 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -81,7 +81,6 @@ class Matrix } $rowNum = $rowNum ?? 0; - $originalColumnNum = $columnNum; $columnNum = $columnNum ?? 0; try { @@ -91,6 +90,17 @@ class Matrix return $e->getMessage(); } + if (is_array($matrix) && count($matrix) === 1 && $rowNum > 1) { + $matrixKey = array_keys($matrix)[0]; + if (is_array($matrix[$matrixKey])) { + $tempMatrix = []; + foreach ($matrix[$matrixKey] as $key => $value) { + $tempMatrix[$key] = [$value]; + } + $matrix = $tempMatrix; + } + } + if (!is_array($matrix) || ($rowNum > count($matrix))) { return ExcelError::REF(); } @@ -101,9 +111,6 @@ class Matrix if ($columnNum > count($columnKeys)) { return ExcelError::REF(); } - if ($originalColumnNum === null && 1 < count($columnKeys)) { - return ExcelError::REF(); - } if ($columnNum === 0) { return self::extractRowValue($matrix, $rowKeys, $rowNum); diff --git a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php index 83cc4ee1f..6667bac54 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class CaseConvert @@ -26,7 +27,11 @@ class CaseConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); } - $mixedCaseValue = Helpers::extractString($mixedCaseValue); + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return StringHelper::strToLower($mixedCaseValue); } @@ -48,7 +53,11 @@ class CaseConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); } - $mixedCaseValue = Helpers::extractString($mixedCaseValue); + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return StringHelper::strToUpper($mixedCaseValue); } @@ -70,7 +79,11 @@ class CaseConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); } - $mixedCaseValue = Helpers::extractString($mixedCaseValue); + try { + $mixedCaseValue = Helpers::extractString($mixedCaseValue, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return StringHelper::strToTitle($mixedCaseValue); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php index 8d90a17ba..06d0f9009 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; @@ -26,7 +27,12 @@ class CharacterConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character); } - $character = Helpers::validateInt($character); + try { + $character = Helpers::validateInt($character, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1; if ($character < $min || $character > 255) { return ExcelError::VALUE(); @@ -52,7 +58,12 @@ class CharacterConvert return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters); } - $characters = Helpers::extractString($characters); + try { + $characters = Helpers::extractString($characters, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + if ($characters === '') { return ExcelError::VALUE(); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php index c2281d436..78940ed16 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -27,7 +27,7 @@ class Concatenate foreach ($aArgs as $arg) { $value = Helpers::extractString($arg); - if (ErrorValue::isError($value)) { + if (ErrorValue::isError($value, true)) { $returnValue = $value; break; @@ -85,7 +85,7 @@ class Concatenate { foreach ($aArgs as $key => &$arg) { $value = Helpers::extractString($arg); - if (ErrorValue::isError($value)) { + if (ErrorValue::isError($value, true)) { return $value; } @@ -123,7 +123,7 @@ class Concatenate if (!is_numeric($repeatCount) || $repeatCount < 0) { $returnValue = ExcelError::VALUE(); - } elseif (ErrorValue::isError($stringValue)) { + } elseif (ErrorValue::isError($stringValue, true)) { $returnValue = $stringValue; } else { $returnValue = str_repeat($stringValue, (int) $repeatCount); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 32e5b967a..1dfb724cd 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -31,7 +31,7 @@ class Extract } try { - $value = Helpers::extractString($value); + $value = Helpers::extractString($value, true); $chars = Helpers::extractInt($chars, 0, 1); } catch (CalcExp $e) { return $e->getMessage(); @@ -61,7 +61,7 @@ class Extract } try { - $value = Helpers::extractString($value); + $value = Helpers::extractString($value, true); $start = Helpers::extractInt($start, 1); $chars = Helpers::extractInt($chars, 0); } catch (CalcExp $e) { @@ -90,7 +90,7 @@ class Extract } try { - $value = Helpers::extractString($value); + $value = Helpers::extractString($value, true); $chars = Helpers::extractInt($chars, 0, 1); } catch (CalcExp $e) { return $e->getMessage(); @@ -132,7 +132,13 @@ class Extract return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); } - $text = Helpers::extractString($text ?? ''); + try { + $text = Helpers::extractString($text ?? '', true); + Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; @@ -190,7 +196,13 @@ class Extract return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); } - $text = Helpers::extractString($text ?? ''); + try { + $text = Helpers::extractString($text ?? '', true); + Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $instance = (int) $instance; $matchMode = (int) $matchMode; $matchEnd = (int) $matchEnd; diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index 40335ced7..0560b376b 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -123,8 +124,13 @@ class Format return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); } - $value = Helpers::extractString($value); - $format = Helpers::extractString($format); + try { + $value = Helpers::extractString($value, true); + $format = Helpers::extractString($format, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } + $format = (string) NumberFormat::convertSystemFormats($format); if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) { @@ -152,6 +158,9 @@ class Format } if (is_string($value)) { $value = trim($value); + if (ErrorValue::isError($value, true)) { + throw new CalcExp($value); + } if ($spacesMeanZero && $value === '') { $value = 0; } @@ -220,7 +229,7 @@ class Format } /** - * TEXT. + * VALUETOTEXT. * * @param mixed $value The value to format * Or can be an array of values diff --git a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php index 15b046704..719de04a8 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -27,7 +27,7 @@ class Helpers if (is_bool($value)) { return self::convertBooleanValue($value); } - if ($throwIfError && is_string($value) && ErrorValue::isError($value)) { + if ($throwIfError && is_string($value) && ErrorValue::isError($value, true)) { throw new CalcExp($value); } @@ -63,18 +63,28 @@ class Helpers $value = (float) $value; } if (!is_numeric($value)) { + if (is_string($value) && ErrorValue::isError($value, true)) { + throw new CalcExp($value); + } + throw new CalcExp(ExcelError::VALUE()); } return (float) $value; } - public static function validateInt(mixed $value): int + public static function validateInt(mixed $value, bool $throwIfError = false): int { if ($value === null) { $value = 0; } elseif (is_bool($value)) { $value = (int) $value; + } elseif ($throwIfError && is_string($value) && !is_numeric($value)) { + if (!ErrorValue::isError($value, true)) { + $value = ExcelError::VALUE(); + } + + throw new CalcExp($value); } return (int) $value; diff --git a/src/PhpSpreadsheet/Calculation/TextData/Search.php b/src/PhpSpreadsheet/Calculation/TextData/Search.php index ad83f1a37..663d49fc2 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Search.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -32,8 +32,8 @@ class Search } try { - $needle = Helpers::extractString($needle); - $haystack = Helpers::extractString($haystack); + $needle = Helpers::extractString($needle, true); + $haystack = Helpers::extractString($haystack, true); $offset = Helpers::extractInt($offset, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); @@ -74,8 +74,8 @@ class Search } try { - $needle = Helpers::extractString($needle); - $haystack = Helpers::extractString($haystack); + $needle = Helpers::extractString($needle, true); + $haystack = Helpers::extractString($haystack, true); $offset = Helpers::extractInt($offset, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 44e0cd402..f988a6c19 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; @@ -17,16 +18,20 @@ class Text * @param mixed $value String Value * Or can be an array of values * - * @return array|int If an array of values is passed for the argument, then the returned result + * @return array|int|string If an array of values is passed for the argument, then the returned result * will also be an array with matching dimensions */ - public static function length(mixed $value = ''): array|int + public static function length(mixed $value = ''): array|int|string { if (is_array($value)) { return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); } - $value = Helpers::extractString($value); + try { + $value = Helpers::extractString($value, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return mb_strlen($value, 'UTF-8'); } @@ -41,17 +46,21 @@ class Text * @param mixed $value2 String Value * Or can be an array of values * - * @return array|bool If an array of values is passed for either of the arguments, then the returned result + * @return array|bool|string If an array of values is passed for either of the arguments, then the returned result * will also be an array with matching dimensions */ - public static function exact(mixed $value1, mixed $value2): array|bool + public static function exact(mixed $value1, mixed $value2): array|bool|string { if (is_array($value1) || is_array($value2)) { return self::evaluateArrayArguments([self::class, __FUNCTION__], $value1, $value2); } - $value1 = Helpers::extractString($value1); - $value2 = Helpers::extractString($value2); + try { + $value1 = Helpers::extractString($value1, true); + $value2 = Helpers::extractString($value2, true); + } catch (CalcExp $e) { + return $e->getMessage(); + } return $value2 === $value1; } @@ -97,11 +106,14 @@ class Text * @param mixed $padding The value with which to pad the result. * The default is #N/A. * - * @return array the array built from the text, split by the row and column delimiters + * @return array|string the array built from the text, split by the row and column delimiters, or an error string */ - public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array + public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array|string { $text = Functions::flattenSingleValue($text); + if (ErrorValue::isError($text, true)) { + return $text; + } $flags = self::matchFlags($matchMode); diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 0f0075bca..6aed63f20 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; @@ -255,6 +256,7 @@ class Cell implements Stringable public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self { $oldValue = $this->value; + $quotePrefix = false; // set the value according to data type switch ($dataType) { @@ -267,6 +269,10 @@ class Cell implements Stringable // no break case DataType::TYPE_STRING: // Synonym for string + if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { + $quotePrefix = true; + } + // no break case DataType::TYPE_INLINE: // Rich text if ($value !== null && !is_scalar($value) && !($value instanceof Stringable)) { @@ -312,6 +318,7 @@ class Cell implements Stringable $this->updateInCollection(); $cellCoordinate = $this->getCoordinate(); self::updateIfCellIsTableHeader($this->getParent()?->getParent(), $this, $oldValue, $value); + $this->getWorksheet()->applyStylesFromArray($cellCoordinate, ['quotePrefix' => $quotePrefix]); return $this->getParent()?->get($cellCoordinate) ?? $this; } @@ -409,7 +416,7 @@ class Cell implements Stringable } SharedDate::setExcelCalendar($currentCalendar); - if ($result === '#Not Yet Implemented') { + if ($result === Functions::NOT_YET_IMPLEMENTED) { return $this->calculatedValue; // Fallback if calculation engine does not support the formula. } diff --git a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php index 6f75a90a8..f36934ed3 100644 --- a/src/PhpSpreadsheet/Cell/DefaultValueBinder.php +++ b/src/PhpSpreadsheet/Cell/DefaultValueBinder.php @@ -3,6 +3,8 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use DateTimeInterface; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -68,6 +70,23 @@ class DefaultValueBinder implements IValueBinder throw new SpreadsheetException("unusable type $gettype"); } if (strlen($value) > 1 && $value[0] === '=') { + $calculation = new Calculation(); + $calculation->disableBranchPruning(); + + try { + if (empty($calculation->parseFormula($value))) { + return DataType::TYPE_STRING; + } + } catch (CalculationException $e) { + $message = $e->getMessage(); + if ( + $message === 'Formula Error: An unexpected error occurred' + || str_contains($message, 'has no operands') + ) { + return DataType::TYPE_STRING; + } + } + return DataType::TYPE_FORMULA; } if (preg_match('/^[\+\-]?(\d+\\.?\d*|\d*\\.?\d+)([Ee][\-\+]?[0-2]?\d{1,3})?$/', $value)) { diff --git a/src/PhpSpreadsheet/Cell/StringValueBinder.php b/src/PhpSpreadsheet/Cell/StringValueBinder.php index 6ff258d93..d86cdabd3 100644 --- a/src/PhpSpreadsheet/Cell/StringValueBinder.php +++ b/src/PhpSpreadsheet/Cell/StringValueBinder.php @@ -8,7 +8,7 @@ use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use Stringable; -class StringValueBinder implements IValueBinder +class StringValueBinder extends DefaultValueBinder implements IValueBinder { protected bool $convertNull = true; @@ -87,12 +87,9 @@ class StringValueBinder implements IValueBinder $cell->setValueExplicit($value, DataType::TYPE_BOOL); } elseif ((is_int($value) || is_float($value)) && $this->convertNumeric === false) { $cell->setValueExplicit($value, DataType::TYPE_NUMERIC); - } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false) { + } elseif (is_string($value) && strlen($value) > 1 && $value[0] === '=' && $this->convertFormula === false && parent::dataTypeForValue($value) === DataType::TYPE_FORMULA) { $cell->setValueExplicit($value, DataType::TYPE_FORMULA); } else { - if (is_string($value) && strlen($value) > 1 && $value[0] === '=') { - $cell->getStyle()->setQuotePrefix(true); - } $cell->setValueExplicit((string) $value, DataType::TYPE_STRING); } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index ceb345dc3..6f130271f 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -6,6 +6,7 @@ use DOMAttr; use DOMDocument; use DOMElement; use DOMNode; +use DOMText; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Helper\Dimension as HelperDimension; @@ -403,8 +404,11 @@ class Ods extends BaseReader } $columnID = 'A'; - /** @var DOMElement $cellData */ + /** @var DOMElement|DOMText $cellData */ foreach ($childNode->childNodes as $cellData) { + if ($cellData instanceof DOMText) { + continue; // should just be whitespace + } if ($this->getReadFilter() !== null) { if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { @@ -436,14 +440,25 @@ class Ods extends BaseReader if ($annotation->length > 0 && $annotation->item(0) !== null) { $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p'); + $textNodeLength = $textNode->length; + $newLineOwed = false; + for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) { + $textNodeItem = $textNode->item($textNodeIndex); + if ($textNodeItem !== null) { + $text = $this->scanElementForText($textNodeItem); + if ($newLineOwed) { + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText("\n"); + } + $newLineOwed = true; - if ($textNode->length > 0 && $textNode->item(0) !== null) { - $text = $this->scanElementForText($textNode->item(0)); - - $spreadsheet->getActiveSheet() - ->getComment($columnID . $rowID) - ->setText($this->parseRichText($text)); -// ->setAuthor( $author ) + $spreadsheet->getActiveSheet() + ->getComment($columnID . $rowID) + ->getText() + ->createText($this->parseRichText($text)); + } } } @@ -492,7 +507,7 @@ class Ods extends BaseReader break; case 'boolean': $type = DataType::TYPE_BOOL; - $dataValue = ($allCellDataText == 'TRUE') ? true : false; + $dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false; break; case 'percentage': @@ -731,6 +746,8 @@ class Ods extends BaseReader /** @var DOMNode $child */ if ($child->nodeType == XML_TEXT_NODE) { $str .= $child->nodeValue; + } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:line-break') { + $str .= "\n"; } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') { // It's a space diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php index 136b92fed..9d71443f4 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -122,11 +122,12 @@ class SheetViewOptions extends BaseParserClass private function printOptions(SimpleXMLElement $printOptionsx): void { $printOptions = $printOptionsx->attributes() ?? []; - if (isset($printOptions['gridLinesSet']) && self::boolean((string) $printOptions['gridLinesSet'])) { - $this->worksheet->setShowGridlines(true); - } + // Spec is weird. gridLines (default false) + // and gridLinesSet (default true) must both be true. if (isset($printOptions['gridLines']) && self::boolean((string) $printOptions['gridLines'])) { - $this->worksheet->setPrintGridlines(true); + if (!isset($printOptions['gridLinesSet']) || self::boolean((string) $printOptions['gridLinesSet'])) { + $this->worksheet->setPrintGridlines(true); + } } if (isset($printOptions['horizontalCentered']) && self::boolean((string) $printOptions['horizontalCentered'])) { $this->worksheet->getPageSetup()->setHorizontalCentered(true); diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index e571cc4f6..bcea8e6a7 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -558,6 +558,9 @@ class Spreadsheet implements JsonSerializable if ($this->activeSheetIndex >= $sheetIndex) { ++$this->activeSheetIndex; } + if ($this->activeSheetIndex < 0) { + $this->activeSheetIndex = 0; + } } if ($worksheet->getParent() === null) { diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 8adada4c1..7afa82b5d 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3673,4 +3673,21 @@ class Worksheet implements IComparable } } } + + public function applyStylesFromArray(string $coordinate, array $styleArray): bool + { + $spreadsheet = $this->parent; + if ($spreadsheet === null) { + return false; + } + $activeSheetIndex = $spreadsheet->getActiveSheetIndex(); + $originalSelected = $this->selectedCells; + $this->getStyle($coordinate)->applyFromArray($styleArray); + $this->selectedCells = $originalSelected; + if ($activeSheetIndex >= 0) { + $spreadsheet->setActiveSheetIndex($activeSheetIndex); + } + + return true; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 457af6612..37a9c8994 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -355,7 +355,11 @@ class Html extends BaseWriter $html .= ' ' . PHP_EOL; $html .= ' ' . PHP_EOL; $html .= ' ' . PHP_EOL; - $html .= ' ' . htmlspecialchars($properties->getTitle(), Settings::htmlEntityFlags()) . '' . PHP_EOL; + $title = $properties->getTitle(); + if ($title === '') { + $title = $this->spreadsheet->getActiveSheet()->getTitle(); + } + $html .= ' ' . htmlspecialchars($title, Settings::htmlEntityFlags()) . '' . PHP_EOL; $html .= self::generateMeta($properties->getCreator(), 'author'); $html .= self::generateMeta($properties->getTitle(), 'title'); $html .= self::generateMeta($properties->getDescription(), 'description'); @@ -1462,11 +1466,21 @@ class Html extends BaseWriter $xcssClass['height'] = $height; } //** end of redundant code ** + if ($this->useInlineCss) { + foreach (['border-top', 'border-bottom', 'border-right', 'border-left'] as $borderType) { + if (($xcssClass[$borderType] ?? '') === 'none #000000') { + unset($xcssClass[$borderType]); + } + } + } if ($htmlx) { $xcssClass['position'] = 'relative'; } $html .= ' style="' . $this->assembleCSS($xcssClass) . '"'; + if ($this->useInlineCss) { + $html .= ' class="gridlines gridlinesp"'; + } } $html = $this->generateRowSpans($html, $rowSpan, $colSpan); diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php index b0829bf1d..f0b7d5704 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Comment.php @@ -24,7 +24,22 @@ class Comment $objWriter->writeAttribute('svg:x', $comment->getMarginLeft()); $objWriter->writeAttribute('svg:y', $comment->getMarginTop()); $objWriter->writeElement('dc:creator', $comment->getAuthor()); - $objWriter->writeElement('text:p', $comment->getText()->getPlainText()); + + $objWriter->startElement('text:p'); + $text = $comment->getText()->getPlainText(); + $textElements = explode("\n", $text); + $newLineOwed = false; + foreach ($textElements as $textSegment) { + if ($newLineOwed) { + $objWriter->writeElement('text:line-break'); + } + $newLineOwed = true; + if ($textSegment !== '') { + $objWriter->writeElement('text:span', $textSegment); + } + } + $objWriter->endElement(); // text:p + $objWriter->endElement(); } } diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 7b052bbcc..7ffcd46b6 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; @@ -209,8 +210,8 @@ class Content extends WriterPart switch ($cell->getDataType()) { case DataType::TYPE_BOOL: $objWriter->writeAttribute('office:value-type', 'boolean'); - $objWriter->writeAttribute('office:value', $cell->getValueString()); - $objWriter->writeElement('text:p', $cell->getValueString()); + $objWriter->writeAttribute('office:boolean-value', $cell->getValue() ? 'true' : 'false'); + $objWriter->writeElement('text:p', Calculation::getInstance()->getLocaleBoolean($cell->getValue() ? 'TRUE' : 'FALSE')); break; case DataType::TYPE_ERROR: diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index a1940f329..bd6eec367 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1528,7 +1528,7 @@ class Worksheet extends WriterPart break; case 's': // String - $this->writeCellString($objWriter, $mappedType, $cellValueString, $flippedStringTable); + $this->writeCellString($objWriter, $mappedType, ($cellValue instanceof RichText) ? $cellValue : $cellValueString, $flippedStringTable); break; case 'f': // Formula diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php index f961c7ac7..978162667 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationFunctionListTest.php @@ -13,21 +13,15 @@ class CalculationFunctionListTest extends TestCase { private string $compatibilityMode; - private string $locale; - protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); - $calculation = Calculation::getInstance(); - $this->locale = $calculation->getLocale(); Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); - $calculation = Calculation::getInstance(); - $calculation->setLocale($this->locale); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 79685d267..797b0bfd7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -15,21 +15,15 @@ class CalculationTest extends TestCase { private string $compatibilityMode; - private string $locale; - protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); - $calculation = Calculation::getInstance(); - $this->locale = $calculation->getLocale(); Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); - $calculation = Calculation::getInstance(); - $calculation->setLocale($this->locale); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php index fbac0ff91..aa7bc529d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Engine/RangeTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace PhpOffice\PhpSpreadsheetTests\Calculation\Engine; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -13,13 +14,23 @@ class RangeTest extends TestCase { private string $incompleteMessage = 'Must be revisited'; - private Spreadsheet $spreadSheet; + private ?Spreadsheet $spreadSheet = null; - protected function setUp(): void + protected function getSpreadsheet(): Spreadsheet { - $this->spreadSheet = new Spreadsheet(); - $this->spreadSheet->getActiveSheet() + $spreadsheet = new Spreadsheet(); + $spreadsheet->getActiveSheet() ->fromArray(array_chunk(range(1, 240), 6), null, 'A1', true); + + return $spreadsheet; + } + + protected function tearDown(): void + { + if ($this->spreadSheet !== null) { + $this->spreadSheet->disconnectWorksheets(); + $this->spreadSheet = null; + } } /** @@ -27,6 +38,7 @@ class RangeTest extends TestCase */ public function testRangeEvaluation(string $formula, int|string $expectedResult): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); $workSheet->setCellValue('H1', $formula); @@ -64,8 +76,20 @@ class RangeTest extends TestCase ]; } + public function test3dRangeParsing(): void + { + // This test shows that parsing throws exception. + // Next test shows that formula is still treated as a formula + // despite the parse failure. + $this->expectExceptionMessage('3D Range references are not yet supported'); + $calculation = new Calculation(); + $calculation->disableBranchPruning(); + $calculation->parseFormula('=SUM(Worksheet!A1:Worksheet2!B3'); + } + public function test3dRangeEvaluation(): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); $workSheet->setCellValue('E1', '=SUM(Worksheet!A1:Worksheet2!B3)'); @@ -78,6 +102,7 @@ class RangeTest extends TestCase */ public function testNamedRangeEvaluation(array $ranges, string $formula, int $expectedResult): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); foreach ($ranges as $id => $range) { $this->spreadSheet->addNamedRange(new NamedRange('GROUP' . ++$id, $workSheet, $range)); @@ -116,6 +141,7 @@ class RangeTest extends TestCase */ public function testUTF8NamedRangeEvaluation(array $names, array $ranges, string $formula, int $expectedResult): void { + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); foreach ($names as $index => $name) { $range = $ranges[$index]; @@ -144,6 +170,7 @@ class RangeTest extends TestCase if ($this->incompleteMessage !== '') { self::markTestIncomplete($this->incompleteMessage); } + $this->spreadSheet = $this->getSpreadsheet(); $workSheet = $this->spreadSheet->getActiveSheet(); $this->spreadSheet->addNamedRange(new NamedRange('COMPOSITE', $workSheet, $composite)); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php index e893fc7f3..3f508405a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php @@ -34,4 +34,37 @@ class IndexOnSpreadsheetTest extends AllSetupTeardown { return require 'tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php'; } + + /** + * @dataProvider providerIndexLiteralArrays + */ + public function testLiteralArrays(mixed $expectedResult, string $indexArgs): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A10')->setValue(10); + $sheet->getCell('B10')->setValue(11); + $sheet->getCell('C10')->setValue(12); + $sheet->getCell('D10')->setValue(13); + $sheet->getCell('X10')->setValue(10); + $sheet->getCell('X11')->setValue(11); + $sheet->getCell('X12')->setValue(12); + $sheet->getCell('X13')->setValue(13); + $sheet->getCell('A1')->setValue("=INDEX($indexArgs)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public static function providerIndexLiteralArrays(): array + { + return [ + 'issue 64' => ['Fourth', '{"First","Second","Third","Fourth","Fifth","Sixth","Seventh"}, 4'], + 'issue 64 selecting first "row"' => ['First', '{"First","Second","Third","Fourth","Fifth","Sixth","Seventh"}, 1'], + 'array result condensed to single value' => [40, '{10,11;20,21;30,31;40,41;50,51;60,61},4'], + 'both row and column' => [41, '{10,11;20,21;30,31;40,41;50,51;60,61},4,2'], + '1*1 array' => ['first', '{"first"},1'], + 'array expressed in rows' => [20, '{10;20;30;40},2'], + 'spreadsheet single row' => [11, 'A10:D10,2'], + 'spreadsheet single column' => [13, 'X10:X13,4'], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php new file mode 100644 index 000000000..9ab19d94c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php @@ -0,0 +1,96 @@ +getSheet(); + $sheet->getCell('A1')->setValue('=ABS("X")'); + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + $sheet->getCell('A2')->setValue('=SQRT(-1)'); + self::assertSame('#NUM!', $sheet->getCell('A2')->getCalculatedValue()); + $sheet->getCell('A3')->setValue('=3/0'); + self::assertSame('#DIV/0!', $sheet->getCell('A3')->getCalculatedValue()); + $sheet->getCell('A4')->setValue('=XXXX()'); + self::assertSame('#NAME?', $sheet->getCell('A4')->getCalculatedValue()); + $sheet->getCell('A5')->setValue('=ABS("X")'); + self::assertSame('#VALUE!', $sheet->getCell('A5')->getCalculatedValue()); + + $sheet->getCell('B1')->setValue('=UPPER(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + $sheet->getCell('B2')->setValue('=LOWER(A2)'); + self::assertSame('#NUM!', $sheet->getCell('A2')->getCalculatedValue()); + $sheet->getCell('B3')->setValue('=PROPER(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('A3')->getCalculatedValue()); + + $sheet->getCell('C2')->setValue('=CHAR(A2)'); + self::assertSame('#NUM!', $sheet->getCell('C2')->getCalculatedValue()); + $sheet->getCell('C3')->setValue('=CODE(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('C3')->getCalculatedValue()); + + $sheet->getCell('D1')->setValue('=CONCATENATE(A1,A1)'); + self::assertSame('#VALUE!', $sheet->getCell('D1')->getCalculatedValue()); + $sheet->getCell('D2')->setValue('=TEXTJOIN(",",TRUE,A2,A3)'); + self::assertSame('#NUM!', $sheet->getCell('D2')->getCalculatedValue()); + $sheet->getCell('D3')->setValue('=REPT(A3,3)'); + self::assertSame('#DIV/0!', $sheet->getCell('D3')->getCalculatedValue()); + $sheet->getCell('D4')->setValue('=CONCAT(A4,A4)'); + self::assertSame('#NAME?', $sheet->getCell('D4')->getCalculatedValue()); + $sheet->getCell('D5')->setValue('="X"&A4'); + self::assertSame('#NAME?', $sheet->getCell('D5')->getCalculatedValue()); + $sheet->getCell('D6')->setValue('=A2&"X"'); + self::assertSame('#NUM!', $sheet->getCell('D6')->getCalculatedValue()); + + $sheet->getCell('E1')->setValue('=LEFT(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('E1')->getCalculatedValue()); + $sheet->getCell('E2')->setValue('=RIGHT(A2)'); + self::assertSame('#NUM!', $sheet->getCell('E2')->getCalculatedValue()); + $sheet->getCell('E3')->setValue('=MID(A3,2,2)'); + self::assertSame('#DIV/0!', $sheet->getCell('E3')->getCalculatedValue()); + $sheet->getCell('E4')->setValue('=TEXTBEFORE(A4,"M")'); + self::assertSame('#NAME?', $sheet->getCell('E4')->getCalculatedValue()); + $sheet->getCell('E5')->setValue('=TEXTAFTER(A5,"U")'); + self::assertSame('#VALUE!', $sheet->getCell('E5')->getCalculatedValue()); + + $sheet->getCell('F1')->setValue('=VALUETOTEXT(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('F1')->getCalculatedValue()); + $sheet->getCell('F2')->setValue('=DOLLAR(A2)'); + self::assertSame('#NUM!', $sheet->getCell('F2')->getCalculatedValue()); + $sheet->getCell('F3')->setValue('=FIXED(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('E3')->getCalculatedValue()); + $sheet->getCell('F4')->setValue('=TEXT(A4,"M")'); + self::assertSame('#NAME?', $sheet->getCell('F4')->getCalculatedValue()); + $sheet->getCell('F5')->setValue('=VALUE(A2)'); + self::assertSame('#NUM!', $sheet->getCell('F5')->getCalculatedValue()); + $sheet->getCell('F6')->setValue('=NUMBERVALUE(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('F6')->getCalculatedValue()); + + $sheet->getCell('G1')->setValue('=REPLACE("oldtext",2,2,A1)'); + self::assertSame('#VALUE!', $sheet->getCell('G1')->getCalculatedValue()); + $sheet->getCell('G2')->setValue('=SUBSTITUTE(A2,"U","V")'); + self::assertSame('#NUM!', $sheet->getCell('G2')->getCalculatedValue()); + + $sheet->getCell('H1')->setValue('=FIND(A1, "U")'); + self::assertSame('#VALUE!', $sheet->getCell('H1')->getCalculatedValue()); + $sheet->getCell('H2')->setValue('=SEARCH(A2,"U")'); + self::assertSame('#NUM!', $sheet->getCell('H2')->getCalculatedValue()); + + $sheet->getCell('I1')->setValue('=LEN(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('I1')->getCalculatedValue()); + $sheet->getCell('I2')->setValue('=EXACT(A2,A2)'); + self::assertSame('#NUM!', $sheet->getCell('I2')->getCalculatedValue()); + $sheet->getCell('I3')->setValue('=T(A3)'); + self::assertSame('#DIV/0!', $sheet->getCell('I3')->getCalculatedValue()); + $sheet->getCell('I4')->setValue('=TEXTSPLIT(A4,"M")'); + self::assertSame('#NAME?', $sheet->getCell('I4')->getCalculatedValue()); + + $sheet->getCell('J1')->setValue('=TRIM(A1)'); + self::assertSame('#VALUE!', $sheet->getCell('J1')->getCalculatedValue()); + $sheet->getCell('J2')->setValue('=CLEAN(A2)'); + self::assertSame('#NUM!', $sheet->getCell('J2')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index d3f0ad664..d8de97a33 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -6,6 +6,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Cell; use PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -232,4 +233,31 @@ class AdvancedValueBinderTest extends TestCase ["Hello\nWorld", true], ]; } + + /** + * @dataProvider formulaProvider + */ + public function testFormula(string $value, string $dataType): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + $sheet->getCell('A1')->setValue($value); + self::assertSame($dataType, $sheet->getCell('A1')->getDataType()); + if ($dataType === DataType::TYPE_FORMULA) { + self::assertFalse($sheet->getStyle('A1')->getQuotePrefix()); + } else { + self::assertTrue($sheet->getStyle('A1')->getQuotePrefix()); + } + + $spreadsheet->disconnectWorksheets(); + } + + public static function formulaProvider(): array + { + return [ + 'normal formula' => ['=SUM(A1:C3)', DataType::TYPE_FORMULA], + 'issue 1310' => ['======', DataType::TYPE_STRING], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php index 43ac2fbd1..71006fff8 100644 --- a/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/StringValueBinderTest.php @@ -211,13 +211,19 @@ class StringValueBinderTest extends TestCase $cell->setValue($value); self::assertSame($expectedValue, $cell->getValue()); self::assertSame($expectedDataType, $cell->getDataType()); + if ($expectedDataType === DataType::TYPE_FORMULA) { + self::assertFalse($sheet->getStyle('A1')->getQuotePrefix()); + } else { + self::assertTrue($sheet->getStyle('A1')->getQuotePrefix()); + } $spreadsheet->disconnectWorksheets(); } public static function providerDataValuesSuppressFormulaConversion(): array { return [ - ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA, false], + 'normal formula' => ['=SUM(A1:C3)', '=SUM(A1:C3)', DataType::TYPE_FORMULA], + 'issue 1310' => ['======', '======', DataType::TYPE_STRING], ]; } diff --git a/tests/PhpSpreadsheetTests/NamedRange3Test.php b/tests/PhpSpreadsheetTests/NamedRange3Test.php new file mode 100644 index 000000000..c25dec561 --- /dev/null +++ b/tests/PhpSpreadsheetTests/NamedRange3Test.php @@ -0,0 +1,52 @@ +getActiveSheet(); + $sheet1->setTitle('sheet1'); + $sheet1->setCellValue('B1', 100); + $sheet1->setCellValue('B2', 200); + $sheet1->setCellValue('B3', 300); + $sheet1->setCellValue('B4', 400); + $sheet1->setCellValue('B5', 500); + + $sheet2 = $spreadsheet->createsheet(); + $sheet2->setTitle('sheet2'); + $sheet2->setCellValue('A1', 10); + $sheet2->setCellValue('A2', 20); + $sheet2->setCellValue('A3', 30); + $sheet2->setCellValue('A4', 40); + $sheet2->setCellValue('A5', 50); + + $spreadsheet->addNamedRange( + new NamedRange('somecells', $sheet2, '$A$1:$A$5', true) + ); + $spreadsheet->addNamedRange( + new NamedRange('cellsonsheet1', $sheet1, '$B$1:$B$5') + ); + + $sheet1->getCell('G1')->setValue('=SUM(cellsonsheet1)'); + self::assertSame(1500, $sheet1->getCell('G1')->getCalculatedValue()); + $sheet1->getCell('G2')->setValue('=SUM(sheet2!somecells)'); + self::assertSame(150, $sheet1->getCell('G2')->getCalculatedValue()); + $sheet1->getCell('G3')->setValue('=SUM(somecells)'); + self::assertSame('#NAME?', $sheet1->getCell('G3')->getCalculatedValue()); + $sheet1->getCell('G4')->setValue('=SUM(sheet2!cellsonsheet1)'); + self::assertSame(1500, $sheet1->getCell('G4')->getCalculatedValue()); + $sheet1->getCell('G5')->setValue('=SUM(sheet2xxx!cellsonsheet1)'); + self::assertSame('#NAME?', $sheet1->getCell('G5')->getCalculatedValue()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/BooleanDataTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/BooleanDataTest.php new file mode 100644 index 000000000..28efec197 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/BooleanDataTest.php @@ -0,0 +1,83 @@ +locale = $calculation->getLocale(); + } + + protected function tearDown(): void + { + $calculation = Calculation::getInstance(); + $calculation->setLocale($this->locale); + if ($this->tempfile !== '') { + unlink($this->tempfile); + $this->tempfile = ''; + } + } + + public function testBooleanData(): void + { + $spreadsheetOld = new Spreadsheet(); + $sheetOld = $spreadsheetOld->getActiveSheet(); + $sheetOld->getCell('A1')->setValue(true); + $sheetOld->getCell('A2')->setValue(false); + $writer = new OdsWriter($spreadsheetOld); + $this->tempfile = File::temporaryFileName(); + $writer->save($this->tempfile); + $spreadsheetOld->disconnectWorksheets(); + $reader = new OdsReader(); + $spreadsheet = $reader->load($this->tempfile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertTrue($sheet->getCell('A1')->getValue()); + self::assertFalse($sheet->getCell('A2')->getValue()); + $spreadsheet->disconnectWorksheets(); + $zipFile = 'zip://' . $this->tempfile . '#content.xml'; + $contents = (string) file_get_contents($zipFile); + self::assertStringContainsString('TRUE', $contents); + self::assertStringContainsString('FALSE', $contents); + } + + public function testBooleanDataGerman(): void + { + $calculation = Calculation::getInstance(); + $calculation->setLocale('de'); + $spreadsheetOld = new Spreadsheet(); + $sheetOld = $spreadsheetOld->getActiveSheet(); + $sheetOld->getCell('A1')->setValue(true); + $sheetOld->getCell('A2')->setValue(false); + $writer = new OdsWriter($spreadsheetOld); + $this->tempfile = File::temporaryFileName(); + $writer->save($this->tempfile); + $spreadsheetOld->disconnectWorksheets(); + $reader = new OdsReader(); + $spreadsheet = $reader->load($this->tempfile); + $sheet = $spreadsheet->getActiveSheet(); + self::assertTrue($sheet->getCell('A1')->getValue()); + self::assertFalse($sheet->getCell('A2')->getValue()); + $spreadsheet->disconnectWorksheets(); + $zipFile = 'zip://' . $this->tempfile . '#content.xml'; + $contents = (string) file_get_contents($zipFile); + self::assertStringContainsString('WAHR', $contents); + self::assertStringContainsString('FALSCH', $contents); + self::assertStringNotContainsString('TRUE', $contents); + self::assertStringNotContainsString('FALSE', $contents); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/Issue804Test.php b/tests/PhpSpreadsheetTests/Reader/Ods/Issue804Test.php new file mode 100644 index 000000000..afb33dabe --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/Issue804Test.php @@ -0,0 +1,38 @@ + + + Name', $data); + } + } + + public function testIssue2810(): void + { + // Whitespace between Xml nodes + $filename = 'tests/data/Reader/Ods/issue.804.ods'; + $reader = new Ods(); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Straße', $sheet->getCell('G1')->getValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php new file mode 100644 index 000000000..f6fd5657a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/MultiLineCommentTest.php @@ -0,0 +1,41 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame("First line.\n\nSecond line.", $sheet->getComment('A1')->getText()->getPlainText()); + $spreadsheet->disconnectWorksheets(); + } + + public function testOneParagraphMultipleSpans(): void + { + $spreadsheetOld = new Spreadsheet(); + $sheetOld = $spreadsheetOld->getActiveSheet(); + $sheetOld->getCell('A1')->setValue('Hello'); + $text = $sheetOld->getComment('A1')->getText(); + $text->createText('First'); + $text->createText(' line.'); + $text->createText("\n"); + $text->createText("\n"); + $text->createText("Second line.\nThird line."); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods'); + $spreadsheetOld->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame("First line.\n\nSecond line.\nThird line.", $sheet->getComment('A1')->getText()->getPlainText()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/GridlinesTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/GridlinesTest.php new file mode 100644 index 000000000..5387ea738 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/GridlinesTest.php @@ -0,0 +1,50 @@ +getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('deliberatelyblank'); + $sheet1->setShowGridlines($display); + $sheet1->setPrintGridlines($print); + $sheet1->fromArray( + [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ] + ); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet1 = $reloadedSpreadsheet->getSheet(0); + $rsheet2 = $reloadedSpreadsheet->getSheet(1); + self::assertSame($display, $rsheet1->getShowGridlines()); + self::assertSame($print, $rsheet1->getPrintGridlines()); + self::assertTrue($rsheet2->getShowGridlines()); + self::assertFalse($rsheet2->getPrintGridlines()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public static function loadDataProvider(): array + { + return [ + [true, true], + [true, false], + [false, true], + [false, false], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php new file mode 100644 index 000000000..6ba87bd53 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php @@ -0,0 +1,25 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=CONCATENATE("Prefix ",MID(CELL("filename"),FIND("]",CELL("filename"))+1,255), " Suffix")', $sheet->getCell('B1')->getValue()); + self::assertSame('Prefix SomeName Suffix', $sheet->getCell('B1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Issue4112Test.php b/tests/PhpSpreadsheetTests/Worksheet/Issue4112Test.php new file mode 100644 index 000000000..9b230a9c9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Issue4112Test.php @@ -0,0 +1,43 @@ +removeSheetByIndex(0); + $worksheet = new Worksheet($mySpreadsheet, 'addedsheet'); + self::assertSame(-1, $mySpreadsheet->getActiveSheetIndex()); + $mySpreadsheet->addSheet($worksheet, $sheetNumber); + self::assertSame('addedsheet', $mySpreadsheet->getActiveSheet()->getTitle()); + $row = 1; + $col = 1; + $worksheet->getCell([$col, $row])->setValue('id_uti'); + self::assertSame('id_uti', $worksheet->getCell([$col, $row])->getValue()); + $mySpreadsheet->disconnectWorksheets(); + } + + public static function providerSheetNumber(): array + { + return [ + 'problem case' => [0], + 'normal case' => [null], + 'negative 1 (as if there were no sheets)' => [-1], + 'diffeent negative number' => [-4], + 'positive number' => [4], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/Issue3678Test.php b/tests/PhpSpreadsheetTests/Writer/Html/Issue3678Test.php index cabcaca8f..e4f292c2d 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/Issue3678Test.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/Issue3678Test.php @@ -24,7 +24,8 @@ class Issue3678Test extends TestCase ]; $sheet->getStyle('A1')->applyFromArray($styleArray); $style1 = "vertical-align:bottom; border-bottom:none #000000; border-top:none #000000; border-left:none #000000; border-right:none #000000; color:#000000; font-family:'Calibri'; font-size:11pt; background-color:#FFFF00"; - $style2 = $style1 . '; text-align:right; width:42pt'; + $style2 = "vertical-align:bottom; color:#000000; font-family:'Calibri'; font-size:11pt; background-color:#FFFF00"; + $style2 .= '; text-align:right; width:42pt'; $writer = new Html($spreadsheet); $html = $writer->generateHtmlAll(); self::assertStringContainsString('td.style1, th.style1 { ' . $style1 . ' }', $html); @@ -33,7 +34,7 @@ class Issue3678Test extends TestCase self::assertStringContainsString('.n { text-align:right }', $html); $writer->setUseInlineCss(true); $html = $writer->generateHtmlAll(); - self::assertStringContainsString('1', $html); + self::assertStringContainsString('1', $html); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Html/NoTitleTest.php b/tests/PhpSpreadsheetTests/Writer/Html/NoTitleTest.php new file mode 100644 index 000000000..7faf8a6ad --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/NoTitleTest.php @@ -0,0 +1,62 @@ +load($file); + self::assertSame('', $spreadsheet->getProperties()->getTitle()); + + $writer = new Html($spreadsheet); + $writer->setUseInlineCss(true); + $html = $writer->generateHTMLAll(); + self::assertStringContainsString('Sheet1', $html); + self::assertStringContainsString('C1', $html); + $writer->setUseInlineCss(false); + $html = $writer->generateHTMLAll(); + self::assertStringContainsString('C1', $html); + $spreadsheet->disconnectWorksheets(); + } + + public function testHideSomeGridlines(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray( + [ + [1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12], + [17, 18, 19, 20, 21, 22], + [27, 28, 29, 30, 31, 32], + [37, 38, 39, 40, 41, 42], + ] + ); + $sheet->getStyle('B2:D4')->getBorders()->applyFromArray( + [ + 'allBorders' => [ + 'borderStyle' => Border::BORDER_NONE, + 'color' => ['rgb' => '808080'], + ], + ], + ); + + $writer = new Html($spreadsheet); + $writer->setUseInlineCss(true); + $html = $writer->generateHTMLAll(); + self::assertStringContainsString('7', $html); + self::assertStringContainsString('19', $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php index 74901dd40..21e64f2a9 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/XlsGifBmpTest.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class XlsGifBmpTest extends AbstractFunctional @@ -83,6 +84,34 @@ class XlsGifBmpTest extends AbstractFunctional $reloadedSpreadsheet->disconnectWorksheets(); } + public function testGifIssue4112(): void + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + $sheet = new Worksheet($spreadsheet, 'Insured List'); + $spreadsheet->addSheet($sheet, 0); + + // Add a drawing to the worksheet + $drawing = new Drawing(); + $drawing->setName('Letters G, I, and G'); + $drawing->setDescription('Handwritten G, I, and F'); + $drawing->setPath(__DIR__ . '/../../../../samples/images/gif.gif'); + $drawing->setHeight(36); + $drawing->setWorksheet($sheet); + $drawing->setCoordinates('A1'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); + $spreadsheet->disconnectWorksheets(); + $worksheet = $reloadedSpreadsheet->getActiveSheet(); + $drawings = $worksheet->getDrawingCollection(); + self::assertCount(1, $drawings); + foreach ($worksheet->getDrawingCollection() as $drawing) { + $mimeType = ($drawing instanceof MemoryDrawing) ? $drawing->getMimeType() : 'notmemorydrawing'; + self::assertEquals('image/png', $mimeType); + } + $reloadedSpreadsheet->disconnectWorksheets(); + } + public function testInvalidTimestamp(): void { $this->expectException(ReaderException::class); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue476Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue476Test.php new file mode 100644 index 000000000..513fe7940 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue476Test.php @@ -0,0 +1,30 @@ +load('tests/data/Writer/XLSX/issue.476.xlsx'); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $richText = $sheet->getCell('A1')->getValue(); + self::assertInstanceOf(RichText::class, $richText); + $plainText = $richText->getPlainText(); + self::assertSame("Art. 1A of the Geneva Refugee Convention and Protocol or other international or national instruments.\n", $plainText); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php b/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php index 76f6ddd46..b2202704d 100644 --- a/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php +++ b/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php @@ -82,7 +82,7 @@ return [ 2, ], 'Column number omitted from 2-column matrix' => [ - '#REF!', // Expected + 'abc', // Expected [ ['abc', 'def'], ['xyz', 'tuv'], diff --git a/tests/data/Cell/DefaultValueBinder.php b/tests/data/Cell/DefaultValueBinder.php index b9f3d51e1..bd4295c0b 100644 --- a/tests/data/Cell/DefaultValueBinder.php +++ b/tests/data/Cell/DefaultValueBinder.php @@ -83,4 +83,7 @@ return [ 's', '1234567890123459012345689012345690', ], + 'Issue 1310 Multiple = at start' => ['s', '======'], + 'Issue 1310 Variant 1' => ['s', '= ====='], + 'Issue 1310 Variant 2' => ['s', '=2*3='], ]; diff --git a/tests/data/Reader/Ods/issue.4081.ods b/tests/data/Reader/Ods/issue.4081.ods new file mode 100644 index 000000000..6d690bd4e Binary files /dev/null and b/tests/data/Reader/Ods/issue.4081.ods differ diff --git a/tests/data/Reader/Ods/issue.804.ods b/tests/data/Reader/Ods/issue.804.ods new file mode 100644 index 000000000..b42de4cf6 Binary files /dev/null and b/tests/data/Reader/Ods/issue.804.ods differ diff --git a/tests/data/Reader/XLSX/issue.2581.xlsx b/tests/data/Reader/XLSX/issue.2581.xlsx new file mode 100644 index 000000000..82af1c627 Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2581.xlsx differ diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index 12140fa92..bb115583c 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -95,11 +95,11 @@ - - 1 + + TRUE - - + + FALSE 1 1 diff --git a/tests/data/Writer/XLSX/issue.476.xlsx b/tests/data/Writer/XLSX/issue.476.xlsx new file mode 100644 index 000000000..f76f2d227 Binary files /dev/null and b/tests/data/Writer/XLSX/issue.476.xlsx differ