diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f19a8cd4..b5e5b419b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ 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 - 3.8.0 +## TBD - 3.9.0 ### Added -- CHOOSECOLS, CHOOSEROWS, DROP, TAKE, and EXPAND. [PR #4286](https://github.com/PHPOffice/PhpSpreadsheet/pull/4286) +- Methods to get style for row or column. [PR #4317](https://github.com/PHPOffice/PhpSpreadsheet/pull/4317) +- Method for duplicating worksheet in spreadsheet. [PR #4315](https://github.com/PHPOffice/PhpSpreadsheet/pull/4315) ### Changed @@ -25,7 +26,23 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Ods Reader Sheet Names with Period. [Issue #4311](https://github.com/PHPOffice/PhpSpreadsheet/issues/4311) [PR #4313](https://github.com/PHPOffice/PhpSpreadsheet/pull/4313) +- Mpdf and Tcpdf Hidden Columns and Merged Cells. [Issue #4319](https://github.com/PHPOffice/PhpSpreadsheet/issues/4319) [PR #4320](https://github.com/PHPOffice/PhpSpreadsheet/pull/4320) +- Html Writer Allow mailto. [Issue #4316](https://github.com/PHPOffice/PhpSpreadsheet/issues/4316) [PR #4322](https://github.com/PHPOffice/PhpSpreadsheet/pull/4322) +- Use composer/pcre rather than preg_* in Writer. [PR #4323](https://github.com/PHPOffice/PhpSpreadsheet/pull/4323) + +## 2025-01-11 - 3.8.0 + +### Added + +- CHOOSECOLS, CHOOSEROWS, DROP, TAKE, and EXPAND. [PR #4286](https://github.com/PHPOffice/PhpSpreadsheet/pull/4286) + +### Fixed + +- Security patch for Html navigation. - Xlsx Reader Shared Formula with Boolean Result. Partial solution for [Issue #4280](https://github.com/PHPOffice/PhpSpreadsheet/issues/4280) [PR #4281](https://github.com/PHPOffice/PhpSpreadsheet/pull/4281) +- Retitling cloned Worksheets. [Issue #641](https://github.com/PHPOffice/PhpSpreadsheet/issues/641) [PR #4302](https://github.com/PHPOffice/PhpSpreadsheet/pull/4302) +- Extremely limited support for GROUPBY function. Partial response to [Issue #4282](https://github.com/PHPOffice/PhpSpreadsheet/issues/4282) [PR #4283](https://github.com/PHPOffice/PhpSpreadsheet/pull/4283) ## 2024-12-26 - 3.7.0 diff --git a/LICENSE b/LICENSE index 3ec5723dde..04a90f083e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 PhpSpreadsheet Authors +Copyright (c) 2019-2025 PhpSpreadsheet Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 7f1777676c..22d22c1a67 100644 --- a/composer.json +++ b/composer.json @@ -79,6 +79,7 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", + "composer/pcre": "^3.3", "maennchen/zipstream-php": "^2.1 || ^3.0", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", diff --git a/composer.lock b/composer.lock index 3025170cb3..ee9da8ac4f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,87 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f7caa2101e268445a358275ee83b90d9", + "content-hash": "264ef911bd6d773a3a9085bc86b0ea48", "packages": [ + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.1.1", @@ -467,85 +546,6 @@ ], "time": "2022-12-23T10:58:28+00:00" }, - { - "name": "composer/pcre", - "version": "3.2.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "reference": "ea4ab6f9580a4fd221e0418f2c357cdd39102a90", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.8" - }, - "require-dev": { - "phpstan/phpstan": "^1.11.8", - "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8 || ^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.2.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2024-07-25T09:36:02+00:00" - }, { "name": "composer/semver", "version": "3.4.2", @@ -1221,16 +1221,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.4.3", + "version": "10.4.4", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "f0db97108aec23a3bbb34721365931af992b83b3" + "reference": "9ad8e2fcc30f765c788a28543e9705fb541d499f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/f0db97108aec23a3bbb34721365931af992b83b3", - "reference": "f0db97108aec23a3bbb34721365931af992b83b3", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/9ad8e2fcc30f765c788a28543e9705fb541d499f", + "reference": "9ad8e2fcc30f765c788a28543e9705fb541d499f", "shasum": "" }, "require": { @@ -1270,9 +1270,9 @@ ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.4.3" + "source": "https://github.com/mitoteam/jpgraph/tree/10.4.4" }, - "time": "2024-12-01T06:36:31+00:00" + "time": "2025-01-01T05:39:20+00:00" }, { "name": "mpdf/mpdf", diff --git a/docs/references/function-list-by-category.md b/docs/references/function-list-by-category.md index 447c97367e..e6a702028f 100644 --- a/docs/references/function-list-by-category.md +++ b/docs/references/function-list-by-category.md @@ -249,6 +249,7 @@ EXPAND | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Choos FILTER | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Filter::filter FORMULATEXT | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Formula::text GETPIVOTDATA | **Not yet Implemented** +GROUPBY | **Not yet Implemented** HLOOKUP | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\HLookup::lookup HYPERLINK | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Hyperlink::set INDEX | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix::index diff --git a/docs/references/function-list-by-name.md b/docs/references/function-list-by-name.md index 868da519a2..3696b95b7c 100644 --- a/docs/references/function-list-by-name.md +++ b/docs/references/function-list-by-name.md @@ -239,6 +239,7 @@ GCD | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpread GEOMEAN | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages\Mean::geometric GESTEP | CATEGORY_ENGINEERING | \PhpOffice\PhpSpreadsheet\Calculation\Engineering\Compare::GESTEP GETPIVOTDATA | CATEGORY_LOOKUP_AND_REFERENCE | **Not yet Implemented** +GROUPBY | CATEGORY_LOOKUP_AND_REFERENCE | **Not yet Implemented** GROWTH | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Trends::GROWTH ## H diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 8b27155d90..076ac79b58 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -925,6 +925,9 @@ getStyle('A1:M500'), rather than styling the cells individually in a loop. This is much faster compared to looping through cells and styling them individually. +**Tip** If you are styling entire row(s) or column(s), e.g. getStyle('A:A'), it is recommended to use applyFromArray as described below rather than setting the styles individually as described above. +Also, starting with release 3.9.0, you should use getRowStyle or getColumnStyle to get the style for an entire row or column. + There is also an alternative manner to set styles. The following code sets a cell's style to font bold, alignment right, top border thin and a gradient fill: diff --git a/docs/topics/worksheets.md b/docs/topics/worksheets.md index 0199f13c6a..5b15089b61 100644 --- a/docs/topics/worksheets.md +++ b/docs/topics/worksheets.md @@ -95,19 +95,38 @@ insert the clone into the workbook. ```php $clonedWorksheet = clone $spreadsheet->getSheetByName('Worksheet 1'); -$clonedWorksheet->setTitle('Copy of Worksheet 1'); +$clonedWorksheet->setTitle('Copy of Worksheet 1'); // must be unique $spreadsheet->addSheet($clonedWorksheet); ``` +Starting with PhpSpreadsheet 3.9.0, this can be done more simply (copied sheet's title will be set to something unique): +```php +$copiedWorksheet = $spreadsheet->duplicateWorksheetByTitle('sheetname'); +``` You can also copy worksheets from one workbook to another, though this is more complex as PhpSpreadsheet also has to replicate the styling between the two workbooks. The `addExternalSheet()` method is provided for this purpose. - $clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1'); - $spreadsheet->addExternalSheet($clonedWorksheet); +```php +$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1'); +$clonedWorksheet->setTitle('Copy of Worksheet 1'); // must be unique +$spreadsheet1->addSheet($clonedWorksheet); +$spreadsheet->addExternalSheet($clonedWorksheet); +``` +Starting with PhpSpreadsheet 3.8.0, this can be simplified: +```php +$clonedWorksheet = clone $spreadsheet1->getSheetByName('Worksheet 1'); +$spreadsheet1->addSheet($clonedWorksheet, null, true); +$spreadsheet->addExternalSheet($clonedWorksheet); +``` +Starting with PhpSpreadsheet 3.9.0, this can be simplified even further: +```php +$clonedWorksheet = $spreadsheet1->duplicateWorksheetByTitle('sheetname'); +$spreadsheet->addExternalSheet($clonedWorksheet); +``` -In both cases, it is the developer's responsibility to ensure that +In the cases commented "must be unique", it is the developer's responsibility to ensure that worksheet names are not duplicated. PhpSpreadsheet will throw an exception if you attempt to copy worksheets that will result in a duplicate name. diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 06fbbcdd66..e34d429456 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,6 +2,7 @@ includes: - phpstan-baseline.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/composer/pcre/extension.neon parameters: level: 8 diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index 38ba4d9c45..e9d3402590 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -1,5 +1,6 @@ EOF; - return preg_replace($bodystring, $bodyrepl, $html) ?? ''; + return preg_replace($bodystring, $bodyrepl, $html) ?? throw new SpreadsheetException('preg failed'); } require __DIR__ . '/../Header.php'; diff --git a/samples/Pdf/21c_Pdf.php b/samples/Pdf/21c_Pdf.php index 5a6a26d379..66d964aedd 100644 --- a/samples/Pdf/21c_Pdf.php +++ b/samples/Pdf/21c_Pdf.php @@ -1,5 +1,6 @@ /'; $simulatedBodyStart = Mpdf::SIMULATED_BODY_START; $bodyrepl = <<S F Y g:i A', (int) $propertyValue) : '*****INVALID*****'; + $propertyValue = is_numeric($propertyValue) ? date('l, j<\s\u\p>S F Y g:i A', (int) $propertyValue) : '*****INVALID*****'; $propertyType = 'date'; break; diff --git a/samples/Reading_workbook_data/Properties.php b/samples/Reading_workbook_data/Properties.php index a24a4967a8..441b12c9a5 100644 --- a/samples/Reading_workbook_data/Properties.php +++ b/samples/Reading_workbook_data/Properties.php @@ -19,7 +19,7 @@ // Read the Date when the workbook was created (as a PHP timestamp value) $creationDatestamp = $spreadsheet->getProperties()->getCreated(); -$creationDate = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'l, d<\s\up>S F Y'); +$creationDate = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'l, j<\s\u\p>S F Y'); $creationTime = Date::formattedDateTimeFromTimestamp("$creationDatestamp", 'g:i A'); $helper->log('Created On: ' . $creationDate . ' at ' . $creationTime); @@ -30,7 +30,7 @@ // Read the Date when the workbook was last modified (as a PHP timestamp value) $modifiedDatestamp = $spreadsheet->getProperties()->getModified(); // Format the date and time using the standard PHP date() function -$modifiedDate = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'l, d<\s\up>S F Y'); +$modifiedDate = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'l, j<\s\u\p>S F Y'); $modifiedTime = Date::formattedDateTimeFromTimestamp("$modifiedDatestamp", 'g:i A'); $helper->log('Last Modified On: ' . $modifiedDate . ' at ' . $modifiedTime); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index c31ea2c749..b256aad3b3 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1256,6 +1256,11 @@ public static function getExcelConstants(string $key): bool|null 'functionCall' => [Functions::class, 'DUMMY'], 'argumentCount' => '2+', ], + 'GROUPBY' => [ + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '3-7', + ], 'GROWTH' => [ 'category' => Category::CATEGORY_STATISTICAL, 'functionCall' => [Statistical\Trends::class, 'GROWTH'], @@ -4601,7 +4606,7 @@ private static function dataTestReference(array &$operandData): mixed private static int $matchIndex10 = 10; /** - * @return array|false + * @return array|false|string */ private function processTokenStack(mixed $tokens, ?string $cellID = null, ?Cell $cell = null) { @@ -5182,6 +5187,9 @@ private function processTokenStack(mixed $tokens, ?string $cellID = null, ?Cell } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) { // if the token is a named range or formula, evaluate it and push the result onto the stack $definedName = $matches[6]; + if (str_starts_with($definedName, '_xleta')) { + return Functions::NOT_YET_IMPLEMENTED; + } if ($cell === null || $pCellWorksheet === null) { return $this->raiseFormulaError("undefined name '$token'"); } @@ -5214,6 +5222,7 @@ private function processTokenStack(mixed $tokens, ?string $cellID = null, ?Cell } $result = $this->evaluateDefinedName($cell, $namedRange, $pCellWorksheet, $stack, $specifiedWorksheet !== ''); + if (isset($storeKey)) { $branchStore[$storeKey] = $result; } diff --git a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php index 27862d7a5a..6e54e0039e 100644 --- a/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php +++ b/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php @@ -6,10 +6,24 @@ class FormulaTranslator { - public static function convertToExcelAddressValue(string $openOfficeAddress): string + private static function replaceQuotedPeriod(string $value): string { - $excelAddress = $openOfficeAddress; + $value2 = ''; + $quoted = false; + foreach (mb_str_split($value, 1, 'UTF-8') as $char) { + if ($char === "'") { + $quoted = !$quoted; + } elseif ($char === '.' && $quoted) { + $char = "\u{fffe}"; + } + $value2 .= $char; + } + return $value2; + } + + public static function convertToExcelAddressValue(string $openOfficeAddress): string + { // Cell range 3-d reference // As we don't support 3-d ranges, we're just going to take a quick and dirty approach // and assume that the second worksheet reference is the same as the first @@ -20,6 +34,7 @@ public static function convertToExcelAddressValue(string $openOfficeAddress): st '/\$?([^\.]+)\.([^\.]+)/miu', // Cell reference in another sheet '/\.([^\.]+):\.([^\.]+)/miu', // Cell range reference '/\.([^\.]+)/miu', // Simple cell reference + '/\\x{FFFE}/miu', // restore quoted periods ], [ '$1!$2:$4', @@ -27,8 +42,9 @@ public static function convertToExcelAddressValue(string $openOfficeAddress): st '$1!$2', '$1:$2', '$1', + '.', ], - $excelAddress + self::replaceQuotedPeriod($openOfficeAddress) ); return $excelAddress; @@ -52,14 +68,16 @@ public static function convertToExcelFormulaValue(string $openOfficeFormula): st '/\[\$?([^\.]+)\.([^\.]+)\]/miu', // Cell reference in another sheet '/\[\.([^\.]+):\.([^\.]+)\]/miu', // Cell range reference '/\[\.([^\.]+)\]/miu', // Simple cell reference + '/\\x{FFFE}/miu', // restore quoted periods ], [ '$1!$2:$3', '$1!$2', '$1:$2', '$1', + '.', ], - $value + self::replaceQuotedPeriod($value) ); // Convert references to defined names/formulae $value = str_replace('$$', '', $value); diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index a16b1cafc0..73bb057dfb 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -638,4 +638,9 @@ public static function testStringAsNumeric(string $textValue): float|string return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue; } + + public static function strlenAllowNull(?string $string): int + { + return strlen("$string"); + } } diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 943db95cc3..86f9d891ea 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -514,7 +514,7 @@ public function getActiveSheet(): Worksheet public function createSheet(?int $sheetIndex = null): Worksheet { $newSheet = new Worksheet($this); - $this->addSheet($newSheet, $sheetIndex); + $this->addSheet($newSheet, $sheetIndex, true); return $newSheet; } @@ -529,14 +529,35 @@ public function sheetNameExists(string $worksheetName): bool return $this->getSheetByName($worksheetName) !== null; } + public function duplicateWorksheetByTitle(string $title): Worksheet + { + $original = $this->getSheetByNameOrThrow($title); + $index = $this->getIndex($original) + 1; + $clone = clone $original; + + return $this->addSheet($clone, $index, true); + } + /** * Add sheet. * * @param Worksheet $worksheet The worksheet to add * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) */ - public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null): Worksheet + public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null, bool $retitleIfNeeded = false): Worksheet { + if ($retitleIfNeeded) { + $title = $worksheet->getTitle(); + if ($this->sheetNameExists($title)) { + $i = 1; + $newTitle = "$title $i"; + while ($this->sheetNameExists($newTitle)) { + ++$i; + $newTitle = "$title $i"; + } + $worksheet->setTitle($newTitle); + } + } if ($this->sheetNameExists($worksheet->getTitle())) { throw new Exception( "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first." @@ -1080,6 +1101,11 @@ public function getCellXfByIndex(int $cellStyleIndex): Style return $this->cellXfCollection[$cellStyleIndex]; } + public function getCellXfByIndexOrNull(?int $cellStyleIndex): ?Style + { + return ($cellStyleIndex === null) ? null : ($this->cellXfCollection[$cellStyleIndex] ?? null); + } + /** * Get cellXf by hash code. * diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 2c4a469559..4b88fcadc9 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -50,7 +50,7 @@ public function setEvaluated(bool $value): void /** * Create a new AutoFilter. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range * A simple string containing a Cell range like 'A1:E10' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 234014a0ff..a7a62a954c 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -64,7 +64,7 @@ class Table implements Stringable /** * Create a new Table. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range * A simple string containing a Cell range like 'A1:E10' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. @@ -268,7 +268,7 @@ public function getRange(): string /** * Set Table Cell Range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range * A simple string containing a Cell range like 'A1:E10' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index 99b5eedbc5..ec78c22b0b 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -36,7 +36,7 @@ public static function validateCellAddress(null|CellAddress|string|array $cellAd /** * Validate a cell address or cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), * or as a CellAddress or AddressRange object. */ @@ -59,7 +59,7 @@ public static function validateCellOrCellRange(AddressRange|CellAddress|int|stri /** * Validate a cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), * or as an AddressRange object. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 0e66f2b894..4bc7d26244 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -49,6 +49,8 @@ class Worksheet public const MERGE_CELL_CONTENT_HIDE = 'hide'; public const MERGE_CELL_CONTENT_MERGE = 'merge'; + public const FUNCTION_LIKE_GROUPBY = '/\\b(groupby|_xleta)\\b/i'; // weird new syntax + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** @@ -321,6 +323,7 @@ public function __construct(?Spreadsheet $parent = null, string $title = 'Worksh { // Set parent and title $this->parent = $parent; + $this->hash = spl_object_id($this); $this->setTitle($title, false); // setTitle can change $pTitle $this->setCodeName($this->getTitle()); @@ -349,7 +352,6 @@ public function __construct(?Spreadsheet $parent = null, string $title = 'Worksh $this->autoFilter = new AutoFilter('', $this); // Table collection $this->tableCollection = new ArrayObject(); - $this->hash = spl_object_id($this); } /** @@ -869,7 +871,7 @@ public function setTitle(string $title, bool $updateFormulaCellReferences = true // Syntax check self::checkSheetTitle($title); - if ($this->parent) { + if ($this->parent && $this->parent->getIndex($this, true) >= 0) { // Is there already such sheet name? if ($this->parent->sheetNameExists($title)) { // Use name, but append with lowest possible integer @@ -899,7 +901,7 @@ public function setTitle(string $title, bool $updateFormulaCellReferences = true // Set title $this->title = $title; - if ($this->parent && $this->parent->getCalculationEngine()) { + if ($this->parent && $this->parent->getIndex($this, true) >= 0 && $this->parent->getCalculationEngine()) { // New title $newTitle = $this->getTitle(); $this->parent->getCalculationEngine() @@ -1333,6 +1335,13 @@ public function getRowDimension(int $row): RowDimension return $this->rowDimensions[$row]; } + public function getRowStyle(int $row): ?Style + { + return $this->parent?->getCellXfByIndexOrNull( + ($this->rowDimensions[$row] ?? null)?->getXfIndex() + ); + } + public function rowDimensionExists(int $row): bool { return isset($this->rowDimensions[$row]); @@ -1376,6 +1385,13 @@ public function getColumnDimensionByColumn(int $columnIndex): ColumnDimension return $this->getColumnDimension(Coordinate::stringFromColumnIndex($columnIndex)); } + public function getColumnStyle(string $column): ?Style + { + return $this->parent?->getCellXfByIndexOrNull( + ($this->columnDimensions[$column] ?? null)?->getXfIndex() + ); + } + /** * Get styles. * @@ -1389,7 +1405,7 @@ public function getStyles(): array /** * Get style for cell. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellCoordinate + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellCoordinate * A simple string containing a cell address like 'A1' or a cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. @@ -1722,7 +1738,7 @@ public function getColumnBreaks(): array /** * Set merge on a cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. * @param string $behaviour How the merged cells should behave. @@ -1847,7 +1863,7 @@ public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behavi /** * Remove merge on a cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. * @@ -1898,7 +1914,7 @@ public function setMergeCells(array $mergeCells): static /** * Set protection on a cell or cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @param string $password Password to unlock the protection @@ -1921,7 +1937,7 @@ public function protectCells(AddressRange|CellAddress|int|string|array $range, s /** * Remove protection on a cell or cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @@ -1979,7 +1995,7 @@ public function getAutoFilter(): AutoFilter /** * Set AutoFilter. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. @@ -2725,7 +2741,7 @@ public function setSelectedCell(string $coordinate): static /** * Select a range of cells. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @@ -3730,7 +3746,9 @@ public function calculateArrays(bool $preCalculateFormulas = true): void $keys = $this->cellCollection->getCoordinates(); foreach ($keys as $key) { if ($this->getCell($key)->getDataType() === DataType::TYPE_FORMULA) { - $this->getCell($key)->getCalculatedValue(); + if (preg_match(self::FUNCTION_LIKE_GROUPBY, $this->getCell($key)->getValue()) !== 1) { + $this->getCell($key)->getCalculatedValue(); + } } } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index d70a067f6f..3f4af3e325 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer; +use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; @@ -561,7 +562,7 @@ public function generateNavigation(): string $html .= '