From 04d6b1897df9ce314f6648bd8dde8529681d9404 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 2 Jan 2024 18:42:56 -0800 Subject: [PATCH 1/8] Partial Support of Fill Handles Fix #3847. Person posing the question calls this behavior "pull formulas", and wants equivalent handling to be available in PhpSpreadsheet. I can do this for formulas in a single cell; I can go horizontal or vertical or (unlike Excel) both. Nevertheless, I call this "partial support" because I cannot think how I can do something similar that Excel does, e.g. putting 2 in cell A1 and 4 in Cell A2, selecting them both and using the fill handle to extend those to rows below, so that cell A5 will contain 10. --- CHANGELOG.md | 3 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 26 ++++++ .../Worksheet/CopyCellsTest.php | 85 +++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/CopyCellsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f4e4325..7a67a5edc 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 diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index dc3c9a9e8..f7f2e5930 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -4004,4 +4004,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/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(); + } +} From 9c279679aadc6d1a1aae9c0134ef48f102f69fbe Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 3 Jan 2024 18:03:28 -0800 Subject: [PATCH 2/8] Use Less Space When Inserting Rows and Columns Fix #3687. Worksheet methods insertNewRowBefore and insertNewColumnBefore call ReferenceHelper insertNewBefore. That function fills in "missing" cells with null values. However, for boundaries, it uses getHighestRow and getHighestColumn. It should be sufficient to use getHighestDataRow and getHighestDataColumn. When there is a big gap between getHighest... and getHighestData..., this can result in a big increase in memory usage, and in file space when saving the spreadsheet. New test InsertTest demonstrates the problem by populating a worksheet with cells A1:D5 (so highestDataRow is 5), but also setting row 1000 to invisible (so highestRow is 1000). The major part of the change is in ReferenceHelper::insertNewBefore, which will now use getHighestData... for its boundaries when filling in the missing cells. Changes of less impact are made to duplicateStylesByColumn and duplicateStylesByRow so that cells which don't yet exist are not created unless the style that will be applied is not the workbook default style. As for reducing the file size, Writer/Xlsx/Worksheet is changed so that cells whose value is null or null-string and which use the workbook default style are not written to the output spreadsheet. This requires some changes to existing test ReadBlankCellsTest; I don't think the difference should matter to the end-user. --- src/PhpSpreadsheet/ReferenceHelper.php | 12 ++- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 10 ++- .../Functional/ReadBlankCellsTest.php | 77 ++++++++++++++---- .../Worksheet/InsertTest.php | 67 +++++++++++++++ tests/data/Reader/XLSX/blankcell.xlsx | Bin 0 -> 8664 bytes 5 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/InsertTest.php create mode 100644 tests/data/Reader/XLSX/blankcell.xlsx diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 5ec796708..200dfa248 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -382,7 +382,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) { @@ -397,7 +399,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 ); @@ -1176,7 +1178,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); + } } } } @@ -1191,7 +1195,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/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 53f44e341..a6aefbf77 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1442,16 +1442,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/Functional/ReadBlankCellsTest.php b/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php index ad1729b00..05f8bfc22 100644 --- a/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php +++ b/tests/PhpSpreadsheetTests/Functional/ReadBlankCellsTest.php @@ -4,43 +4,88 @@ declare(strict_types=1); namespace PhpOffice\PhpSpreadsheetTests\Functional; -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(); + } - $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, $format, function ($reader): void { - $reader->setReadEmptyCells(false); - }); + /** + * 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(); 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/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 0000000000000000000000000000000000000000..3a7604dee705cbed59be90534581b4845d2029bb GIT binary patch literal 8664 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 Date: Sun, 7 Jan 2024 22:27:02 +0800 Subject: [PATCH 3/8] Remove a few mixed type --- .../Custom_properties.php | 2 +- .../Calculation/LookupRef/Filter.php | 2 +- src/PhpSpreadsheet/Document/Properties.php | 24 +++++++-------- src/PhpSpreadsheet/Reader/Ods/Properties.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx/Properties.php | 29 ++++++++----------- src/PhpSpreadsheet/Writer/Ods/Meta.php | 8 ++--- src/PhpSpreadsheet/Writer/Xlsx/DocProps.php | 4 +-- 7 files changed, 32 insertions(+), 39 deletions(-) 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/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/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; } From 5f232348d7806948dcf87bb156145d7540b044ed Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:29:00 -0800 Subject: [PATCH 4/8] Advanced Value Binder False Positive Looking for Fractions Fix #3861. Strings ending in `/` were inappropriately identifed as fractions. Fix regexp accordingly. --- src/PhpSpreadsheet/Cell/AdvancedValueBinder.php | 4 ++-- tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) 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/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], + [' /', ' /'], + [' / ', ' / '], ]; } From 31473ef34bbb56762e30b2f97e91bc66af63d46c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 9 Jan 2024 20:04:56 -0800 Subject: [PATCH 5/8] Update CHANGELOG.md --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f4e4325..be155ffab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,10 +85,11 @@ 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) ## 1.29.0 - 2023-06-15 From 9e89c36c97f17ae14b2e32fd82cec7a382d7aaed Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 11 Jan 2024 23:03:55 -0800 Subject: [PATCH 6/8] Excel Omits `Between` Operator for Data Validation Fix #3863. Data Validation default operator is `between`. When Excel writes out a data validation item, it may omit the operator. Xlsx reader will therefore initialize operator to null string. Issue indicates that user wants `between` returned for `getOperator`. A more serious problem is that `isValid` method does not handle this situation correctly. Data Validation is changed to set Operator to the default value if an attempt is made to set it to null string. --- src/PhpSpreadsheet/Cell/DataValidation.php | 5 ++- .../Reader/Xlsx/Issue3863Test.php | 42 ++++++++++++++++++ tests/data/Reader/XLSX/issue.3863.xlsx | Bin 0 -> 8528 bytes 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/Issue3863Test.php create mode 100644 tests/data/Reader/XLSX/issue.3863.xlsx 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/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/data/Reader/XLSX/issue.3863.xlsx b/tests/data/Reader/XLSX/issue.3863.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..684ba23f69059eb6a521bafe5c05d222b4678100 GIT binary patch literal 8528 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 J_q#v7{U3Vv-0J`U literal 0 HcmV?d00001 From 3bf77c48dfb282cff0a862806ee5899855dda2db Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 11 Jan 2024 23:25:44 -0800 Subject: [PATCH 7/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe2d5e24..181ab0120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - 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/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) ## 1.29.0 - 2023-06-15 From 57c6d12e196ee675ca3614397e69840685af6c8e Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sat, 13 Jan 2024 08:38:23 -0800 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 181ab0120..9e6ed9882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - 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