diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f4e4325..9e6ed9882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Check if Coordinate is Inside Range [PR #3779](https://github.com/PHPOffice/PhpSpreadsheet/pull/3779) - Flipping Images [Issue #731](https://github.com/PHPOffice/PhpSpreadsheet/issues/731) [PR #3801](https://github.com/PHPOffice/PhpSpreadsheet/pull/3801) - Chart Dynamic Title and Font Properties [Issue #3797](https://github.com/PHPOffice/PhpSpreadsheet/issues/3797) [PR #3800](https://github.com/PHPOffice/PhpSpreadsheet/pull/3800) -- Chart Axis Display Units and Logarithmic Scale. [Issue #3833](https://github.com/PHPOffice/PhpSpreadsheet/pull/3833) [PR #3836](https://github.com/PHPOffice/PhpSpreadsheet/pull/3836) +- Chart Axis Display Units and Logarithmic Scale. [Issue #3833](https://github.com/PHPOffice/PhpSpreadsheet/issues/3833) [PR #3836](https://github.com/PHPOffice/PhpSpreadsheet/pull/3836) +- Partial Support of Fill Handles. [Discussion #3847](https://github.com/PHPOffice/PhpSpreadsheet/discussions/3847) [PR #3855](https://github.com/PHPOffice/PhpSpreadsheet/pull/3855) ### Changed @@ -85,10 +86,13 @@ and this project adheres to [Semantic Versioning](https://semver.org). - COUNTIFS Does Not Require xlfn. [Issue #3819](https://github.com/PHPOffice/PhpSpreadsheet/issues/3819) [PR #3827](https://github.com/PHPOffice/PhpSpreadsheet/pull/3827) - Strip `xlfn.` and `xlws.` from Formula Translations. [Issue #3819](https://github.com/PHPOffice/PhpSpreadsheet/issues/3819) [PR #3828](https://github.com/PHPOffice/PhpSpreadsheet/pull/3828) - Recurse directories searching for font file. [Issue #2809](https://github.com/PHPOffice/PhpSpreadsheet/issues/2809) [PR #3830](https://github.com/PHPOffice/PhpSpreadsheet/pull/3830) -- Reduce memory consumption of Worksheet::rangeToArray() when many empty rows are read. [Issue #3814](https://github.com/PHPOffice/PhpSpreadsheet/pull/3814) [PR #3834](https://github.com/PHPOffice/PhpSpreadsheet/pull/3834) +- Reduce memory consumption of Worksheet::rangeToArray() when many empty rows are read. [Issue #3814](https://github.com/PHPOffice/PhpSpreadsheet/issues/3814) [PR #3834](https://github.com/PHPOffice/PhpSpreadsheet/pull/3834) - Reduce time used by Worksheet::rangeToArray() when many empty rows are read. [PR #3839](https://github.com/PHPOffice/PhpSpreadsheet/pull/3839) - Html Reader Tolerate Invalid Sheet Title. [PR #3845](https://github.com/PHPOffice/PhpSpreadsheet/pull/3845) -- Do not include unparsed drawings when new drawing added. [Issue #3843](https://github.com/PHPOffice/PhpSpreadsheet/pull/3843) [PR #3846](https://github.com/PHPOffice/PhpSpreadsheet/pull/3846) +- Do not include unparsed drawings when new drawing added. [Issue #3843](https://github.com/PHPOffice/PhpSpreadsheet/issues/3843) [PR #3846](https://github.com/PHPOffice/PhpSpreadsheet/pull/3846) +- Do not include unparsed drawings when new drawing added. [Issue #3861](https://github.com/PHPOffice/PhpSpreadsheet/issues/3861) [PR #3862](https://github.com/PHPOffice/PhpSpreadsheet/pull/3862) +- Excel omits `between` operator for data validation. [Issue #3863](https://github.com/PHPOffice/PhpSpreadsheet/issues/3863) [PR #3865](https://github.com/PHPOffice/PhpSpreadsheet/pull/3865) +- Use less space when inserting rows and columns. [Issue #3687](https://github.com/PHPOffice/PhpSpreadsheet/issues/3687) [PR #3856](https://github.com/PHPOffice/PhpSpreadsheet/pull/3856) ## 1.29.0 - 2023-06-15 diff --git a/samples/Reading_workbook_data/Custom_properties.php b/samples/Reading_workbook_data/Custom_properties.php index 1c222b500..78fd39347 100644 --- a/samples/Reading_workbook_data/Custom_properties.php +++ b/samples/Reading_workbook_data/Custom_properties.php @@ -38,7 +38,7 @@ foreach ($customPropertyList as $customPropertyName) { break; case 'd': // date - $propertyValue = date('l, d<\s\up>S F Y g:i A', $propertyValue); + $propertyValue = date('l, d<\s\up>S F Y g:i A', (int) $propertyValue); $propertyType = 'date'; break; diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php index a8ef114b4..e3b6cbe51 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Filter { - public static function filter(mixed $lookupArray, mixed $matchArray, mixed $ifEmpty = null): mixed + public static function filter(array $lookupArray, mixed $matchArray, mixed $ifEmpty = null): mixed { if (!is_array($matchArray)) { return ExcelError::VALUE(); diff --git a/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php b/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php index 09c2f749d..53ea87b37 100644 --- a/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php +++ b/src/PhpSpreadsheet/Cell/AdvancedValueBinder.php @@ -43,9 +43,9 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder } // Check for fractions - if (preg_match('/^([+-]?)\s*(\d+)\s?\/\s*(\d+)$/', $value, $matches)) { + if (preg_match('~^([+-]?)\s*(\d+)\s*/\s*(\d+)$~', $value, $matches)) { return $this->setProperFraction($matches, $cell); - } elseif (preg_match('/^([+-]?)(\d*) +(\d*)\s?\/\s*(\d*)$/', $value, $matches)) { + } elseif (preg_match('~^([+-]?)(\d+)\s+(\d+)\s*/\s*(\d+)$~', $value, $matches)) { return $this->setImproperFraction($matches, $cell); } diff --git a/src/PhpSpreadsheet/Cell/DataValidation.php b/src/PhpSpreadsheet/Cell/DataValidation.php index 184564b8d..9a5f44e34 100644 --- a/src/PhpSpreadsheet/Cell/DataValidation.php +++ b/src/PhpSpreadsheet/Cell/DataValidation.php @@ -28,6 +28,7 @@ class DataValidation const OPERATOR_LESSTHANOREQUAL = 'lessThanOrEqual'; const OPERATOR_NOTBETWEEN = 'notBetween'; const OPERATOR_NOTEQUAL = 'notEqual'; + private const DEFAULT_OPERATOR = self::OPERATOR_BETWEEN; /** * Formula 1. @@ -52,7 +53,7 @@ class DataValidation /** * Operator. */ - private string $operator = self::OPERATOR_BETWEEN; + private string $operator = self::DEFAULT_OPERATOR; /** * Allow Blank. @@ -198,7 +199,7 @@ class DataValidation */ public function setOperator(string $operator): static { - $this->operator = $operator; + $this->operator = ($operator === '') ? self::DEFAULT_OPERATOR : $operator; return $this; } diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 24a5065cd..2a4664ecd 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -81,7 +81,7 @@ class Properties /** * Custom Properties. * - * @var array{value: mixed, type: string}[] + * @var array{value: null|bool|float|int|string, type: string}[] */ private array $customProperties = []; @@ -359,7 +359,7 @@ class Properties /** * Get a Custom Property Value. */ - public function getCustomPropertyValue(string $propertyName): mixed + public function getCustomPropertyValue(string $propertyName): bool|int|float|string|null { if (isset($this->customProperties[$propertyName])) { return $this->customProperties[$propertyName]['value']; @@ -376,7 +376,7 @@ class Properties return $this->customProperties[$propertyName]['type'] ?? null; } - private function identifyPropertyType(mixed $propertyValue): string + private function identifyPropertyType(bool|int|float|string|null $propertyValue): string { if (is_float($propertyValue)) { return self::PROPERTY_TYPE_FLOAT; @@ -398,18 +398,16 @@ class Properties * * @return $this */ - public function setCustomProperty(string $propertyName, mixed $propertyValue = '', ?string $propertyType = null): self + public function setCustomProperty(string $propertyName, bool|int|float|string|null $propertyValue = '', ?string $propertyType = null): self { if (($propertyType === null) || (!in_array($propertyType, self::VALID_PROPERTY_TYPE_LIST))) { $propertyType = $this->identifyPropertyType($propertyValue); } - if (!is_object($propertyValue)) { - $this->customProperties[$propertyName] = [ - 'value' => self::convertProperty($propertyValue, $propertyType), - 'type' => $propertyType, - ]; - } + $this->customProperties[$propertyName] = [ + 'value' => self::convertProperty($propertyValue, $propertyType), + 'type' => $propertyType, + ]; return $this; } @@ -451,7 +449,7 @@ class Properties /** * Convert property to form desired by Excel. */ - public static function convertProperty(mixed $propertyValue, string $propertyType): mixed + public static function convertProperty(bool|int|float|string|null $propertyValue, string $propertyType): bool|int|float|string|null { return self::SPECIAL_TYPES[$propertyType] ?? self::convertProperty2($propertyValue, $propertyType); } @@ -459,7 +457,7 @@ class Properties /** * Convert property to form desired by Excel. */ - private static function convertProperty2(mixed $propertyValue, string $type): mixed + private static function convertProperty2(bool|int|float|string|null $propertyValue, string $type): bool|int|float|string|null { $propertyType = self::convertPropertyType($type); switch ($propertyType) { @@ -470,7 +468,7 @@ class Properties case self::PROPERTY_TYPE_FLOAT: return (float) $propertyValue; case self::PROPERTY_TYPE_DATE: - return self::intOrFloatTimestamp($propertyValue); + return self::intOrFloatTimestamp($propertyValue); // @phpstan-ignore-line case self::PROPERTY_TYPE_BOOLEAN: return is_bool($propertyValue) ? $propertyValue : ($propertyValue === 'true'); default: // includes string diff --git a/src/PhpSpreadsheet/Reader/Ods/Properties.php b/src/PhpSpreadsheet/Reader/Ods/Properties.php index f56561082..a5f0c79f4 100644 --- a/src/PhpSpreadsheet/Reader/Ods/Properties.php +++ b/src/PhpSpreadsheet/Reader/Ods/Properties.php @@ -101,7 +101,7 @@ class Properties } } - private function setUserDefinedProperty(mixed $propertyValueAttributes, string $propertyValue, DocumentProperties $docProps): void + private function setUserDefinedProperty(iterable $propertyValueAttributes, string $propertyValue, DocumentProperties $docProps): void { $propertyValueName = ''; $propertyValueType = DocumentProperties::PROPERTY_TYPE_STRING; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php index beac9f9a8..fb501e84e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Properties.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -19,11 +19,6 @@ class Properties $this->docProps = $docProps; } - private static function nullOrSimple(mixed $obj): ?SimpleXMLElement - { - return ($obj instanceof SimpleXMLElement) ? $obj : null; - } - private function extractPropertyData(string $propertyData): ?SimpleXMLElement { // okay to omit namespace because everything will be processed by xpath @@ -33,7 +28,7 @@ class Properties Settings::getLibXmlLoaderOptions() ); - return self::nullOrSimple($obj); + return $obj === false ? null : $obj; } public function readCoreProperties(string $propertyData): void @@ -45,15 +40,15 @@ class Properties $xmlCore->registerXPathNamespace('dcterms', Namespaces::DC_TERMS); $xmlCore->registerXPathNamespace('cp', Namespaces::CORE_PROPERTIES2); - $this->docProps->setCreator((string) self::getArrayItem($xmlCore->xpath('dc:creator'))); - $this->docProps->setLastModifiedBy((string) self::getArrayItem($xmlCore->xpath('cp:lastModifiedBy'))); - $this->docProps->setCreated((string) self::getArrayItem($xmlCore->xpath('dcterms:created'))); //! respect xsi:type - $this->docProps->setModified((string) self::getArrayItem($xmlCore->xpath('dcterms:modified'))); //! respect xsi:type - $this->docProps->setTitle((string) self::getArrayItem($xmlCore->xpath('dc:title'))); - $this->docProps->setDescription((string) self::getArrayItem($xmlCore->xpath('dc:description'))); - $this->docProps->setSubject((string) self::getArrayItem($xmlCore->xpath('dc:subject'))); - $this->docProps->setKeywords((string) self::getArrayItem($xmlCore->xpath('cp:keywords'))); - $this->docProps->setCategory((string) self::getArrayItem($xmlCore->xpath('cp:category'))); + $this->docProps->setCreator($this->getArrayItem($xmlCore->xpath('dc:creator'))); + $this->docProps->setLastModifiedBy($this->getArrayItem($xmlCore->xpath('cp:lastModifiedBy'))); + $this->docProps->setCreated($this->getArrayItem($xmlCore->xpath('dcterms:created'))); //! respect xsi:type + $this->docProps->setModified($this->getArrayItem($xmlCore->xpath('dcterms:modified'))); //! respect xsi:type + $this->docProps->setTitle($this->getArrayItem($xmlCore->xpath('dc:title'))); + $this->docProps->setDescription($this->getArrayItem($xmlCore->xpath('dc:description'))); + $this->docProps->setSubject($this->getArrayItem($xmlCore->xpath('dc:subject'))); + $this->docProps->setKeywords($this->getArrayItem($xmlCore->xpath('cp:keywords'))); + $this->docProps->setCategory($this->getArrayItem($xmlCore->xpath('cp:category'))); } } @@ -96,8 +91,8 @@ class Properties } } - private static function getArrayItem(null|array|false $array, mixed $key = 0): ?SimpleXMLElement + private function getArrayItem(null|array|false $array): string { - return is_array($array) ? ($array[$key] ?? null) : null; + return is_array($array) ? (string) ($array[0] ?? '') : ''; } } diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index fc9429a9c..1acedf116 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -377,7 +377,9 @@ class ReferenceHelper // Clear cells if we are removing columns or rows $highestColumn = $worksheet->getHighestColumn(); + $highestDataColumn = $worksheet->getHighestDataColumn(); $highestRow = $worksheet->getHighestRow(); + $highestDataRow = $worksheet->getHighestDataRow(); // 1. Clear column strips if we are removing columns if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) { @@ -392,7 +394,7 @@ class ReferenceHelper // Find missing coordinates. This is important when inserting column before the last column $cellCollection = $worksheet->getCellCollection(); $missingCoordinates = array_filter( - array_map(fn ($row): string => "{$highestColumn}{$row}", range(1, $highestRow)), + array_map(fn ($row): string => "{$highestDataColumn}{$row}", range(1, $highestDataRow)), fn ($coordinate): bool => $cellCollection->has($coordinate) === false ); @@ -1171,7 +1173,9 @@ class ReferenceHelper if ($worksheet->cellExists($coordinate)) { $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { - $worksheet->getCell([$j, $i])->setXfIndex($xfIndex); + if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) { + $worksheet->getCell([$j, $i])->setXfIndex($xfIndex); + } } } } @@ -1186,7 +1190,9 @@ class ReferenceHelper if ($worksheet->cellExists($coordinate)) { $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { - $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + if (!empty($xfIndex) || $worksheet->cellExists([$j, $i])) { + $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + } } } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 01a35b763..9b85506ed 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3587,4 +3587,30 @@ class Worksheet implements IComparable return $this; } + + /** + * Copy cells, adjusting relative cell references in formulas. + * Acts similarly to Excel "fill handle" feature. + * + * @param string $fromCell Single source cell, e.g. C3 + * @param string $toCells Single cell or cell range, e.g. C4 or C4:C10 + * @param bool $copyStyle Copy styles as well as values, defaults to true + */ + public function copyCells(string $fromCell, string $toCells, bool $copyStyle = true): void + { + $toArray = Coordinate::extractAllCellReferencesInRange($toCells); + $value = $this->getCell($fromCell)->getValue(); + $style = $this->getStyle($fromCell)->exportArray(); + $fromIndexes = Coordinate::indexesFromString($fromCell); + $referenceHelper = ReferenceHelper::getInstance(); + foreach ($toArray as $destination) { + if ($destination !== $fromCell) { + $toIndexes = Coordinate::indexesFromString($destination); + $this->getCell($destination)->setValue($referenceHelper->updateFormulaReferences($value, 'A1', $toIndexes[0] - $fromIndexes[0], $toIndexes[1] - $fromIndexes[1])); + if ($copyStyle) { + $this->getCell($destination)->getStyle()->applyFromArray($style); + } + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Ods/Meta.php b/src/PhpSpreadsheet/Writer/Ods/Meta.php index 75023a6da..be937c256 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Meta.php +++ b/src/PhpSpreadsheet/Writer/Ods/Meta.php @@ -85,7 +85,7 @@ class Meta extends WriterPart private static function writeDocPropsCustom(XMLWriter $objWriter, Spreadsheet $spreadsheet): void { $customPropertyList = $spreadsheet->getProperties()->getCustomProperties(); - foreach ($customPropertyList as $key => $customProperty) { + foreach ($customPropertyList as $customProperty) { $propertyValue = $spreadsheet->getProperties()->getCustomPropertyValue($customProperty); $propertyType = $spreadsheet->getProperties()->getCustomPropertyType($customProperty); @@ -96,7 +96,7 @@ class Meta extends WriterPart case Properties::PROPERTY_TYPE_INTEGER: case Properties::PROPERTY_TYPE_FLOAT: $objWriter->writeAttribute('meta:value-type', 'float'); - $objWriter->writeRawData($propertyValue); + $objWriter->writeRawData($propertyValue); // @phpstan-ignore-line break; case Properties::PROPERTY_TYPE_BOOLEAN: @@ -106,12 +106,12 @@ class Meta extends WriterPart break; case Properties::PROPERTY_TYPE_DATE: $objWriter->writeAttribute('meta:value-type', 'date'); - $dtobj = Date::dateTimeFromTimestamp($propertyValue ?? 0); + $dtobj = Date::dateTimeFromTimestamp($propertyValue ?? 0); // @phpstan-ignore-line $objWriter->writeRawData($dtobj->format(DATE_W3C)); break; default: - $objWriter->writeRawData($propertyValue); + $objWriter->writeRawData($propertyValue); // @phpstan-ignore-line break; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index d426d118b..78accd585 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -216,7 +216,7 @@ class DocProps extends WriterPart switch ($propertyType) { case Properties::PROPERTY_TYPE_INTEGER: - $objWriter->writeElement('vt:i4', $propertyValue); + $objWriter->writeElement('vt:i4', $propertyValue); // @phpstan-ignore-line break; case Properties::PROPERTY_TYPE_FLOAT: @@ -235,7 +235,7 @@ class DocProps extends WriterPart break; default: - $objWriter->writeElement('vt:lpwstr', $propertyValue); + $objWriter->writeElement('vt:lpwstr', $propertyValue); // @phpstan-ignore-line break; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index dedb8ef3b..39d3ed35b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1431,16 +1431,20 @@ class Worksheet extends WriterPart { // Cell $pCell = $worksheet->getCell($cellAddress); + $xfi = $pCell->getXfIndex(); + $cellValue = $pCell->getValue(); + $writeValue = $cellValue !== '' && $cellValue !== null; + if (empty($xfi) && !$writeValue) { + return; + } $objWriter->startElement('c'); $objWriter->writeAttribute('r', $cellAddress); // Sheet styles - $xfi = $pCell->getXfIndex(); self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi"); // If cell value is supplied, write cell value - $cellValue = $pCell->getValue(); - if (is_object($cellValue) || $cellValue !== '') { + if ($writeValue) { // Map type $mappedType = $pCell->getDataType(); diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index 7183b4fa7..d3f0ad664 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -150,6 +150,12 @@ class AdvancedValueBinderTest extends TestCase ['1 16/20', 1.8], ['12 20/100', 12.2], ['-1 4/20', -1.2], + ['407 / ', '407 / '], + ['407 /', '407 /'], + ['407 3/', '407 3/'], + ['-407 /4', -101.75], + [' /', ' /'], + [' / ', ' / '], ]; } diff --git a/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php b/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php index 08fea3ccd..be00dd392 100644 --- a/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php +++ b/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php @@ -4,44 +4,89 @@ declare(strict_types=1); namespace PhpOffice\PhpSpreadsheetTests\Functional; -use PhpOffice\PhpSpreadsheet\Reader\IReader; -use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; class ReadBlankCellsTest extends AbstractFunctional { public static function providerSheetFormat(): array { return [ - ['Xlsx'], - ['Xls'], - // ['Ods'], // Broken. Requires fix in Ods reader. - // ['Csv'], // never reads blank cells - // ['Html'], // never reads blank cells + ['Xlsx', false], + ['Xls', true], + ['Ods', true], + ['Csv', false], + ['Html', false], ]; } + /** + * Test load file with explicitly empty cells. + */ + public function testLoadReadEmptyCells(): void + { + $filename = 'tests/data/Reader/XLSX/blankcell.xlsx'; + $reader = new Xlsx(); + $reloadedSpreadsheet = $reader->load($filename); + self::assertTrue($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('B2')); + self::assertFalse($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C2')); + self::assertTrue($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C3')); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + /** + * Test load file ignoring empty cells. + */ + public function testLoadDontReadEmptyCells(): void + { + $filename = 'tests/data/Reader/XLSX/blankcell.xlsx'; + $reader = new Xlsx(); + $reader->setReadEmptyCells(false); + $reloadedSpreadsheet = $reader->load($filename); + self::assertFalse($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('B2')); + self::assertFalse($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C2')); + self::assertTrue($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C3')); + $reloadedSpreadsheet->disconnectWorksheets(); + } + /** * Test generate file with some empty cells. * * @dataProvider providerSheetFormat */ - public function testXlsxLoadWithNoBlankCells(mixed $format): void + public function testLoadAndSaveReadEmpty(string $format, bool $expected): void { - $spreadsheet = new Spreadsheet(); - $spreadsheet->getActiveSheet()->getCell('B2')->setValue(''); - $spreadsheet->getActiveSheet()->getCell('C1')->setValue('C1'); - $spreadsheet->getActiveSheet()->getCell('C3')->setValue('C3'); - + $filename = 'tests/data/Reader/XLSX/blankcell.xlsx'; + $reader = new Xlsx(); + //$reader->setReadEmptyCells(false); + $spreadsheet = $reader->load($filename); $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); - self::assertTrue($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('B2')); + $spreadsheet->disconnectWorksheets(); + self::assertSame($expected, $reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('B2')); + if ($expected) { + self::assertContains($reloadedSpreadsheet->getActiveSheet()->getCell('B2')->getValue(), ['', null]); + } self::assertFalse($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C2')); self::assertTrue($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C3')); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + /** + * Test generate file with some empty cells. + * + * @dataProvider providerSheetFormat + */ + public function testLoadAndSaveDontReadEmpty(string $format): void + { + $filename = 'tests/data/Reader/XLSX/blankcell.xlsx'; + $reader = new Xlsx(); + $reader->setReadEmptyCells(false); + $spreadsheet = $reader->load($filename); + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format); + $spreadsheet->disconnectWorksheets(); - $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format, function (IReader $reader): void { - $reader->setReadEmptyCells(false); - }); self::assertFalse($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('B2')); self::assertFalse($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C2')); self::assertTrue($reloadedSpreadsheet->getActiveSheet()->getCellCollection()->has('C3')); + $reloadedSpreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3863Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3863Test.php new file mode 100644 index 000000000..1e378b565 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3863Test.php @@ -0,0 +1,42 @@ +510', $data); + } + } + + public function testValidData(): void + { + $reader = new Xlsx(); + $spreadsheet = $reader->load(self::$testbook); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('between', $sheet->getCell('A1')->getDataValidation()->getOperator()); + $validator = new DataValidator(); + self::assertTrue($validator->isValid($sheet->getCell('A1'))); + $sheet->getCell('A1')->setValue(3); + self::assertFalse($validator->isValid($sheet->getCell('A1'))); + $sheet->getCell('A1')->setValue(7); + self::assertTrue($validator->isValid($sheet->getCell('A1'))); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/CopyCellsTest.php b/tests/PhpSpreadsheetTests/Worksheet/CopyCellsTest.php new file mode 100644 index 000000000..98d7c056d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/CopyCellsTest.php @@ -0,0 +1,85 @@ +getActiveSheet(); + $sheet->fromArray( + [ + ['hello1', 'goodbye1', 'neither1', 'constant'], + ['hello2', 'goodbye2', 'neither2'], + ['hello3', 'goodbye3', 'neither3'], + ['hello4', 'goodbye4', 'neither4'], + ['hello5', 'goodbye5', 'neither5'], + ], + ); + $sheet->getCell('E3')->setValue('=A1&B1'); + $sheet->getStyle('E3')->getFont()->setBold(true); + $sheet->copyCells('E3', 'E3:F7'); + $result1 = $sheet->rangeToArray('E3:F7', null, false, false); + $expected1 = [ + ['=A1&B1', '=B1&C1'], + ['=A2&B2', '=B2&C2'], + ['=A3&B3', '=B3&C3'], + ['=A4&B4', '=B4&C4'], + ['=A5&B5', '=B5&C5'], + ]; + self::assertSame($expected1, $result1); + self::assertSame('goodbye3neither3', $sheet->getCell('F5')->getCalculatedValue()); + self::assertTrue($sheet->getCell('F5')->getStyle()->getFont()->getBold()); + + $sheet->getCell('E14')->setValue('=A5&$D$1'); + $sheet->copyCells('E14', 'E10:E14'); + $result2 = $sheet->rangeToArray('E10:E14', null, false, false); + $expected2 = [ + ['=A1&$D$1'], + ['=A2&$D$1'], + ['=A3&$D$1'], + ['=A4&$D$1'], + ['=A5&$D$1'], + ]; + self::assertSame($expected2, $result2); + self::assertSame('hello4constant', $sheet->getCell('E13')->getCalculatedValue()); + + $sheet->getCell('I3')->setValue('=A1&$B1'); + $sheet->getStyle('I3')->getFont()->setItalic(true); + $sheet->copyCells('I3', 'I3:J7', false); + $result3 = $sheet->rangeToArray('I3:J7', null, false, false); + $expected3 = [ + ['=A1&$B1', '=B1&$B1'], + ['=A2&$B2', '=B2&$B2'], + ['=A3&$B3', '=B3&$B3'], + ['=A4&$B4', '=B4&$B4'], + ['=A5&$B5', '=B5&$B5'], + ]; + self::assertSame($expected3, $result3); + self::assertSame('hello2goodbye2', $sheet->getCell('I4')->getCalculatedValue()); + self::assertFalse($sheet->getCell('I5')->getStyle()->getFont()->getItalic()); + + try { + $sheet->copyCells('invalid', 'Z1:Z10'); + self::fail('Did not receive expected exception'); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Invalid cell coordinate', $e->getMessage()); + } + + try { + $sheet->copyCells('A1', 'invalid'); + self::fail('Did not receive expected exception'); + } catch (SpreadsheetException $e) { + self::assertStringContainsString('Column string index', $e->getMessage()); + } + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php b/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php new file mode 100644 index 000000000..4db7f9bf7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php @@ -0,0 +1,67 @@ +getActiveSheet(); + $sheet->fromArray([ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + [17, 18, 19, 20], + ]); + $sheet->getRowDimension(1000)->setVisible(false); + $sheet->getStyle('C3')->getFont()->setBold(true); + self::assertSame(1000, $sheet->getHighestRow()); + self::assertSame(5, $sheet->getHighestDataRow()); + $currentRow = 4; + $sheet->insertNewRowBefore($currentRow, 1); + self::assertSame(1001, $sheet->getHighestRow()); + self::assertSame(6, $sheet->getHighestDataRow()); + self::assertTrue($sheet->getStyle('C3')->getFont()->getBold()); + self::assertSame(11, $sheet->getCell('C3')->getValue()); + self::assertTrue($sheet->getStyle('C4')->getFont()->getBold()); + self::assertNull($sheet->getCell('C4')->getValue()); + self::assertFalse($sheet->getRowDimension(1001)->getVisible()); + self::assertTrue($sheet->getRowDimension(1000)->getVisible()); + $spreadsheet->disconnectWorksheets(); + } + + public function testInsertColumn(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([ + [1, 2, 3, 4], + [5, 6, 7, 8], + [9, 10, 11, 12], + [13, 14, 15, 16], + [17, 18, 19, 20], + ]); + $sheet->getColumnDimension('ZY')->setVisible(false); + $sheet->getStyle('C3')->getFont()->setBold(true); + self::assertSame('ZY', $sheet->getHighestColumn()); + self::assertSame('D', $sheet->getHighestDataColumn()); + $currentColumn = 'D'; + $sheet->insertNewColumnBefore($currentColumn, 1); + self::assertSame('ZZ', $sheet->getHighestColumn()); + self::assertSame('E', $sheet->getHighestDataColumn()); + self::assertTrue($sheet->getStyle('C3')->getFont()->getBold()); + self::assertSame(11, $sheet->getCell('C3')->getValue()); + self::assertTrue($sheet->getStyle('D3')->getFont()->getBold()); + self::assertNull($sheet->getCell('D3')->getValue()); + self::assertFalse($sheet->getColumnDimension('ZZ')->getVisible()); + self::assertTrue($sheet->getColumnDimension('ZY')->getVisible()); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Reader/XLSX/blankcell.xlsx b/tests/data/Reader/XLSX/blankcell.xlsx new file mode 100644 index 000000000..3a7604dee Binary files /dev/null and b/tests/data/Reader/XLSX/blankcell.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.3863.xlsx b/tests/data/Reader/XLSX/issue.3863.xlsx new file mode 100644 index 000000000..684ba23f6 Binary files /dev/null and b/tests/data/Reader/XLSX/issue.3863.xlsx differ