diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index 546deedef..23f58c2bc 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cd2fe8425..12241164e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install locales run: sudo apt-get update && sudo apt-get install -y language-pack-fr language-pack-de @@ -43,7 +43,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 2 @@ -93,7 +93,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -108,7 +108,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -124,7 +124,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -139,7 +139,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -155,7 +155,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -170,7 +170,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -186,7 +186,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -201,7 +201,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -217,7 +217,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -239,7 +239,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -266,7 +266,7 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: ref: ${{ github.ref }} # Otherwise our annotated tag is not fetched and we cannot get correct version diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5b8adf2..5df44961a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,9 @@ Some earlier branches remain supported and security fixes are applied to them; i ### Added -- Nothing yet. +- Navigate Cell as Cursor. [Issue #863](https://github.com/PHPOffice/PhpSpreadsheet/issues/863) [PR #4909](https://github.com/PHPOffice/PhpSpreadsheet/pull/4909) +- Chart DataTables. [Issue #413](https://github.com/PHPOffice/PhpSpreadsheet/issues/413) [PR #4911](https://github.com/PHPOffice/PhpSpreadsheet/pull/4911) +- Permit Ignoring "Misleading Format" Tag. [PR #4914](https://github.com/PHPOffice/PhpSpreadsheet/pull/4914) ### Removed @@ -33,7 +35,13 @@ Some earlier branches remain supported and security fixes are applied to them; i ### Fixed -- Nothing yet. +- Small improvement for Radar Charts. [Issue #661](https://github.com/PHPOffice/PhpSpreadsheet/issues/661) [PR #4908](https://github.com/PHPOffice/PhpSpreadsheet/pull/4908) +- Allow Use of Multiple Ranges When Setting Styles. [Issue #411](https://github.com/PHPOffice/PhpSpreadsheet/issues/411) [PR #4910](https://github.com/PHPOffice/PhpSpreadsheet/pull/4910) +- Small change to Writer Html. [Issue #434](https://github.com/PHPOffice/PhpSpreadsheet/issues/434) [PR #4912](https://github.com/PHPOffice/PhpSpreadsheet/pull/4912) +- Avoid hard-coding some chart Xml attributes. [PR #4915](https://github.com/PHPOffice/PhpSpreadsheet/pull/4915) +- Readers should directly access readFilter. [PR #4919](https://github.com/PHPOffice/PhpSpreadsheet/pull/4919) +- Very minor changes to Worksheet, Reader/Xlsx, and Worksheet/AutoFilter. [Issue #4917](https://github.com/PHPOffice/PhpSpreadsheet/issues/4917) [PR #4926](https://github.com/PHPOffice/PhpSpreadsheet/pull/4926) +- Consistent HighestRow/Column after row/columnDelete. [Issue #943](https://github.com/PHPOffice/PhpSpreadsheet/issues/943) [PR #4925](https://github.com/PHPOffice/PhpSpreadsheet/pull/4925) ## 2026-06-06 - 5.8.0 diff --git a/composer.lock b/composer.lock index 78b8a0a9d..f34c3f20c 100644 --- a/composer.lock +++ b/composer.lock @@ -8,28 +8,29 @@ "packages": [ { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -67,7 +68,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -77,13 +78,9 @@ { "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" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "maennchen/zipstream-php", @@ -961,16 +958,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.95.4", + "version": "v3.95.11", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "3f8f68856837a77e1f1d870354eca3c8747f2f72" + "reference": "35f98e1293283397824d7f349ce5afb8747c3cd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/3f8f68856837a77e1f1d870354eca3c8747f2f72", - "reference": "3f8f68856837a77e1f1d870354eca3c8747f2f72", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/35f98e1293283397824d7f349ce5afb8747c3cd5", + "reference": "35f98e1293283397824d7f349ce5afb8747c3cd5", "shasum": "" }, "require": { @@ -988,7 +985,7 @@ "react/event-loop": "^1.5", "react/socket": "^1.16", "react/stream": "^1.4", - "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0", + "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0 || ^8.0 || ^9.0", "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", @@ -1004,16 +1001,16 @@ "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.11.0", "infection/infection": "^0.32.7", - "justinrainbow/json-schema": "^6.8.0", + "justinrainbow/json-schema": "^6.10.0", "keradus/cli-executor": "^2.3", "mikey179/vfsstream": "^1.6.12", "php-coveralls/php-coveralls": "^2.9.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.8", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.8", "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55", - "symfony/polyfill-php85": "^1.37", - "symfony/var-dumper": "^5.4.48 || ^6.4.32 || ^7.4.4 || ^8.0.8", - "symfony/yaml": "^5.4.45 || ^6.4.30 || ^7.4.1 || ^8.0.11" + "symfony/polyfill-php85": "^1.38", + "symfony/var-dumper": "^5.4.48 || ^6.4.36 || ^7.4.8 || ^8.1.0", + "symfony/yaml": "^5.4.53 || ^6.4.41 || ^7.4.13 || ^8.1.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1054,7 +1051,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.4" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.11" }, "funding": [ { @@ -1062,7 +1059,7 @@ "type": "github" } ], - "time": "2026-06-03T18:02:44+00:00" + "time": "2026-06-25T14:17:04+00:00" }, { "name": "masterminds/html5", @@ -1710,11 +1707,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.2", + "version": "2.2.3", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", - "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4048833dd47b377287818841877fb3087289509c", + "reference": "4048833dd47b377287818841877fb3087289509c", "shasum": "" }, "require": { @@ -1770,7 +1767,7 @@ "type": "github" } ], - "time": "2026-06-05T09:00:01+00:00" + "time": "2026-06-30T21:15:26+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -1824,21 +1821,22 @@ }, { "name": "phpstan/phpstan-phpunit", - "version": "2.0.16", + "version": "2.0.17", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32" + "reference": "c2f977551f0736d60467b3d754b2e0cf4e337b3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6ab598e1bc106e6827fd346ae4a12b4a5d634c32", - "reference": "6ab598e1bc106e6827fd346ae4a12b4a5d634c32", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/c2f977551f0736d60467b3d754b2e0cf4e337b3f", + "reference": "c2f977551f0736d60467b3d754b2e0cf4e337b3f", "shasum": "" }, "require": { + "phar-io/version": "^3.2", "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^2.1.32" + "phpstan/phpstan": "^2.2.3" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -1848,7 +1846,8 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-strict-rules": "^2.0", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^9.6", + "shipmonk/name-collision-detector": "^2.1" }, "type": "phpstan-extension", "extra": { @@ -1874,9 +1873,9 @@ ], "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.16" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.17" }, - "time": "2026-02-14T09:05:21+00:00" + "time": "2026-06-29T05:32:23+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4436,16 +4435,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -4483,7 +4482,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -4503,7 +4502,7 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/event-dispatcher", @@ -4592,16 +4591,16 @@ }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -4648,7 +4647,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -4668,7 +4667,7 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", @@ -5131,16 +5130,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.38.1", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", - "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -5192,7 +5191,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -5212,7 +5211,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T12:51:13+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php80", @@ -5525,16 +5524,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -5588,7 +5587,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -5608,7 +5607,7 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/stopwatch", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bbe5e3641..29ec2fc13 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,6 +8,7 @@ includes: parameters: level: 10 + treatPhpDocTypesAsCertain: false paths: - samples/ - src/ diff --git a/samples/Chart33a/33_Chart_create_area_3.php b/samples/Chart33a/33_Chart_create_area_3.php new file mode 100644 index 000000000..5e5a3f802 --- /dev/null +++ b/samples/Chart33a/33_Chart_create_area_3.php @@ -0,0 +1,108 @@ +getTheme()->setThemeColorName(SpreadsheetTheme::COLOR_SCHEME_2013_2022_NAME); +$worksheet = $spreadsheet->getActiveSheet(); +$sheet2 = $spreadsheet->createSheet(); +$sheet2->setTitle('Sheet2'); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 +]; +// Set the Data values for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), +]; + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setDataTable(new DataTable()); +// No need for Legend if using DataTable +$legend = null; //new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test %age-Stacked Area Chart'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel +); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('H18'); + +// Add the chart to the worksheet +$sheet2->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); + +$helper->renderChart($chart, __FILE__); + +// Save Excel 2007 file +$helper->write($spreadsheet, __FILE__, ['Xlsx'], true, null, false); diff --git a/samples/Chart33a/33_Chart_create_line_dateaxis.php b/samples/Chart33a/33_Chart_create_line_dateaxis.php index 86415c4aa..7cdefbba7 100644 --- a/samples/Chart33a/33_Chart_create_line_dateaxis.php +++ b/samples/Chart33a/33_Chart_create_line_dateaxis.php @@ -10,6 +10,10 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../Header.php'; /** @var \PhpOffice\PhpSpreadsheet\Helper\Sample $helper */ $spreadsheet = new Spreadsheet(); +// following stmt can be commented in/out to test both calendars +$spreadsheet->setExcelCalendar(SharedDate::CALENDAR_MAC_1904); +$calendar = $spreadsheet->getExcelCalendar(); +$use1904 = $calendar === SharedDate::CALENDAR_MAC_1904; $dataSheet = $spreadsheet->getActiveSheet(); $dataSheet->setTitle('Data'); // changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date @@ -185,6 +189,9 @@ $chart = new Chart( $xAxis, // xAxis $yAxis, // yAxis ); +if ($use1904) { + $chart->setDate1904(true); +} // Set the position of the chart in the chart sheet $chart->setTopLeftPosition('A1'); @@ -275,7 +282,7 @@ $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); $xAxis->setAxisType('dateAx'); // dateAx available ONLY for LINECHART, not SCATTERCHART // measure the time span in Quarters, of data. -$dateMinMax = dateRange(8, $spreadsheet); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q +$dateMinMax = dateRange(8, $spreadsheet, $calendar); // array 'min'=>earliest date of first Q, 'max'=>latest date of final Q // change xAxis tick marks to match Qtr boundaries $nQtrs = sprintf('%3.2f', (($dateMinMax['max'] - $dateMinMax['min']) / 30.5) / 4); @@ -321,6 +328,9 @@ $chart = new Chart( $xAxis, // xAxis $yAxis, // yAxis ); +if ($use1904) { + $chart->setDate1904(true); +} // Set the position of the chart in the chart sheet below the first chart $chart->setTopLeftPosition('A13'); @@ -339,7 +349,7 @@ $helper->write($spreadsheet, __FILE__, ['Xlsx'], true, resetActiveSheet: false); $spreadsheet->disconnectWorksheets(); /** @return array{'min': float|int, 'max': float|int} */ -function dateRange(int $nrows, Spreadsheet $wrkbk): array +function dateRange(int $nrows, Spreadsheet $wrkbk, int $calendar): array { $dataSheet = $wrkbk->getSheetByNameOrThrow('Data'); @@ -357,7 +367,7 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array $qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0); $qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3)); $qtrStartStr = "$startYr-$qtrStartMonth-01"; - $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr); + $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr, $calendar); // end the xaxis at the end of the quarter of the last date /** @var string */ @@ -377,7 +387,7 @@ function dateRange(int $nrows, Spreadsheet $wrkbk): array } $lastDOM = $lastDOMDate->format('t'); $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; - $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); + $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr, $calendar); $minMaxDates = ['min' => $ExcelQtrStartDateVal, 'max' => $ExcelQtrEndDateVal]; diff --git a/samples/templates/32readwriteRadarChart4.xlsx b/samples/templates/32readwriteRadarChart4.xlsx new file mode 100644 index 000000000..d752fb8b6 Binary files /dev/null and b/samples/templates/32readwriteRadarChart4.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index f379ff1fc..da7115344 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -640,7 +640,7 @@ class Calculation extends CalculationLocale if ($this->formulaTokenCacheMaxSize > 0) { // Phpstan says if condition is always false, // but coverage report says next statement is covered. - if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) { // @phpstan-ignore-line + if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) { $this->formulaTokenCache = []; } // Cache key is the original formula string (before ANCHORARRAY transformation) @@ -1391,7 +1391,7 @@ class Calculation extends CalculationLocale // do we now have a function/variable/number? $expectingOperator = true; $expectingOperand = false; - $val = $match[1] ?? ''; //* @phpstan-ignore-line + $val = $match[1] ?? ''; $length = strlen($val); if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { @@ -1955,7 +1955,7 @@ class Calculation extends CalculationLocale if ($breakNeeded) { break; } - $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line + $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); if ($pCellParent !== null && $this->spreadsheet !== null) { $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false); } else { diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index db757e577..74f189e0f 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -46,13 +46,22 @@ class DateValue return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); } + return self::fromString2($dateValue, null); + } + + /** + * @return array|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * depending on the value of the ReturnDateType flag + */ + public static function fromString2(null|string|int|bool|float $dateValue, ?int $calendar = null): array|string|float|int|DateTime + { // try to parse as date iff there is at least one digit if (is_string($dateValue) && preg_match('/\d/', $dateValue) !== 1) { return ExcelError::VALUE(); } $dti = new DateTimeImmutable(); - $baseYear = SharedDateHelper::getExcelCalendar(); + $baseYear = $calendar ?? SharedDateHelper::getExcelCalendar(); $dateValue = trim((string) $dateValue, '"'); // Strip any ordinals because they're allowed in Excel (English only) $dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); @@ -160,9 +169,9 @@ class DateValue $day = self::getInt($PHPDateArray, 'day'); $year = self::getInt($PHPDateArray, 'year'); if (!checkdate($month, $day, $year)) { - return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE(); + return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0, calendar: $baseYear) : ExcelError::VALUE(); } - $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true); + $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true, calendar: $baseYear); } return $retValue; diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php index 492447dd0..5fc951c8b 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -30,10 +30,10 @@ class Helpers * * @return float Excel date/time serial value */ - public static function getDateValue(mixed $dateValue, bool $allowBool = true): float + public static function getDateValue(mixed $dateValue, bool $allowBool = true, ?int $calendar = null): float { if (is_object($dateValue)) { - $retval = SharedDateHelper::PHPToExcel($dateValue); + $retval = SharedDateHelper::PHPToExcel($dateValue, calendar: $calendar); if (is_bool($retval)) { throw new Exception(ExcelError::VALUE()); } @@ -41,7 +41,7 @@ class Helpers return $retval; } - self::nullFalseTrueToNumber($dateValue, $allowBool); + self::nullFalseTrueToNumber($dateValue, $allowBool, $calendar); if (!is_numeric($dateValue)) { $saveReturnDateType = Functions::getReturnDateType(); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); @@ -58,7 +58,7 @@ class Helpers } try { - SharedDateHelper::excelToDateTimeObject((float) $dateValue); + SharedDateHelper::excelToDateTimeObject((float) $dateValue, calendar: $calendar); } catch (Throwable) { throw new Exception(ExcelError::NAN()); } @@ -87,10 +87,10 @@ class Helpers * * @param float|int $dateValue date to be adjusted */ - public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime + public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0, ?int $calendar = null): DateTime { // Execute function - $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue, calendar: $calendar); $oMonth = (int) $PHPDateObject->format('m'); $oYear = (int) $PHPDateObject->format('Y'); @@ -141,7 +141,7 @@ class Helpers * * @param array{year: int, month: int, day: int, hour: int, minute: int, second: int} $dateArray */ - public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int + public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false, ?int $calendar = null): DateTime|float|int { $retType = Functions::getReturnDateType(); if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { @@ -161,58 +161,60 @@ class Helpers $dateArray['day'], $dateArray['hour'], $dateArray['minute'], - $dateArray['second'] + $dateArray['second'], + calendar: $calendar ); if ($retType === Functions::RETURNDATE_EXCEL) { return $noFrac ? floor($excelDateValue) : $excelDateValue; } // RETURNDATE_UNIX_TIMESTAMP) - return SharedDateHelper::excelToTimestamp($excelDateValue); + return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar); } /** * Return result in one of three formats. */ - public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime + public static function returnIn3FormatsFloat(float $excelDateValue, ?int $calendar = null): float|int|DateTime { $retType = Functions::getReturnDateType(); if ($retType === Functions::RETURNDATE_EXCEL) { return $excelDateValue; } if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { - return SharedDateHelper::excelToTimestamp($excelDateValue); + return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar); } // RETURNDATE_PHP_DATETIME_OBJECT - return SharedDateHelper::excelToDateTimeObject($excelDateValue); + return SharedDateHelper::excelToDateTimeObject($excelDateValue, calendar: $calendar); } /** * Return result in one of three formats. */ - public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int + public static function returnIn3FormatsObject(DateTime $PHPDateObject, ?int $calendar = null): DateTime|float|int { $retType = Functions::getReturnDateType(); if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { return $PHPDateObject; } if ($retType === Functions::RETURNDATE_EXCEL) { - return (float) SharedDateHelper::PHPToExcel($PHPDateObject); + return (float) SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar); } // RETURNDATE_UNIX_TIMESTAMP - $stamp = SharedDateHelper::PHPToExcel($PHPDateObject); + $stamp = SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar); $stamp = is_bool($stamp) ? ((int) $stamp) : $stamp; - return SharedDateHelper::excelToTimestamp($stamp); + return SharedDateHelper::excelToTimestamp($stamp, calendar: $calendar); } - private static function baseDate(): int + private static function baseDate(?int $calendar): int { if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { return 0; } - if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) { + $calendar ??= SharedDateHelper::getExcelCalendar(); + if ($calendar === SharedDateHelper::CALENDAR_MAC_1904) { return 0; } @@ -222,10 +224,10 @@ class Helpers /** * Many functions accept null/false/true argument treated as 0/0/1. */ - public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true): void + public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true, ?int $calendar = null): void { $number = Functions::flattenSingleValue($number); - $nullVal = self::baseDate(); + $nullVal = self::baseDate($calendar); if ($number === null) { $number = $nullVal; } elseif ($allowBool && is_bool($number)) { diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php index 53668d134..130e4c103 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -166,7 +166,7 @@ class NonPeriodic $foundneg = false; for ($i = 0; $i < $valCount; ++$i) { $fld = $values[$i]; - if (!is_numeric($fld)) { //* @phpstan-ignore-line + if (!is_numeric($fld)) { return ExcelError::VALUE(); } elseif ($fld > 0) { $foundpos = true; diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index 4fee352c6..8280bc5ef 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -54,9 +54,7 @@ class Value } try { - // Phpstan claims cellValue can't be null. - // I don't see why. - [$column, $row] = Coordinate::indexesFromString($cellValue ?? ''); // @phpstan-ignore-line + [$column, $row] = Coordinate::indexesFromString($cellValue ?? ''); } catch (SpreadsheetException) { return false; } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php index 486194e46..3106d4e7e 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -58,7 +58,7 @@ class Filter */ private static function filterByRow(array $lookupArray, array $matchArray): array { - $matchArray = array_values(array_column($matchArray, 0)); // @phpstan-ignore-line + $matchArray = array_values(array_column($matchArray, 0)); return array_filter( array_values($lookupArray), diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 2b0da2fa5..5fdccfc8d 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -191,12 +191,22 @@ class Cell implements Stringable public function getFormattedValue(): string { $currentCalendar = SharedDate::getExcelCalendar(); - SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar()); - $formattedValue = (string) NumberFormat::toFormattedString( - $this->getCalculatedValueString(), - (string) $this->getStyle()->getNumberFormat()->getFormatCode(true) + SharedDate::setExcelCalendar( + $this->getWorksheet() + ->getParent() + ?->getExcelCalendar() ); - SharedDate::setExcelCalendar($currentCalendar); + + try { + $formattedValue = NumberFormat::toFormattedString( + $this->getCalculatedValueString(), + (string) $this->getStyle() + ->getNumberFormat() + ->getFormatCode(true) + ); + } finally { + SharedDate::setExcelCalendar($currentCalendar); + } return $formattedValue; } @@ -1071,4 +1081,96 @@ class Cell implements Stringable return $hidden !== Protection::PROTECTION_UNPROTECTED; } + + /** + * Return cell $right positions to the right of this one. + */ + public function cursorRight(int $right = 1): self + { + $row = $this->getRow(); + $col = $this->getColumn(); + $colIndex = Coordinate::columnIndexFromString($col); + $newCol = max( + 1, + min($colIndex + $right, AddressRange::MAX_COLUMN_INT) + ); + $newColStr = Coordinate::stringFromColumnIndex($newCol); + + return $this->getWorksheet()->getCell("$newColStr$row"); + } + + /** + * Return cell $left positions to the left of this one. + */ + public function cursorLeft(int $left = 1): self + { + return $this->cursorRight(-$left); + } + + /** + * Return cell $down positions below this one. + */ + public function cursorDown(int $down = 1): self + { + $row = $this->getRow(); + $col = $this->getColumn(); + $newRow = max( + 1, + min($row + $down, AddressRange::MAX_ROW) + ); + + return $this->getWorksheet()->getCell("$col$newRow"); + } + + /** + * Return cell $up positions above this one. + */ + public function cursorUp(int $up = 1): self + { + return $this->cursorDown(-$up); + } + + /** + * Return cell at row $row in current column. + */ + public function cursorRow(int $row = 1): self + { + $col = $this->getColumn(); + $newRow = max( + 1, + min($row, AddressRange::MAX_ROW) + ); + + return $this->getWorksheet()->getCell("$col$newRow"); + } + + /** + * Return cell at column $column in current row. + */ + public function cursorColumn(string $column = 'A'): self + { + $row = $this->getRow(); + $colIndex = Coordinate::columnIndexFromString($column); + $newCol = max( + 1, + min($colIndex, AddressRange::MAX_COLUMN_INT) + ); + $newColStr = Coordinate::stringFromColumnIndex($newCol); + + return $this->getWorksheet()->getCell("$newColStr$row"); + } + + /** + * Return cell adjusted for Xls limits if applicable. + */ + public function cursorXlsLimits(): self + { + $row = min($this->getRow(), AddressRange::MAX_ROW_XLS); + $column = $this->getColumn(); + $colIndex = Coordinate::columnIndexFromString($column); + $newCol = min($colIndex, AddressRange::MAX_COLUMN_INT_XLS); + $newColStr = Coordinate::stringFromColumnIndex($newCol); + + return $this->getWorksheet()->getCell("$newColStr$row"); + } } diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index f7daa9d9b..038c4b065 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -745,7 +745,7 @@ abstract class Coordinate } } - if ($rowStart !== null) { // @phpstan-ignore-line + if ($rowStart !== null) { if ($rowStart == $rowEnd) { $ranges[] = $hashedValue->col . $rowStart; } else { diff --git a/src/PhpSpreadsheet/Cell/IgnoredErrors.php b/src/PhpSpreadsheet/Cell/IgnoredErrors.php index 0d88fe342..37b4de476 100644 --- a/src/PhpSpreadsheet/Cell/IgnoredErrors.php +++ b/src/PhpSpreadsheet/Cell/IgnoredErrors.php @@ -14,6 +14,8 @@ class IgnoredErrors private bool $evalError = false; + private bool $misleadingFormat = false; + public function setNumberStoredAsText(bool $value): self { $this->numberStoredAsText = $value; @@ -73,4 +75,16 @@ class IgnoredErrors { return $this->evalError; } + + public function setMisleadingFormat(bool $value): self + { + $this->misleadingFormat = $value; + + return $this; + } + + public function getMisleadingFormat(): bool + { + return $this->misleadingFormat; + } } diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index e750edc21..6288ea1d7 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -110,6 +110,37 @@ class Chart private bool $roundedCorners = false; + private bool $date1904 = false; + + private string $lang = 'en-GB'; + + /** @var array{ + * b?: numeric-string, + * l?: numeric-string, + * r?: numeric-string, + * t?: numeric-string, + * header?: numeric-string, + * footer?: numeric-string, + * } + */ + private array $pageMargins = [ + 'b' => '0.75', + 'l' => '0.7', + 'r' => '0.7', + 't' => '0.75', + 'header' => '0.3', + 'footer' => '0.3', + ]; + + /** @var array{ + * paperSize?: string, + * orientation?: string, + * } + */ + private array $pageSetup = [ + 'orientation' => 'portrait', + ]; + private GridLines $borderLines; private ChartColor $fillColor; @@ -680,9 +711,11 @@ class Chart return $this->autoTitleDeleted; } - public function setAutoTitleDeleted(bool $autoTitleDeleted): self + public function setAutoTitleDeleted(?bool $autoTitleDeleted): self { - $this->autoTitleDeleted = $autoTitleDeleted; + if (is_bool($autoTitleDeleted)) { + $this->autoTitleDeleted = $autoTitleDeleted; + } return $this; } @@ -725,6 +758,34 @@ class Chart return $this; } + public function getDate1904(): bool + { + return $this->date1904; + } + + public function setDate1904(?bool $date1904): self + { + if ($date1904 !== null) { + $this->date1904 = $date1904; + } + + return $this; + } + + public function getLang(): string + { + return $this->lang; + } + + public function setLang(?string $lang): self + { + if ($lang !== null && $lang !== '') { + $this->lang = $lang; + } + + return $this; + } + public function getBorderLines(): GridLines { return $this->borderLines; @@ -782,4 +843,58 @@ class Chart $this->borderLines = clone $this->borderLines; $this->fillColor = clone $this->fillColor; } + + /** @return array{ + * b?: numeric-string, + * l?: numeric-string, + * r?: numeric-string, + * t?: numeric-string, + * header?: numeric-string, + * footer?: numeric-string, + * } + */ + public function getPageMargins(): array + { + return $this->pageMargins; + } + + /** @param mixed $pageMargins expecting array matching $this->pageMargins */ + public function setPageMargins(mixed $pageMargins): self + { + if (is_array($pageMargins)) { + foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) { + $value = $pageMargins[$key] ?? null; + if (is_string($value) && is_numeric($value)) { + $this->pageMargins[$key] = "$value"; + } + } + } + + return $this; + } + + /** @return array{ + * paperSize?: string, + * orientation?: string, + * } + */ + public function getPageSetup(): array + { + return $this->pageSetup; + } + + /** @param mixed $pageSetup expecting array matching $this->pageSetup */ + public function setPageSetup(mixed $pageSetup): self + { + if (is_array($pageSetup)) { + foreach (['paperSize', 'orientation'] as $key) { + $value = $pageSetup[$key] ?? null; + if (is_string($value)) { + $this->pageSetup[$key] = "$value"; + } + } + } + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/DataTable.php b/src/PhpSpreadsheet/Chart/DataTable.php new file mode 100644 index 000000000..71c18fe7c --- /dev/null +++ b/src/PhpSpreadsheet/Chart/DataTable.php @@ -0,0 +1,62 @@ +showHorizontalBorder; + } + + public function getShowVerticalBorder(): bool + { + return $this->showVerticalBorder; + } + + public function getShowOutline(): bool + { + return $this->showOutline; + } + + public function getShowKeys(): bool + { + return $this->showKeys; + } + + public function setShowHorizontalBorder(bool $showHorizontalBorder): self + { + $this->showHorizontalBorder = $showHorizontalBorder; + + return $this; + } + + public function setShowVerticalBorder(bool $showVerticalBorder): self + { + $this->showVerticalBorder = $showVerticalBorder; + + return $this; + } + + public function setShowOutline(bool $showOutline): self + { + $this->showOutline = $showOutline; + + return $this; + } + + public function setShowKeys(bool $showKeys): self + { + $this->showKeys = $showKeys; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Chart/Layout.php b/src/PhpSpreadsheet/Chart/Layout.php index 95e113d5e..dbf9f75fe 100644 --- a/src/PhpSpreadsheet/Chart/Layout.php +++ b/src/PhpSpreadsheet/Chart/Layout.php @@ -99,6 +99,26 @@ class Layout private ?Properties $labelEffects = null; + /** @var array{ + * vertOverflow?: string, + * horzOverflow?: string, + * wrap?: string, + * lIns?: numeric-string, + * tIns?: numeric-string, + * rIns?: numeric-string, + * bIns?: numeric-string, + * anchor?: string, + * } + */ + private array $bodyPr = [ + 'wrap' => 'square', + 'lIns' => '38100', + 'tIns' => '19050', + 'rIns' => '38100', + 'bIns' => '19050', + 'anchor' => 'ctr', + ]; + /** * Create a new Layout. * @@ -106,7 +126,22 @@ class Layout */ public function __construct(array $layout = []) { - /** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */ + /** @var array{ + * layoutTarget?: string, + * xMode?: string, + * yMode?: string, + * x?: float, + * y?: float, + * w?:float, + * h?:float, + * dLblPos?: string, + * labelFont?: ?mixed, + * labelFontColor?: ?mixed, + * labelEffects?: ?mixed, + * numFmtCode?: string, + * bodyPr?: mixed, + * } $layout + */ if (isset($layout['layoutTarget'])) { $this->layoutTarget = $layout['layoutTarget']; } @@ -156,6 +191,10 @@ class Layout if ($labelEffects instanceof Properties) { $this->labelEffects = $labelEffects; } + $bodyPr = $layout['bodyPr'] ?? null; + if (is_array($bodyPr)) { + $this->setBodyPr($bodyPr); + } } /** @param mixed[] $layout */ @@ -469,11 +508,7 @@ class Layout public function getLabelFontColor(): ?ChartColor { - if ($this->labelFont === null) { - return null; - } - - return $this->labelFont->getChartColor(); + return $this->labelFont?->getChartColor(); } public function setLabelFontColor(?ChartColor $chartColor): self @@ -523,6 +558,45 @@ class Layout return $this; } + /** @return array{ + * vertOverflow?: string, + * horzOverflow?: string, + * wrap?: string, + * lIns?: numeric-string, + * tIns?: numeric-string, + * rIns?: numeric-string, + * bIns?: numeric-string, + * anchor?: string, + * } + */ + public function getBodyPr(): array + { + return $this->bodyPr; + } + + /** + * @param mixed $bodyPr expect array matching $this->bodyPr + */ + public function setBodyPr(mixed $bodyPr): self + { + if (is_array($bodyPr)) { + foreach (['vertOverflow', 'horzOverflow', 'wrap', 'anchor'] as $key) { + $value = $bodyPr[$key] ?? null; + if (is_string($value)) { + $this->bodyPr[$key] = "$value"; + } + } + foreach (['lIns', 'tIns', 'rIns', 'bIns'] as $key) { + $value = $bodyPr[$key] ?? null; + if (is_string($value) && is_numeric($value)) { + $this->bodyPr[$key] = "$value"; + } + } + } + + return $this; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index e5e390789..05a9ce20e 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -38,6 +38,8 @@ class PlotArea */ private array $plotSeries; + private ?DataTable $dataTable = null; + /** * Create a new PlotArea. * @@ -195,6 +197,18 @@ class PlotArea return $this; } + public function setDataTable(DataTable $dataTable): self + { + $this->dataTable = $dataTable; + + return $this; + } + + public function getDataTable(): ?DataTable + { + return $this->dataTable; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/IOFactory.php b/src/PhpSpreadsheet/IOFactory.php index 7444aa879..7e4010f66 100644 --- a/src/PhpSpreadsheet/IOFactory.php +++ b/src/PhpSpreadsheet/IOFactory.php @@ -265,8 +265,7 @@ abstract class IOFactory */ public static function registerWriter(string $writerType, string $writerClass): void { - // We want phpstan to validate caller, but still need this test - if (!is_a($writerClass, IWriter::class, true)) { //* @phpstan-ignore-line + if (!is_a($writerClass, IWriter::class, true)) { throw new Writer\Exception('Registered writers must implement ' . IWriter::class); } @@ -280,8 +279,7 @@ abstract class IOFactory */ public static function registerReader(string $readerType, string $readerClass): void { - // We want phpstan to validate caller, but still need this test - if (!is_a($readerClass, IReader::class, true)) { //* @phpstan-ignore-line + if (!is_a($readerClass, IReader::class, true)) { throw new Reader\Exception('Registered readers must implement ' . IReader::class); } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 85f8404bd..0e881b28d 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -311,7 +311,7 @@ class Gnumeric extends BaseReader $column = Coordinate::stringFromColumnIndex($column + 1); // Read cell? - if (!$this->getReadFilter()->readCell($column, $row, $worksheetName)) { + if (!$this->readFilter->readCell($column, $row, $worksheetName)) { continue; } diff --git a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php index 85ff4e8ad..0729f2ef2 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric/Styles.php @@ -130,14 +130,10 @@ class Styles $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_BOTH; } elseif (isset($srssb->Diagonal)) { - // No idea what Phpstan objects to, - // especially since it has no problem with identical - // statement just above - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); // @phpstan-ignore-line + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->Diagonal->attributes()); $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_UP; } elseif (isset($srssb->{'Rev-Diagonal'})) { - // No idea what Phpstan objects to - $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); // @phpstan-ignore-line + $styleArray['borders']['diagonal'] = self::parseBorderAttributes($srssb->{'Rev-Diagonal'}->attributes()); $styleArray['borders']['diagonalDirection'] = Borders::DIAGONAL_DOWN; } } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 818753a79..4ddbae090 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -851,7 +851,7 @@ class Ods extends BaseReader // repeated range passes the read filter. If not, skip the entire group. // If some columns pass, we need to fall through to the processing block // which will handle per-column filtering. - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) { if ($colRepeats <= 1) { StringHelper::stringIncrement($columnID); @@ -865,7 +865,7 @@ class Ods extends BaseReader if ($i > 0) { StringHelper::stringIncrement($tempCol); } - if ($this->getReadFilter()->readCell($tempCol, $rowID, $worksheetName)) { + if ($this->readFilter->readCell($tempCol, $rowID, $worksheetName)) { $anyColumnPasses = true; break; @@ -1143,7 +1143,7 @@ class Ods extends BaseReader StringHelper::stringIncrement($columnID); } - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) { continue; } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index b7d34bc28..a79c66269 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -2728,7 +2728,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($column + 1); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset: 4; size: 2; index to XF record $xfIndex = self::getUInt2d($recordData, 4); @@ -2773,7 +2773,7 @@ class Xls extends XlsBase $cell = null; // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset: 4; size: 2; index to XF record $xfIndex = self::getUInt2d($recordData, 4); @@ -2874,7 +2874,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($colFirst + $i); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset: var; size: 2; index to XF record $xfIndex = self::getUInt2d($recordData, $offset); @@ -2918,7 +2918,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($column + 1); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset 4; size: 2; index to XF record $xfIndex = self::getUInt2d($recordData, 4); @@ -2984,7 +2984,7 @@ class Xls extends XlsBase } // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { if ($isPartOfSharedFormula) { // formula is added to this cell after the sheet has been read $this->sharedFormulaParts[$columnString . ($row + 1)] = $this->baseCell; @@ -3150,7 +3150,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($column + 1); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset: 4; size: 2; index to XF record $xfIndex = self::getUInt2d($recordData, 4); @@ -3214,7 +3214,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($fc + $i + 1); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { $xfIndex = self::getUInt2d($recordData, 4 + 2 * $i); if (isset($this->mapCellXfIndex[$xfIndex])) { $this->phpSheet->getCell($columnString . ($row + 1))->setXfIndex($this->mapCellXfIndex[$xfIndex]); @@ -3252,7 +3252,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($column + 1); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset: 4; size: 2; XF index $xfIndex = self::getUInt2d($recordData, 4); @@ -3297,7 +3297,7 @@ class Xls extends XlsBase $columnString = Coordinate::stringFromColumnIndex($col + 1); // Read cell? - if ($this->getReadFilter()->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($columnString, $row + 1, $this->phpSheet->getTitle())) { // offset: 4; size: 2; XF index $xfIndex = self::getUInt2d($recordData, 4); @@ -3605,7 +3605,7 @@ class Xls extends XlsBase StringHelper::stringIncrement($rangeBoundaries[1][0]); for ($row = $rangeBoundaries[0][1]; $row <= $rangeBoundaries[1][1]; ++$row) { for ($column = $rangeBoundaries[0][0]; $column != $rangeBoundaries[1][0]; StringHelper::stringIncrement($column)) { - if ($this->getReadFilter()->readCell($column, $row, $this->phpSheet->getTitle())) { + if ($this->readFilter->readCell($column, $row, $this->phpSheet->getTitle())) { $includeCellRange = true; break 2; diff --git a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php index 402b9f59d..ebe9d515f 100644 --- a/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php +++ b/src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php @@ -551,7 +551,7 @@ class LoadSpreadsheet extends Xls /** @var int $row */ [$column, $row] = Coordinate::coordinateFromString($cell); /** @var string $baseCell */ - if ($xls->getReadFilter()->readCell($column, $row, $xls->phpSheet->getTitle())) { + if ($xls->readFilter->readCell($column, $row, $xls->phpSheet->getTitle())) { /** @var string */ $temp = $xls->sharedFormulas[$baseCell]; $formula = $xls->getFormulaFromStructure($temp, $cell); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 4aa55b999..0c0199676 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -890,7 +890,7 @@ class Xlsx extends BaseReader $sheetViewOptions->load($this->readDataOnly, $this->styleReader); (new ColumnAndRowAttributes($docSheet, $xmlSheetNS)) - ->load($this->getReadFilter(), $this->readDataOnly, $this->ignoreRowsWithNoCells); + ->load($this->readFilter, $this->readDataOnly, $this->ignoreRowsWithNoCells); } $holdSelectedCells = $docSheet->getSelectedCells(); @@ -1931,7 +1931,7 @@ class Xlsx extends BaseReader // Read cell? $coordinates = Coordinate::coordinateFromString($r); - if (!$this->getReadFilter()->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) { + if (!$this->readFilter->readCell($coordinates[0], (int) $coordinates[1], $docSheet->getTitle())) { // Normally, just testing for the f attribute should identify this cell as containing a formula // that we need to read, even though it is outside of the filter range, in case it is a shared formula. // But in some cases, this attribute isn't set; so we need to delve a level deeper and look at @@ -2403,7 +2403,7 @@ class Xlsx extends BaseReader { $returnValue = null; $protectKey = $protection[$key]; - if (!empty($protectKey)) { + if (isset($protectKey)) { $protectKey = (string) $protectKey; $returnValue = $protectKey !== 'false' && (bool) $protectKey; } @@ -2661,6 +2661,8 @@ class Xlsx extends BaseReader $formulaRange = (string) ($attributes['formulaRange'] ?? ''); $twoDigitTextYear = (string) ($attributes['twoDigitTextYear'] ?? ''); $evalError = (string) ($attributes['evalError'] ?? ''); + $attributes2 = self::getAttributes($xml, Namespaces::MISLEADING_FORMAT); + $misleadingFormat = (string) ($attributes2['misleadingFormat'] ?? ''); if (!empty($sqref)) { $explodedSqref = explode(' ', $sqref); $pattern1 = '/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/'; @@ -2682,19 +2684,34 @@ class Xlsx extends BaseReader continue; } if ($numberStoredAsText === '1') { - $sheet->getCell("$col$row")->getIgnoredErrors()->setNumberStoredAsText(true); + $sheet->getCell("$col$row") + ->getIgnoredErrors() + ->setNumberStoredAsText(true); } if ($formula === '1') { - $sheet->getCell("$col$row")->getIgnoredErrors()->setFormula(true); + $sheet->getCell("$col$row") + ->getIgnoredErrors() + ->setFormula(true); } if ($formulaRange === '1') { - $sheet->getCell("$col$row")->getIgnoredErrors()->setFormulaRange(true); + $sheet->getCell("$col$row") + ->getIgnoredErrors() + ->setFormulaRange(true); } if ($twoDigitTextYear === '1') { - $sheet->getCell("$col$row")->getIgnoredErrors()->setTwoDigitTextYear(true); + $sheet->getCell("$col$row") + ->getIgnoredErrors() + ->setTwoDigitTextYear(true); } if ($evalError === '1') { - $sheet->getCell("$col$row")->getIgnoredErrors()->setEvalError(true); + $sheet->getCell("$col$row") + ->getIgnoredErrors() + ->setEvalError(true); + } + if ($misleadingFormat === '1') { + $sheet->getCell("$col$row") + ->getIgnoredErrors() + ->setMisleadingFormat(true); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 4ae3fd4e2..d00a7aed0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Chart\AxisText; use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\DataTable; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; @@ -91,13 +92,26 @@ class Chart $chartFillColor = null; $gradientArray = []; $gradientLin = null; - $roundedCorners = false; + $roundedCorners = null; + $date1904 = null; + $lang = null; $gapWidth = null; $useUpBars = null; $useDownBars = null; $noBorder = false; + $pageMargins = []; + $pageSetup = []; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'printSettings': + if (isset($chartElement->pageMargins)) { + $pageMargins = current((array) $chartElement->pageMargins->attributes()); + } + if (isset($chartElement->pageSetup)) { + $pageSetup = current((array) $chartElement->pageSetup->attributes()); + } + + break; case 'spPr': $children = $chartElementsC->spPr->children($this->aNamespace); if (isset($children->noFill)) { @@ -116,16 +130,22 @@ class Chart break; case 'roundedCorners': - /** @var bool $roundedCorners */ $roundedCorners = self::getAttributeBoolean($chartElementsC->roundedCorners, 'val'); + break; + case 'date1904': + $date1904 = self::getAttributeBoolean($chartElementsC->date1904, 'val'); + + break; + case 'lang': + $lang = self::getAttributeString($chartElementsC->lang, 'val'); + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { $chartDetails = Xlsx::testSimpleXml($chartDetails); switch ($chartDetailsKey) { case 'autoTitleDeleted': - /** @var bool $autoTitleDeleted */ $autoTitleDeleted = self::getAttributeBoolean($chartElementsC->chart->autoTitleDeleted, 'val'); break; @@ -137,7 +157,7 @@ class Chart break; case 'plotArea': - $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; + $plotAreaLayout = $XaxisLabel = $YaxisLabel = $dataTable = null; $plotSeries = $plotAttributes = []; $catAxRead = false; $plotNoFill = false; @@ -358,6 +378,10 @@ class Chart } $plotAttributes = $this->readChartAttributes($chartDetail); + break; + case 'dTable': + $dataTable = $this->readDataTable($chartDetail); + break; } } @@ -365,6 +389,11 @@ class Chart $plotAreaLayout = new Layout(); } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); + if ($dataTable !== null) { + $plotArea->setDataTable( + $dataTable + ); + } $this->setChartAttributes($plotAreaLayout, $plotAttributes); if ($plotNoFill) { $plotArea->setNoFill(true); @@ -472,23 +501,18 @@ class Chart if ($chartBorderLines !== null) { $chart->setBorderLines($chartBorderLines); } - $chart->setNoBorder($noBorder); - $chart->setRoundedCorners($roundedCorners); - if (is_bool($autoTitleDeleted)) { - $chart->setAutoTitleDeleted($autoTitleDeleted); - } - if (is_int($rotX)) { - $chart->setRotX($rotX); - } - if (is_int($rotY)) { - $chart->setRotY($rotY); - } - if (is_int($rAngAx)) { - $chart->setRAngAx($rAngAx); - } - if (is_int($perspective)) { - $chart->setPerspective($perspective); - } + $chart + ->setNoBorder($noBorder) + ->setRoundedCorners($roundedCorners) + ->setDate1904($date1904) + ->setLang($lang) + ->setPageMargins($pageMargins) + ->setPageSetup($pageSetup) + ->setAutoTitleDeleted($autoTitleDeleted) + ->setRotX($rotX) + ->setRotY($rotY) + ->setRAngAx($rAngAx) + ->setPerspective($perspective); return $chart; } @@ -1289,12 +1313,38 @@ class Chart $plotAttributes['labelEffects'] = $labelEffects; } } + if (isset($txpr->bodyPr)) { + $plotAttributes['bodyPr'] = current((array) $txpr->bodyPr->attributes()); + } } } return $plotAttributes; } + private function readDataTable(SimpleXMLElement $chartDetail): DataTable + { + $dataTable = new DataTable(); + $temp = self::getAttributeBoolean($chartDetail->showHorzBorder, 'val'); + if ($temp !== null) { + $dataTable->setShowHorizontalBorder($temp); + } + $temp = self::getAttributeBoolean($chartDetail->showVertBorder, 'val'); + if ($temp !== null) { + $dataTable->setShowVerticalBorder($temp); + } + $temp = self::getAttributeBoolean($chartDetail->showOutline, 'val'); + if ($temp !== null) { + $dataTable->setShowOutline($temp); + } + $temp = self::getAttributeBoolean($chartDetail->showKeys, 'val'); + if ($temp !== null) { + $dataTable->setShowKeys($temp); + } + + return $dataTable; + } + /** @param array $plotAttributes */ private function setChartAttributes(Layout $plotArea, array $plotAttributes): void { @@ -1333,6 +1383,11 @@ class Chart /** @var ?Font $plotAttributeValue */ $plotArea->setLabelFont($plotAttributeValue); + break; + case 'bodyPr': + /** @var mixed $plotAttributeValue */ + $plotArea->setBodyPr($plotAttributeValue); + break; } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index 5d94f2fd4..1d51acc71 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -133,4 +133,6 @@ class Namespaces const FEATURE_PROPERTY_BAG = 'http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag'; const RELATIONSHIPS_FEATURE_PROPERTY_BAG = 'http://schemas.microsoft.com/office/2022/11/relationships/FeaturePropertyBag'; const STYLE_CHECKBOX_URI = '{C7286773-470A-42A8-94C5-96B5CB345126}'; + + const MISLEADING_FORMAT = 'http://schemas.microsoft.com/office/spreadsheetml/2018/08/main'; } diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 4fba7dcd7..f5cdc8108 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -432,7 +432,7 @@ class Xml extends BaseReader $arrayRef = AddressHelper::convertFormulaToA1($arrayRange, $rowID, $columnIndex); } - if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { + if (!$this->readFilter->readCell($columnID, $rowID, $worksheetName)) { continue; } diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 9c2beaa03..7759d9250 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -52,8 +52,7 @@ class Settings */ public static function setChartRenderer(string $rendererClassName): void { - // We want phpstan to validate caller, but still need this test - if (!is_a($rendererClassName, IRenderer::class, true)) { //* @phpstan-ignore-line + if (!is_a($rendererClassName, IRenderer::class, true)) { throw new Exception('Chart renderer must implement ' . IRenderer::class); } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 2e2df9ada..fbb48c927 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -160,7 +160,7 @@ class Date * serialized timestamp. * See https://en.wikipedia.org/wiki/ISO_8601 for details of the ISO-8601 standard format. */ - public static function convertIsoDate(mixed $value): float|int + public static function convertIsoDate(mixed $value, ?int $calendar = null): float|int { if (!is_string($value)) { throw new Exception('Non-string value supplied for Iso Date conversion'); @@ -173,7 +173,7 @@ class Date throw new Exception("Invalid string $value supplied for datatype Date"); } - $newValue = self::dateTimeToExcel($date); + $newValue = self::dateTimeToExcel($date, $calendar); if (preg_match('/^\s*\d?\d:\d\d(:\d\d([.]\d+)?)?\s*(am|pm)?\s*$/i', $value) == 1) { $newValue = fmod($newValue, 1.0); @@ -194,16 +194,17 @@ class Date * * @return DateTime PHP date/time object */ - public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null): DateTime + public static function excelToDateTimeObject(float|int $excelTimestamp, null|DateTimeZone|string $timeZone = null, ?int $calendar = null): DateTime { + $calendar ??= self::$excelCalendar; $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone); if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) { + if ($excelTimestamp < 1 && $calendar === self::CALENDAR_WINDOWS_1900) { // Unix timestamp base date $baseDate = new DateTime('1970-01-01', $timeZone); } else { // MS Excel calendar base dates - if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { + if ($calendar == self::CALENDAR_WINDOWS_1900) { // Allow adjustment for 1900 Leap Year in MS Excel $baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone); } else { @@ -252,9 +253,9 @@ class Date * * @return int Unix timetamp for this date/time */ - public static function excelToTimestamp($excelTimestamp, $timeZone = null): int + public static function excelToTimestamp($excelTimestamp, $timeZone = null, ?int $calendar = null): int { - $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone); + $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone, $calendar); self::roundMicroseconds($dto); return (int) $dto->format('U'); @@ -269,14 +270,16 @@ class Date * @return false|float Excel date/time value * or boolean FALSE on failure */ - public static function PHPToExcel(mixed $dateValue) + public static function PHPToExcel(mixed $dateValue, ?int $calendar = null) { if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) { - return self::dateTimeToExcel($dateValue); - } elseif (is_numeric($dateValue)) { - return self::timestampToExcel($dateValue); - } elseif (is_string($dateValue)) { - return self::stringToExcel($dateValue); + return self::dateTimeToExcel($dateValue, $calendar); + } + if (is_numeric($dateValue)) { + return self::timestampToExcel($dateValue, $calendar); + } + if (is_string($dateValue)) { + return self::stringToExcel($dateValue, $calendar); } return false; @@ -289,7 +292,7 @@ class Date * * @return float MS Excel serialized date/time value */ - public static function dateTimeToExcel(DateTimeInterface $dateValue): float + public static function dateTimeToExcel(DateTimeInterface $dateValue, ?int $calendar = null): float { $seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u')); @@ -299,7 +302,8 @@ class Date (int) $dateValue->format('d'), (int) $dateValue->format('H'), (int) $dateValue->format('i'), - $seconds + $seconds, + $calendar ); } @@ -312,13 +316,13 @@ class Date * * @return false|float MS Excel serialized date/time value */ - public static function timestampToExcel($unixTimestamp): bool|float + public static function timestampToExcel($unixTimestamp, ?int $calendar = null): bool|float { if (!is_numeric($unixTimestamp)) { return false; } - return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp)); + return self::dateTimeToExcel(new DateTime('@' . $unixTimestamp), $calendar); } /** @@ -326,9 +330,10 @@ class Date * * @return float Excel date/time value */ - public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0): float + public static function formattedPHPToExcel(int $year, int $month, int $day, int $hours = 0, int $minutes = 0, float|int $seconds = 0, ?int $calendar = null): float { - if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { + $calendar ??= self::$excelCalendar; + if ($calendar === self::CALENDAR_WINDOWS_1900) { // // Fudge factor for the erroneous fact that the year 1900 is treated as a Leap Year in MS Excel // This affects every date following 28th February 1900 @@ -474,7 +479,7 @@ class Date * * @return false|float Excel date/time serial value */ - public static function stringToExcel(string $dateValue): bool|float + public static function stringToExcel(string $dateValue, ?int $calendar = null): bool|float { if (strlen($dateValue) < 2) { return false; @@ -483,7 +488,7 @@ class Date return false; } - $dateValueNew = DateTimeExcel\DateValue::fromString($dateValue); + $dateValueNew = DateTimeExcel\DateValue::fromString2($dateValue, $calendar); if (!is_float($dateValueNew)) { return false; diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php index 435e6a60d..6d23b9d92 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php @@ -192,8 +192,7 @@ class CellValue extends WizardAbstract implements WizardInterface $retVal = true; $array = array_merge(array_keys(self::SINGLE_OPERATORS), array_keys(self::RANGE_OPERATORS)); foreach ($array as $value) { - // PhpStan is correct about next statement, but we want to test anyhow - $retVal = $retVal && in_array($value, self::MAGIC_OPERATIONS, true); // @phpstan-ignore-line + $retVal = $retVal && in_array($value, self::MAGIC_OPERATIONS, true); } return $retVal; diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php index 934a364ab..c1657666d 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php @@ -169,8 +169,7 @@ class TextValue extends WizardAbstract implements WizardInterface $retVal = true; $array = array_keys(self::OPERATORS); foreach ($array as $value) { - // PhpStan is correct about next statement, but we want to test anyhow - $retVal = $retVal && in_array($value, self::MAGIC_OPERATIONS, true); // @phpstan-ignore-line + $retVal = $retVal && in_array($value, self::MAGIC_OPERATIONS, true); } return $retVal; diff --git a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php index f869d0736..7dcf81f58 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php @@ -55,10 +55,8 @@ class NumberFormatter extends BaseFormatter $number = floor($numberFloat / $divisor); $mask = substr_replace($mask, $blockValue, $offset, $size); } - /** @var string $numberString */ - $numberString = $number; if ($number > 0) { - $mask = substr_replace($mask, $numberString, $offset, 0); + $mask = substr_replace($mask, "$number", $offset, 0); } $result = $mask; } diff --git a/src/PhpSpreadsheet/Style/Style.php b/src/PhpSpreadsheet/Style/Style.php index 9956c358d..9df62897a 100644 --- a/src/PhpSpreadsheet/Style/Style.php +++ b/src/PhpSpreadsheet/Style/Style.php @@ -201,6 +201,18 @@ class Style extends Supervisor { if ($this->isSupervisor) { $pRange = $this->getSelectedCells(); + if (str_contains($pRange, ',')) { + $ranges = explode(',', $pRange); + $sheet = $this->getActiveSheet(); + foreach ($ranges as $range) { + $sheet + ->setSelectedCells(trim($range)); + $this->applyFromArray($styleArray, $advancedBorders); + } + $sheet->setSelectedCells($pRange); + + return $this; + } // Uppercase coordinate and strip any Worksheet reference from the selected range $pRange = strtoupper($pRange); diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 04070e5d9..ad7330252 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use Composer\Pcre\Preg; use DateTime; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; @@ -146,7 +147,7 @@ class AutoFilter implements Stringable $this->evaluated = false; if ($this->workSheet !== null) { $thisrange = $this->range; - $range = (string) preg_replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); + $range = Preg::replace('/\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } @@ -239,12 +240,10 @@ class AutoFilter implements Stringable public function setColumn(AutoFilter\Column|string $columnObjectOrString): static { $this->evaluated = false; - if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) { + if (is_string($columnObjectOrString)) { $column = $columnObjectOrString; - } elseif ($columnObjectOrString instanceof AutoFilter\Column) { - $column = $columnObjectOrString->getColumnIndex(); } else { - throw new Exception('Column is not within the autofilter range.'); + $column = $columnObjectOrString->getColumnIndex(); } $this->testColumnInRange($column); @@ -434,32 +433,15 @@ class AutoFilter implements Stringable }; } else { // String values are always tested for equality, factoring in for wildcards (hence a regexp test) - switch ($ruleOperator) { - case Rule::AUTOFILTER_COLUMN_RULE_EQUAL: - $retVal = (bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString); - - break; - case Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL: - $retVal = !((bool) preg_match('/^' . $ruleValue . '$/i', $cellValueString)); - - break; - case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN: - $retVal = strcasecmp($cellValueString, $ruleValue) > 0; - - break; - case Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL: - $retVal = strcasecmp($cellValueString, $ruleValue) >= 0; - - break; - case Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN: - $retVal = strcasecmp($cellValueString, $ruleValue) < 0; - - break; - case Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL: - $retVal = strcasecmp($cellValueString, $ruleValue) <= 0; - - break; - } + $retVal = match ($ruleOperator) { + Rule::AUTOFILTER_COLUMN_RULE_EQUAL => Preg::isMatch('/^' . $ruleValue . '$/i', $cellValueString), + Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL => !(Preg::isMatch('/^' . $ruleValue . '$/i', $cellValueString)), + Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN => strcasecmp($cellValueString, $ruleValue) > 0, + Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL => strcasecmp($cellValueString, $ruleValue) >= 0, + Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN => strcasecmp($cellValueString, $ruleValue) < 0, + // Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL + default => strcasecmp($cellValueString, $ruleValue) <= 0, + }; } // If there are multiple conditions, then we need to test both using the appropriate join operator switch ($join) { @@ -752,14 +734,11 @@ class AutoFilter implements Stringable private function dynamicFilterDateRange(string $dynamicRuleType, AutoFilter\Column &$filterColumn): array { $ruleValues = []; - $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType]]; // What if not found? + $callBack = [__CLASS__, self::DATE_FUNCTIONS[$dynamicRuleType] ?? throw new Exception("invalid dynamic rule type $dynamicRuleType")]; // Calculate start/end dates for the required date range based on current date // Val is lowest permitted value. // Maxval is greater than highest permitted value - $val = $maxval = 0; - if (is_callable($callBack)) { //* @phpstan-ignore-line - [$val, $maxval] = $callBack(); - } + [$val, $maxval] = $callBack(); $val = Date::dateTimeToExcel($val); $maxval = Date::dateTimeToExcel($maxval); @@ -1087,26 +1066,16 @@ class AutoFilter implements Stringable */ public function __clone() { - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if (is_object($value)) { - if ($key === 'workSheet') { - // Detach from worksheet - $this->{$key} = null; - } else { - $this->{$key} = clone $value; - } - } elseif ((is_array($value)) && ($key == 'columns')) { - // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\AutoFilter objects - $this->{$key} = []; - foreach ($value as $k => $v) { - $this->{$key}[$k] = clone $v; //* @phpstan-ignore-line - // attach the new cloned Column to this new cloned Autofilter object - $this->{$key}[$k]->setParent($this); //* @phpstan-ignore-line - } - } else { - $this->{$key} = $value; - } + // workSheet is only property which is an object + // detach from worksheet + $this->workSheet = null; + // columns is the only property which is an array + $columns = $this->columns; + $this->columns = []; + foreach ($columns as $k => $v) { + $this->columns[$k] = clone $v; + // attach the new cloned Column to this new cloned Autofilter object + $this->columns[$k]->setParent($this); } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 5c26e2dd8..eb79c994d 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -360,9 +360,12 @@ class Worksheet /** * Disconnect all cells from this Worksheet object, * typically so that the worksheet object can be unset. + * The worksheet will be in an unusable state after + * this method has completed. */ public function disconnectCells(): void { + // isset needed to avoid problems at destruct time if (isset($this->cellCollection)) { //* @phpstan-ignore-line $this->cellCollection->unsetWorksheetCells(); unset($this->cellCollection); @@ -462,6 +465,7 @@ class Worksheet */ public function getCoordinates(bool $sorted = true): array { + // isset needed to avoid problems at destruct time if (!isset($this->cellCollection)) { //* @phpstan-ignore-line return []; } @@ -796,8 +800,9 @@ class Worksheet $this->activePane = $holdActivePane; } if ($activeSheet !== null && $activeSheet >= 0) { - // Not sure what PhpStan doesn't like about next stmt - $this->getParent()?->setActiveSheetIndex($activeSheet); // @phpstan-ignore-line + // Okay, I get it now - if $activeSheet is not null, + // then $this->getParent() must also be non-null. + $this->getParent()->setActiveSheetIndex($activeSheet); } $this->setSelectedCells($selectedCells); @@ -1234,8 +1239,7 @@ class Worksheet throw new Exception('Sheet not found for named range: ' . $namedRange->getName()); } - /** @phpstan-ignore-next-line */ - $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!'); + $cellCoordinate = ltrim(substr($namedRange->getValue(), (int) strrpos($namedRange->getValue(), '!')), '!'); $finalCoordinate = str_replace('$', '', $cellCoordinate); } } @@ -1683,7 +1687,7 @@ class Worksheet public function duplicateConditionalStyle(array $styles, string $range = ''): static { foreach ($styles as $cellStyle) { - if (!($cellStyle instanceof Conditional)) { // @phpstan-ignore-line + if (!($cellStyle instanceof Conditional)) { throw new Exception('Style is not a conditional style'); } } @@ -2443,6 +2447,18 @@ class Worksheet if ($row < 1) { throw new Exception('Rows to be deleted should at least start from row 1.'); } + if ($numberOfRows === 0) { + return $this; + } + if ($numberOfRows < 0) { + $newRow = max(1, $row + $numberOfRows + 1); + $numberOfRows = $row - $newRow + 1; + $row = $newRow; + } + $newHighestRow = $this->cachedHighestRow; + if ($newHighestRow >= $row) { + $newHighestRow = max($row - 1, $this->cachedHighestRow - $numberOfRows); + } $startRow = $row; $endRow = $startRow + $numberOfRows - 1; $removeKeys = []; @@ -2499,6 +2515,7 @@ class Worksheet } $this->rowDimensions = $holdRowDimensions; + $this->cachedHighestRow = $newHighestRow; return $this; } @@ -2537,6 +2554,19 @@ class Worksheet throw new Exception('Column references should not be numeric.'); } $startColumnInt = Coordinate::columnIndexFromString($column); + if ($numberOfColumns === 0) { + return $this; + } + if ($numberOfColumns < 0) { + $newStartColumnInt = max(1, $startColumnInt + $numberOfColumns + 1); + $numberOfColumns = $startColumnInt - $newStartColumnInt + 1; + $startColumnInt = $newStartColumnInt; + $column = Coordinate::stringFromColumnIndex($startColumnInt); + } + $newHighestColumn = $this->cachedHighestColumn; + if ($newHighestColumn >= $startColumnInt) { + $newHighestColumn = max($startColumnInt - 1, $this->cachedHighestColumn - $numberOfColumns); + } $endColumnInt = $startColumnInt + $numberOfColumns - 1; $removeKeys = []; $addKeys = []; @@ -2587,6 +2617,8 @@ class Worksheet $this->columnDimensions = $holdColumnDimensions; if ($pColumnIndex > $highestColumnIndex) { + $this->cachedHighestColumn = $newHighestColumn; + return $this; } @@ -2596,6 +2628,7 @@ class Worksheet $this->cellCollection->removeColumn($highestColumn); $highestColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($highestColumn) - 1); } + $this->cachedHighestColumn = $newHighestColumn; $this->garbageCollect(); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 3678cf67b..fcb8a8ddb 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -1595,7 +1595,13 @@ class Html extends BaseWriter $origData2 = $cell->getValueString(); } } - $formatCode = $worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); + $style = $worksheet->getParent() + ?->getCellXfByIndexOrNull( + $cell->getXfIndex() + ); + $formatCode = $style + ?->getNumberFormat() + ->getFormatCode(); $cellData = NumberFormat::toFormattedString( $origData2, @@ -1606,9 +1612,9 @@ class Html extends BaseWriter if ($cellData === $origData) { $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags()); } - if ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { + if (true === $style?->getFont()->getSuperscript()) { $cellData = '' . $cellData . ''; - } elseif ($worksheet->getParentOrThrow()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSubscript()) { + } elseif (true === $style?->getFont()->getSubscript()) { $cellData = '' . $cellData . ''; } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index db8c0321c..d16b42354 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Chart\Chart as SpreadsheetChart; use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\DataTable; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; @@ -52,10 +53,10 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:r', Namespaces::SCHEMA_OFFICE_DOCUMENT); $objWriter->startElement('c:date1904'); - $objWriter->writeAttribute('val', '0'); + $objWriter->writeAttribute('val', $chart->getDate1904() ? '1' : '0'); $objWriter->endElement(); $objWriter->startElement('c:lang'); - $objWriter->writeAttribute('val', 'en-GB'); + $objWriter->writeAttribute('val', $chart->getLang()); $objWriter->endElement(); $objWriter->startElement('c:roundedCorners'); $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0'); @@ -71,7 +72,6 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted()); $objWriter->endElement(); - $objWriter->startElement('c:view3D'); $surface2D = false; $plotArea = $chart->getPlotArea(); if ($plotArea !== null) { @@ -84,11 +84,14 @@ class Chart extends WriterPart } } } + $this->view3DStarted = false; $this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90); $this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D); $this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D); $this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D); - $objWriter->endElement(); // view3D + if ($this->view3DStarted) { + $objWriter->endElement(); // view3D + } $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); @@ -122,7 +125,7 @@ class Chart extends WriterPart $this->writeEffects($objWriter, $borderLines); $objWriter->endElement(); // c:spPr - $this->writePrintSettings($objWriter); + $this->writePrintSettings($objWriter, $chart); $objWriter->endElement(); // c:chartSpace @@ -130,12 +133,18 @@ class Chart extends WriterPart return $objWriter->getData(); } + private bool $view3DStarted = false; + private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void { if ($value === null && $surface2D) { $value = $default; } if ($value !== null) { + if (!$this->view3DStarted) { + $objWriter->startElement('c:view3D'); + $this->view3DStarted = true; + } $objWriter->startElement($tag); $objWriter->writeAttribute('val', "$value"); $objWriter->endElement(); @@ -304,6 +313,8 @@ class Chart extends WriterPart $objWriter->endElement(); // c:legend } + private string $chartType = ''; + /** * Write Chart Plot Area. */ @@ -327,6 +338,7 @@ class Chart extends WriterPart $chartType = null; foreach ($chartTypes as $chartType) { $objWriter->startElement('c:' . $chartType); + $this->chartType = $chartType; $groupCount = $plotArea->getPlotGroupCount(); $plotGroup = null; @@ -444,6 +456,10 @@ class Chart extends WriterPart $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } else { $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis()); + $dataTable = $plotArea->getDataTable(); + if ($dataTable !== null) { + $this->writeDataTable($objWriter, $dataTable); + } } $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis()); @@ -482,6 +498,29 @@ class Chart extends WriterPart $objWriter->endElement(); // c:plotArea } + private function writeDataTable(XMLWriter $objWriter, DataTable $dataTable): void + { + $objWriter->startElement('c:dTable'); + + $objWriter->startElement('c:showHorzBorder'); + $objWriter->writeAttribute('val', $dataTable->getShowHorizontalBorder() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:showVertBorder'); + $objWriter->writeAttribute('val', $dataTable->getShowVerticalBorder() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:showOutline'); + $objWriter->writeAttribute('val', $dataTable->getShowOutline() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->startElement('c:showKeys'); + $objWriter->writeAttribute('val', $dataTable->getShowKeys() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->endElement(); // c:dTable + } + private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void { if ($value !== null) { @@ -518,12 +557,12 @@ class Chart extends WriterPart $objWriter->startElement('c:txPr'); $objWriter->startElement('a:bodyPr'); - $objWriter->writeAttribute('wrap', 'square'); - $objWriter->writeAttribute('lIns', '38100'); - $objWriter->writeAttribute('tIns', '19050'); - $objWriter->writeAttribute('rIns', '38100'); - $objWriter->writeAttribute('bIns', '19050'); - $objWriter->writeAttribute('anchor', 'ctr'); + $bodyPr = $chartLayout->getBodyPr(); + foreach (['vertOverflow', 'horzOverflow', 'wrap', 'lIns', 'tIns', 'rIns', 'bIns', 'anchor'] as $key) { + if (isset($bodyPr[$key])) { + $objWriter->writeAttribute($key, $bodyPr[$key]); + } + } $objWriter->startElement('a:spAutoFit'); $objWriter->endElement(); // a:spAutoFit $objWriter->endElement(); // a:bodyPr @@ -1685,27 +1724,32 @@ class Chart extends WriterPart /** * Write Printer Settings. */ - private function writePrintSettings(XMLWriter $objWriter): void + private function writePrintSettings(XMLWriter $objWriter, SpreadsheetChart $chart): void { $objWriter->startElement('c:printSettings'); $objWriter->startElement('c:headerFooter'); $objWriter->endElement(); + $pageMargins = $chart->getPageMargins(); $objWriter->startElement('c:pageMargins'); - $objWriter->writeAttribute('footer', '0.3'); - $objWriter->writeAttribute('header', '0.3'); - $objWriter->writeAttribute('r', '0.7'); - $objWriter->writeAttribute('l', '0.7'); - $objWriter->writeAttribute('t', '0.75'); - $objWriter->writeAttribute('b', '0.75'); - $objWriter->endElement(); + foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) { + if (array_key_exists($key, $pageMargins)) { + $objWriter->writeAttribute($key, $pageMargins[$key]); + } + } + $objWriter->endElement(); // c:pageMargins + $pageSetup = $chart->getPageSetup(); $objWriter->startElement('c:pageSetup'); - $objWriter->writeAttribute('orientation', 'portrait'); - $objWriter->endElement(); + foreach (['paperSize', 'orientation'] as $key) { + if (array_key_exists($key, $pageSetup)) { + $objWriter->writeAttribute($key, $pageSetup[$key]); + } + } + $objWriter->endElement(); // c:pageSetup - $objWriter->endElement(); + $objWriter->endElement(); // c:printSettings } private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void @@ -1875,9 +1919,11 @@ class Chart extends WriterPart $objWriter->startElement('a:lumMod'); $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness)); $objWriter->endElement(); // a:lumMod - $objWriter->startElement('a:lumOff'); - $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); - $objWriter->endElement(); // a:lumOff + if ($this->chartType !== DataSeries::TYPE_RADARCHART) { + $objWriter->startElement('a:lumOff'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); + $objWriter->endElement(); // a:lumOff + } } $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr if ($solidFill) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 3a21c4154..4a448afcf 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -35,6 +35,8 @@ class Worksheet extends WriterPart private string $evalError = ''; + private string $misleadingFormat = ''; + private bool $explicitStyle0; private bool $useDynamicArrays = false; @@ -59,6 +61,7 @@ class Worksheet extends WriterPart $this->formulaRange = ''; $this->twoDigitTextYear = ''; $this->evalError = ''; + $this->misleadingFormat = ''; // Create XML writer $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { @@ -177,7 +180,13 @@ class Worksheet extends WriterPart } $objWriter->startElement('ignoredError'); $objWriter->writeAttribute('sqref', substr($cells, 1)); - $objWriter->writeAttribute($attr, '1'); + if ($attr === 'misleadingFormat') { + $prefix = 'x16r3'; + $objWriter->writeAttribute("xmlns:$prefix", Namespaces::MISLEADING_FORMAT); + $objWriter->writeAttribute("$prefix:$attr", '1'); + } else { + $objWriter->writeAttribute($attr, '1'); + } $objWriter->endElement(); } } @@ -190,6 +199,7 @@ class Worksheet extends WriterPart $this->writeIgnoredError($objWriter, $started, 'formulaRange', $this->formulaRange); $this->writeIgnoredError($objWriter, $started, 'twoDigitTextYear', $this->twoDigitTextYear); $this->writeIgnoredError($objWriter, $started, 'evalError', $this->evalError); + $this->writeIgnoredError($objWriter, $started, 'misleadingFormat', $this->misleadingFormat); if ($started) { $objWriter->endElement(); } @@ -1492,6 +1502,9 @@ class Worksheet extends WriterPart if ($worksheet->getCell($coord)->getIgnoredErrors()->getEvalError()) { $this->evalError .= " $coord"; } + if ($worksheet->getCell($coord)->getIgnoredErrors()->getMisleadingFormat()) { + $this->misleadingFormat .= " $coord"; + } $this->writeCell($objWriter, $worksheet, $coord, $aFlippedStringTable); } } diff --git a/tests/PhpSpreadsheetTests/Cell/CursorTest.php b/tests/PhpSpreadsheetTests/Cell/CursorTest.php new file mode 100644 index 000000000..61ca3c61a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Cell/CursorTest.php @@ -0,0 +1,67 @@ +getActiveSheet(); + $sheet->fromArray($data); + $cell = $sheet->getCell('A1')->cursorRight(); + self::assertSame('1b', $cell->getValue()); + $cell = $cell->cursorDown(2)->cursorLeft(); + self::assertSame('3a', $cell->getValue()); + $cell = $cell->cursorUp()->cursorRight(3); + self::assertSame('2d', $cell->getValue()); + $cell = $cell->cursorUp(2); + self::assertSame('1d', $cell->getValue(), 'no move above row 1'); + $cell = $cell->cursorLeft(5); + self::assertSame('1a', $cell->getValue(), 'no move to the left of column A'); + $cell = $cell->cursorRight(AddressRange::MAX_COLUMN_INT); + self::assertSame(AddressRange::MAX_COLUMN . '1', $cell->getCoordinate(), 'no column beyond MAX_COLUMN_INT'); + $cell = $cell->cursorDown(AddressRange::MAX_ROW); + self::assertSame(AddressRange::MAX_COLUMN . AddressRange::MAX_ROW, $cell->getCoordinate(), 'no row beyond MAX_ROW'); + self::assertSame( + '4d', + $sheet->getCell('A2') + ->cursorDown(2) // takes us to A4 + ->cursorRight(3) // takes us to D4 + ->getValue() + ); + $sheet->getCell('H5')->setValue(15) + ->cursorDown()->setValue(16) + ->cursorDown()->setValue(17); + self::assertSame(15, $sheet->getCell('H5')->getValue()); + self::assertSame(16, $sheet->getCell('H6')->getValue()); + self::assertSame(17, $sheet->getCell('H7')->getValue()); + + $cell = $sheet->getCell('H5')->cursorXlsLimits(); + self::assertSame('H5', $cell->getCoordinate()); + $cell = $cell->cursorRow(2); + self::assertSame('H2', $cell->getCoordinate()); + $cell = $cell->cursorRow(0); + self::assertSame('H1', $cell->getCoordinate()); + $cell = $cell->cursorColumn('ABC'); + self::assertSame('ABC1', $cell->getCoordinate()); + + $cell = $sheet->getCell('JK71234')->cursorXlsLimits(); + self::assertSame('IV65536', $cell->getCoordinate()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32Radar4Test.php b/tests/PhpSpreadsheetTests/Chart/Charts32Radar4Test.php new file mode 100644 index 000000000..81af8e6f7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Charts32Radar4Test.php @@ -0,0 +1,48 @@ +outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + public function test1LummodNoLumoff(): void + { + $infile = 'samples/templates/32readwriteRadarChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($infile); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $this->outputFileName = File::temporaryFilename(); + $writer->save($this->outputFileName); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFileName; + $file .= '#xl/charts/chart2.xml'; + $data = file_get_contents($file); + self::assertNotFalse($data); + self::assertSame(2, substr_count($data, 'setIncludeCharts(true); + $spreadsheet = $reader->load($infile); + $sheet = $spreadsheet->getSheetByNameOrThrow('Charts'); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0] ?? null; + self::assertInstanceOf(Chart::class, $chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + //echo $data; + self::assertStringContainsString('', $data, 'From input even though same as default'); + self::assertStringContainsString('', $data, 'From input, different from default'); + self::assertStringNotContainsString('view3D', $data, 'No empty view3D tag'); + self::assertStringContainsString('', $data, 'A couple of extra attributes'); + self::assertStringContainsString('', $data, 'Some different values plus re-shuffling'); + self::assertStringContainsString('', $data, 'An extra attribute'); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/DataTableTest.php b/tests/PhpSpreadsheetTests/Chart/DataTableTest.php new file mode 100644 index 000000000..f9448a866 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/DataTableTest.php @@ -0,0 +1,173 @@ +getShowHorizontalBorder()); + self::assertTrue($dataTable->getShowVerticalBorder()); + self::assertTrue($dataTable->getShowOutline()); + self::assertTrue($dataTable->getShowKeys()); + $dataTable->setShowHorizontalBorder(false) + ->setShowVerticalBorder(false) + ->setShowOutline(false) + ->setShowKeys(false); + self::assertFalse($dataTable->getShowHorizontalBorder()); + self::assertFalse($dataTable->getShowVerticalBorder()); + self::assertFalse($dataTable->getShowOutline()); + self::assertFalse($dataTable->getShowKeys()); + } + + private function readCharts(XlsxReader $reader): void + { + $reader->setIncludeCharts(true); + } + + private function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testCopyDataTable(): void + { + $spreadsheet = new Spreadsheet(); + // based on 33_Chart_create_area3. + $spreadsheet->getTheme() + ->setThemeColorName( + SpreadsheetTheme::COLOR_SCHEME_2013_2022_NAME + ); + $worksheet = $spreadsheet->getActiveSheet(); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Sheet2'); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + + // Set the Labels for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + ]; + // Set the Data values for each data series we want to plot + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', null, 4), + ]; + + // Build the dataseries + $series = new DataSeries( + DataSeries::TYPE_AREACHART, // plotType + DataSeries::GROUPING_PERCENT_STACKED, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + $plotArea->setDataTable(new DataTable()); + // No need for Legend if using DataTable + $legend = null; //new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + + // Create the chart + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel // yAxisLabel + ); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A1'); + $chart->setBottomRightPosition('H18'); + + // Add the chart to the worksheet + $sheet2->addChart($chart); + $spreadsheet->setActiveSheetIndex(1); + + // Save Excel 2007 file + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload( + $spreadsheet, + 'Xlsx', + $this->readCharts(...), + $this->writeCharts(...) + ); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea = $chart2->getPlotArea(); + self::assertNotNull($plotArea); + $dtab = $plotArea->getDataTable(); + self::assertNotNull($dtab); + self::assertTrue($dtab->getShowHorizontalBorder()); + self::assertTrue($dtab->getShowVerticalBorder()); + self::assertTrue($dtab->getShowOutline()); + self::assertTrue($dtab->getShowKeys()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php index 2e1bd9d81..c2357e902 100644 --- a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php +++ b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php @@ -81,7 +81,7 @@ class Issue2931Test extends TestCase '', ]; $expectedXml3D = [ - '', + 'c:view3D', // empty view3d no longer generated ]; $expectedXmlNoX = [ 'c:grouping', @@ -102,7 +102,7 @@ class Issue2931Test extends TestCase $data = $writerChart->writeChart($chart); // confirm that file contains expected tags foreach ($expectedXml3D as $expected) { - self::assertSame(1, substr_count($data, $expected), $expected); + self::assertSame(0, substr_count($data, $expected), $expected); } foreach ($expectedXmlNoX as $expected) { self::assertSame(0, substr_count($data, $expected), $expected); diff --git a/tests/PhpSpreadsheetTests/Chart/RenderTest.php b/tests/PhpSpreadsheetTests/Chart/RenderTest.php index fe1a0c1eb..50254d9e0 100644 --- a/tests/PhpSpreadsheetTests/Chart/RenderTest.php +++ b/tests/PhpSpreadsheetTests/Chart/RenderTest.php @@ -4,14 +4,45 @@ declare(strict_types=1); namespace PhpOffice\PhpSpreadsheetTests\Chart; +use finfo; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class RenderTest extends TestCase { + protected function tearDown(): void + { + Settings::unsetChartRenderer(); + } + public function testNoRenderer(): void { $chart = new Chart('Chart1'); self::assertFalse($chart->render()); } + + public function testPhpOutput(): void + { + $infile = 'samples/templates/32readwriteAreaChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($infile); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertInstanceOf(Chart::class, $chart); + Settings::setChartRenderer(MtJpGraphRenderer::class); + ob_start(); + $chart->render('php://output'); + $data = ob_get_clean(); + self::assertNotFalse($data); + $finfo = new finfo(FILEINFO_MIME_TYPE); + $type = $finfo->buffer($data); + self::assertSame('image/png', $type); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Utility/File.php b/tests/PhpSpreadsheetTests/Reader/Utility/File.php deleted file mode 100644 index 0290617b5..000000000 --- a/tests/PhpSpreadsheetTests/Reader/Utility/File.php +++ /dev/null @@ -1,201 +0,0 @@ - '3g2', - 'video/3gp' => '3gp', - 'video/3gpp' => '3gp', - 'application/x-compressed' => '7zip', - 'audio/x-acc' => 'aac', - 'audio/ac3' => 'ac3', - 'application/postscript' => 'ai', - 'audio/x-aiff' => 'aif', - 'audio/aiff' => 'aif', - 'audio/x-au' => 'au', - 'video/x-msvideo' => 'avi', - 'video/msvideo' => 'avi', - 'video/avi' => 'avi', - 'application/x-troff-msvideo' => 'avi', - 'application/macbinary' => 'bin', - 'application/mac-binary' => 'bin', - 'application/x-binary' => 'bin', - 'application/x-macbinary' => 'bin', - 'image/bmp' => 'bmp', - 'image/x-bmp' => 'bmp', - 'image/x-bitmap' => 'bmp', - 'image/x-xbitmap' => 'bmp', - 'image/x-win-bitmap' => 'bmp', - 'image/x-windows-bmp' => 'bmp', - 'image/ms-bmp' => 'bmp', - 'image/x-ms-bmp' => 'bmp', - 'application/bmp' => 'bmp', - 'application/x-bmp' => 'bmp', - 'application/x-win-bitmap' => 'bmp', - 'application/cdr' => 'cdr', - 'application/coreldraw' => 'cdr', - 'application/x-cdr' => 'cdr', - 'application/x-coreldraw' => 'cdr', - 'image/cdr' => 'cdr', - 'image/x-cdr' => 'cdr', - 'zz-application/zz-winassoc-cdr' => 'cdr', - 'application/mac-compactpro' => 'cpt', - 'application/pkix-crl' => 'crl', - 'application/pkcs-crl' => 'crl', - 'application/x-x509-ca-cert' => 'crt', - 'application/pkix-cert' => 'crt', - 'text/css' => 'css', - 'text/x-comma-separated-values' => 'csv', - 'text/comma-separated-values' => 'csv', - 'application/vnd.msexcel' => 'csv', - 'application/x-director' => 'dcr', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', - 'application/x-dvi' => 'dvi', - 'message/rfc822' => 'eml', - 'application/x-msdownload' => 'exe', - 'video/x-f4v' => 'f4v', - 'audio/x-flac' => 'flac', - 'video/x-flv' => 'flv', - 'image/gif' => 'gif', - 'application/gpg-keys' => 'gpg', - 'application/x-gtar' => 'gtar', - 'application/x-gzip' => 'gzip', - 'application/mac-binhex40' => 'hqx', - 'application/mac-binhex' => 'hqx', - 'application/x-binhex40' => 'hqx', - 'application/x-mac-binhex40' => 'hqx', - 'text/html' => 'html', - 'image/x-icon' => 'ico', - 'image/x-ico' => 'ico', - 'image/vnd.microsoft.icon' => 'ico', - 'text/calendar' => 'ics', - 'application/java-archive' => 'jar', - 'application/x-java-application' => 'jar', - 'application/x-jar' => 'jar', - 'image/jp2' => 'jp2', - 'video/mj2' => 'jp2', - 'image/jpx' => 'jp2', - 'image/jpm' => 'jp2', - 'image/jpeg' => 'jpeg', - 'image/pjpeg' => 'jpeg', - 'application/x-javascript' => 'js', - 'application/json' => 'json', - 'text/json' => 'json', - 'application/vnd.google-earth.kml+xml' => 'kml', - 'application/vnd.google-earth.kmz' => 'kmz', - 'text/x-log' => 'log', - 'audio/x-m4a' => 'm4a', - 'audio/mp4' => 'm4a', - 'application/vnd.mpegurl' => 'm4u', - 'audio/midi' => 'mid', - 'application/vnd.mif' => 'mif', - 'video/quicktime' => 'mov', - 'video/x-sgi-movie' => 'movie', - 'audio/mpeg' => 'mp3', - 'audio/mpg' => 'mp3', - 'audio/mpeg3' => 'mp3', - 'audio/mp3' => 'mp3', - 'video/mp4' => 'mp4', - 'video/mpeg' => 'mpeg', - 'application/oda' => 'oda', - 'audio/ogg' => 'ogg', - 'video/ogg' => 'ogg', - 'application/ogg' => 'ogg', - 'font/otf' => 'otf', - 'application/x-pkcs10' => 'p10', - 'application/pkcs10' => 'p10', - 'application/x-pkcs12' => 'p12', - 'application/x-pkcs7-signature' => 'p7a', - 'application/pkcs7-mime' => 'p7c', - 'application/x-pkcs7-mime' => 'p7c', - 'application/x-pkcs7-certreqresp' => 'p7r', - 'application/pkcs7-signature' => 'p7s', - 'application/pdf' => 'pdf', - 'application/octet-stream' => 'pdf', - 'application/x-x509-user-cert' => 'pem', - 'application/x-pem-file' => 'pem', - 'application/pgp' => 'pgp', - 'application/x-httpd-php' => 'php', - 'application/php' => 'php', - 'application/x-php' => 'php', - 'text/php' => 'php', - 'text/x-php' => 'php', - 'application/x-httpd-php-source' => 'php', - 'image/png' => 'png', - 'image/x-png' => 'png', - 'application/powerpoint' => 'ppt', - 'application/vnd.ms-powerpoint' => 'ppt', - 'application/vnd.ms-office' => 'ppt', - 'application/msword' => 'doc', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', - 'application/x-photoshop' => 'psd', - 'image/vnd.adobe.photoshop' => 'psd', - 'audio/x-realaudio' => 'ra', - 'audio/x-pn-realaudio' => 'ram', - 'application/x-rar' => 'rar', - 'application/rar' => 'rar', - 'application/x-rar-compressed' => 'rar', - 'audio/x-pn-realaudio-plugin' => 'rpm', - 'application/x-pkcs7' => 'rsa', - 'text/rtf' => 'rtf', - 'text/richtext' => 'rtx', - 'video/vnd.rn-realvideo' => 'rv', - 'application/x-stuffit' => 'sit', - 'application/smil' => 'smil', - 'text/srt' => 'srt', - 'image/svg+xml' => 'svg', - 'application/x-shockwave-flash' => 'swf', - 'application/x-tar' => 'tar', - 'application/x-gzip-compressed' => 'tgz', - 'image/tiff' => 'tiff', - 'font/ttf' => 'ttf', - 'text/plain' => 'txt', - 'text/x-vcard' => 'vcf', - 'application/videolan' => 'vlc', - 'text/vtt' => 'vtt', - 'audio/x-wav' => 'wav', - 'audio/wave' => 'wav', - 'audio/wav' => 'wav', - 'application/wbxml' => 'wbxml', - 'video/webm' => 'webm', - 'image/webp' => 'webp', - 'audio/x-ms-wma' => 'wma', - 'application/wmlc' => 'wmlc', - 'video/x-ms-wmv' => 'wmv', - 'video/x-ms-asf' => 'wmv', - 'font/woff' => 'woff', - 'font/woff2' => 'woff2', - 'application/xhtml+xml' => 'xhtml', - 'application/excel' => 'xl', - 'application/msexcel' => 'xls', - 'application/x-msexcel' => 'xls', - 'application/x-ms-excel' => 'xls', - 'application/x-excel' => 'xls', - 'application/x-dos_ms_excel' => 'xls', - 'application/xls' => 'xls', - 'application/x-xls' => 'xls', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', - 'application/vnd.ms-excel' => 'xlsx', - 'application/xml' => 'xml', - 'text/xml' => 'xml', - 'text/xsl' => 'xsl', - 'application/xspf+xml' => 'xspf', - 'application/x-compress' => 'z', - 'application/x-zip' => 'zip', - 'application/zip' => 'zip', - 'application/x-zip-compressed' => 'zip', - 'application/s-compressed' => 'zip', - 'multipart/x-zip' => 'zip', - 'text/x-scriptzsh' => 'zsh', - ]; - - return $mime_map[$mime] ?? ''; - } -} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php index 36e3b3075..409dac81e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/IgnoredErrorTest.php @@ -13,6 +13,8 @@ class IgnoredErrorTest extends AbstractFunctional { private const FILENAME = 'tests/data/Reader/XLSX/ignoreerror.xlsx'; + private const FILENAME2 = 'tests/data/Reader/XLSX/misleading.xlsx'; + public function testIgnoredError(): void { $reader = new Xlsx(); @@ -69,4 +71,90 @@ class IgnoredErrorTest extends AbstractFunctional self::assertTrue($sheet->getCell('A2')->getIgnoredErrors()->getNumberStoredAsText()); $spreadsheet->disconnectWorksheets(); } + + public function testMisleading(): void + { + $reader = new Xlsx(); + $originalSpreadsheet = $reader->load(self::FILENAME2); + $sheet = $originalSpreadsheet->getActiveSheet(); + self::assertTrue( + $sheet->getCell('C1') + ->getIgnoredErrors() + ->getMisleadingFormat() + ); + self::assertFalse( + $sheet->getCell('C2') + ->getIgnoredErrors() + ->getMisleadingFormat() + ); + self::assertFalse( + $sheet->getCell('C3') + ->getIgnoredErrors() + ->getMisleadingFormat() + ); + self::assertTrue( + $sheet->getCell('D1') + ->getIgnoredErrors() + ->getNumberStoredAsText() + ); + self::assertFalse( + $sheet->getCell('D2') + ->getIgnoredErrors() + ->getNumberStoredAsText() + ); + self::assertFalse( + $sheet->getCell('D3') + ->getIgnoredErrors() + ->getNumberStoredAsText() + ); + + $sheet->getCell('C1') + ->getIgnoredErrors() + ->setMisleadingFormat(false); + $sheet->getCell('D1') + ->getIgnoredErrors() + ->setNumberStoredAsText(false); + $sheet->getCell('C2') + ->getIgnoredErrors() + ->setMisleadingFormat(true); + $sheet->getCell('D2') + ->getIgnoredErrors() + ->setNumberStoredAsText(true); + + $spreadsheet = $this->writeAndReload($originalSpreadsheet, 'Xlsx'); + $originalSpreadsheet->disconnectWorksheets(); + $sheet = $spreadsheet->getActiveSheet(); + self::assertFalse( + $sheet->getCell('C1') + ->getIgnoredErrors() + ->getMisleadingFormat() + ); + self::assertTrue( + $sheet->getCell('C2') + ->getIgnoredErrors() + ->getMisleadingFormat() + ); + self::assertFalse( + $sheet->getCell('C3') + ->getIgnoredErrors() + ->getMisleadingFormat() + ); + self::assertFalse( + $sheet->getCell('D1') + ->getIgnoredErrors() + ->getNumberStoredAsText() + ); + self::assertTrue( + $sheet->getCell('D2') + ->getIgnoredErrors() + ->getNumberStoredAsText() + ); + self::assertFalse( + $sheet->getCell('D3') + ->getIgnoredErrors() + ->getNumberStoredAsText() + ); + + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4416Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4416Test.php index 8a139b03c..2e5ae320a 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4416Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue4416Test.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; +use PhpOffice\PhpSpreadsheet\Reader\DefaultReadFilter; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PHPUnit\Framework\TestCase; @@ -49,7 +50,9 @@ class Issue4416Test extends TestCase { $file = self::$file; $reader = new XlsxReader(); + self::assertInstanceOf(DefaultReadFilter::class, $reader->getReadFilter()); $reader->setReadFilter(new Issue4416Filter()); + self::assertInstanceOf(Issue4416Filter::class, $reader->getReadFilter()); $spreadsheet = $reader->load($file); $sheet = $spreadsheet->getActiveSheet(); self::assertEqualsWithDelta( diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php index 69e0eee24..ce371deed 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php @@ -8,7 +8,6 @@ use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; -use PhpOffice\PhpSpreadsheetTests\Reader\Utility\File; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 6f90aed29..1f6556c57 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -100,6 +100,17 @@ class DateTest extends TestCase self::assertEqualsWithDelta($expectedResult, $result, 1E-5); } + #[DataProvider('providerDateTimeDateTimeToExcel')] + public function testDateTime2DateTimeToExcel(float|int $expectedResult, DateTimeInterface $dateTimeObject): void + { + // Show new parameter will override static value + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + + $result = Date::dateTimeToExcel($dateTimeObject, Date::CALENDAR_WINDOWS_1900); + self::assertEqualsWithDelta($expectedResult, $result, 1E-5); + self::assertSame(Date::CALENDAR_MAC_1904, Date::getExcelCalendar()); + } + public static function providerDateTimeDateTimeToExcel(): array { return require 'tests/data/Shared/Date/DateTimeToExcel.php'; @@ -134,6 +145,18 @@ class DateTest extends TestCase self::assertEquals($expectedResult, $result); } + #[DataProvider('providerDateTimeExcelToTimestamp1904')] + public function testDateTime2ExcelToTimestamp1904(float|int $expectedResult, float|int $excelDateTimeValue): void + { + if ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN) { + self::markTestSkipped('Test invalid on 32-bit system.'); + } + + $result = Date::excelToTimestamp($excelDateTimeValue, calendar: Date::CALENDAR_MAC_1904); + self::assertEquals($expectedResult, $result); + self::assertSame($this->excelCalendar, Date::getExcelCalendar()); + } + public static function providerDateTimeExcelToTimestamp1904(): array { return require 'tests/data/Shared/Date/ExcelToTimestamp1904.php'; @@ -148,6 +171,14 @@ class DateTest extends TestCase self::assertEqualsWithDelta($expectedResult, $result, 1E-5); } + #[DataProvider('providerDateTimeTimestampToExcel1904')] + public function testDateTime2TimestampToExcel1904(mixed $expectedResult, float|int|string $unixTimestamp): void + { + $result = Date::timestampToExcel($unixTimestamp, Date::CALENDAR_MAC_1904); + self::assertEqualsWithDelta($expectedResult, $result, 1E-5); + self::assertSame($this->excelCalendar, Date::getExcelCalendar()); + } + public static function providerDateTimeTimestampToExcel1904(): array { return require 'tests/data/Shared/Date/TimestampToExcel1904.php'; @@ -204,6 +235,9 @@ class DateTest extends TestCase self::assertNotFalse($timestamp2); self::assertEqualsWithDelta(45803.60277777778, $timestamp1, 1.0E-10); self::assertSame($timestamp1, $timestamp2); + $timestamp3 = Date::stringToExcel('26.05.2025 14:28:00.00', Date::CALENDAR_MAC_1904); + self::assertEqualsWithDelta(45803.60277777778, 1462 + $timestamp3, 1.0E-10); + self::assertSame($this->excelCalendar, Date::getExcelCalendar()); $date = Date::PHPToExcel('2020-01-01'); self::assertEquals(43831.0, $date); diff --git a/tests/PhpSpreadsheetTests/Style/MultipleRangeTest.php b/tests/PhpSpreadsheetTests/Style/MultipleRangeTest.php new file mode 100644 index 000000000..c07161beb --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/MultipleRangeTest.php @@ -0,0 +1,84 @@ +getActiveSheet(); + $data = [ + [10, 11, 12, 13, 14, 15], + [20, 21, 22, 23, 24, 25], + [30, 31, 32, 33, 34, 35], + [40, 41, 42, 43, 44, 45], + [50, 51, 52, 53, 54, 55], + [60, 61, 62, 63, 64, 65], + ]; + $sheet->fromArray($data); + $styleArray = ['font' => ['bold' => true]]; + $range1 = 'A2:C5,E2:F5'; + $range1Array = Coordinate::extractAllCellReferencesInRange($range1); + $sheet + ->getStyle($range1) + ->applyFromArray($styleArray); + self::assertSame($range1, $sheet->getSelectedCells()); + self::assertTrue( + $sheet->getStyle($range1)->getFont()->getBold() + ); + $range2 = 'A1,F6,B2:D3'; + $range2Array = Coordinate::extractAllCellReferencesInRange($range2); + $sheet + ->getStyle($range2) + ->getFont() + ->setItalic(true); + self::assertSame($range2, $sheet->getSelectedCells()); + self::assertTrue( + $sheet->getStyle($range2)->getFont()->getItalic() + ); + // A1 is part of range2 but not range1 + self::assertNotContains('A1', $range1Array); + self::assertContains('A1', $range2Array); + self::assertTrue( + $sheet->getStyle('A1')->getFont()->getItalic() + ); + self::assertFalse( + $sheet->getStyle('A1')->getFont()->getBold() + ); + // B1 is part of neither range2 nor range1 + self::assertNotContains('B1', $range1Array); + self::assertNotContains('B1', $range2Array); + self::assertFalse( + $sheet->getStyle('B1')->getFont()->getItalic() + ); + self::assertFalse( + $sheet->getStyle('B1')->getFont()->getBold() + ); + // B2 is part of both range2 and range1 + self::assertContains('B2', $range1Array); + self::assertContains('B2', $range2Array); + self::assertTrue( + $sheet->getStyle('B2')->getFont()->getItalic() + ); + self::assertTrue( + $sheet->getStyle('B2')->getFont()->getBold() + ); + // C4 is part of range1 but not range2 + self::assertContains('C4', $range1Array); + self::assertNotContains('C4', $range2Array); + self::assertTrue( + $sheet->getStyle('C4')->getFont()->getBold() + ); + self::assertFalse( + $sheet->getStyle('C4')->getFont()->getItalic() + ); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php index e12b2d0ca..5463cd6c3 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/ColumnTest.php @@ -51,6 +51,14 @@ class ColumnTest extends SetupTeardown $sheet->getAutoFilter()->getColumn('B'); } + public function testSetBadColumnIndex(): void + { + $this->expectException(PhpSpreadsheetException::class); + $this->expectExceptionMessage('Column string index can not be empty'); + $sheet = $this->initSheet(); + $sheet->getAutoFilter()->setColumn(''); + } + public function testSetColumnIndex(): void { $sheet = $this->initSheet(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/RuleCustom2Test.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/RuleCustom2Test.php new file mode 100644 index 000000000..a5fd2c2fd --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/RuleCustom2Test.php @@ -0,0 +1,62 @@ +getSheet(); + $sheet->getCell('A1')->setValue('Heading'); + $sheet->getCell('A2')->setValue(2); + //$sheet->getCell('A3')->setValue(3); + $sheet->getCell('A4')->setValue(4); + $sheet->getCell('B1')->setValue('Heading2'); + $sheet->getCell('B2')->setValue(1); + $sheet->getCell('B3')->setValue(2); + $sheet->getCell('B4')->setValue(3); + $this->maxRow = $maxRow = 4; + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange("A1:B$maxRow"); + + return $sheet; + } + + /** @param int[] $expectedResult */ + #[DataProvider('providerCondition')] + public function testComparisonToNullString(array $expectedResult, string $condition): void + { + $sheet = $this->initSheet(); + $columnFilter = $sheet->getAutoFilter()->getColumn('A'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); + $columnFilter->createRule() + ->setRule( + $condition, + '' + ) + ->setRuleType(Rule::AUTOFILTER_RULETYPE_CUSTOMFILTER); + self::assertEquals($expectedResult, $this->getVisible()); + } + + /** @return array */ + public static function providerCondition(): array + { + // Comparing to null-string, equal and notequal work as expected. + // Other conditions filter everything. + return [ + [[3], Rule::AUTOFILTER_COLUMN_RULE_EQUAL], + [[2, 4], Rule::AUTOFILTER_COLUMN_RULE_NOTEQUAL], + [[2, 3, 4], Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN], + [[2, 3, 4], Rule::AUTOFILTER_COLUMN_RULE_GREATERTHANOREQUAL], + [[2, 3, 4], Rule::AUTOFILTER_COLUMN_RULE_LESSTHAN], + [[2, 3, 4], Rule::AUTOFILTER_COLUMN_RULE_LESSTHANOREQUAL], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php b/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php index 33eb86ec8..2392e2700 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/InsertTest.php @@ -28,12 +28,24 @@ class InsertTest extends TestCase $sheet->insertNewRowBefore($currentRow, 1); self::assertSame(1001, $sheet->getHighestRow()); self::assertSame(6, $sheet->getHighestDataRow()); - self::assertTrue($sheet->getStyle('C3')->getFont()->getBold()); + self::assertTrue( + $sheet->getStyle('C3')->getFont()->getBold() + ); self::assertSame(11, $sheet->getCell('C3')->getValue()); - self::assertTrue($sheet->getStyle('C4')->getFont()->getBold()); + 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()); + self::assertFalse( + $sheet->getRowDimension(1001)->getVisible() + ); + self::assertTrue( + $sheet->getRowDimension(1000)->getVisible() + ); + $sheet->removeRow(15, 10); + self::assertSame(991, $sheet->getHighestRow(), 'highest row decreases by 10'); + $sheet->removeRow(985, 10); + self::assertSame(984, $sheet->getHighestRow(), 'delete range overlaps highest row so highest is now row before delete'); $spreadsheet->disconnectWorksheets(); } @@ -56,12 +68,24 @@ class InsertTest extends TestCase $sheet->insertNewColumnBefore($currentColumn, 1); self::assertSame('ZZ', $sheet->getHighestColumn()); self::assertSame('E', $sheet->getHighestDataColumn()); - self::assertTrue($sheet->getStyle('C3')->getFont()->getBold()); + self::assertTrue( + $sheet->getStyle('C3')->getFont()->getBold() + ); self::assertSame(11, $sheet->getCell('C3')->getValue()); - self::assertTrue($sheet->getStyle('D3')->getFont()->getBold()); + 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()); + self::assertFalse( + $sheet->getColumnDimension('ZZ')->getVisible() + ); + self::assertTrue( + $sheet->getColumnDimension('ZY')->getVisible() + ); + $sheet->removeColumn('G', 5); + self::assertSame('ZU', $sheet->getHighestColumn(), 'ZZ moved over 5 columns'); + $sheet->removeColumn('ZR', 5); + self::assertSame('ZQ', $sheet->getHighestColumn(), 'delete range overlaps highest column so new highest is one before deleted columns'); $spreadsheet->disconnectWorksheets(); } @@ -97,7 +121,9 @@ class InsertTest extends TestCase ]); $sheet->getCell('XFD1')->setValue('lastcol'); $sheet->insertNewColumnBefore('D', 4); - self::assertFalse($sheet->getCellCollection()->has('XFH1')); + self::assertFalse( + $sheet->getCellCollection()->has('XFH1') + ); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/RemoveTest.php b/tests/PhpSpreadsheetTests/Worksheet/RemoveTest.php index 9778aa9b0..e33a8802c 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/RemoveTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/RemoveTest.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Fill; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class RemoveTest extends TestCase @@ -86,4 +87,62 @@ class RemoveTest extends TestCase $spreadsheet->disconnectWorksheets(); } + + /** + * @param array> $expectedArray + */ + #[DataProvider('providerColumnEdgeCases')] + public function testColumnEdgeCases(string $start, int $num, array $expectedArray, string $expectedHighestColumn): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + $sheet->removeColumn($start, $num); + self::assertSame($expectedArray, $sheet->toArray(formatData: false)); + self::assertSame($expectedHighestColumn, $sheet->getHighestColumn()); + + $spreadsheet->disconnectWorksheets(); + } + + /** + * @return array + */ + public static function providerColumnEdgeCases(): array + { + return [ + 'remove positive cols' => ['E', 2, [[1, 2, 3, 4, 7, 8, 9, 10]], 'H'], + 'remove negative cols' => ['E', -2, [[1, 2, 3, 6, 7, 8, 9, 10]], 'H'], + 'remove zero cols' => ['E', 0, [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], 'J'], + 'remove cols above highest' => ['T', 2, [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], 'J'], + ]; + } + + /** + * @param array> $expectedArray + */ + #[DataProvider('providerRowEdgeCases')] + public function testRowEdgeCases(int $start, int $num, array $expectedArray, int $expectedHighestRow): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->fromArray([[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]); + $sheet->removeRow($start, $num); + self::assertSame($expectedArray, $sheet->toArray(formatData: false)); + self::assertSame($expectedHighestRow, $sheet->getHighestRow()); + + $spreadsheet->disconnectWorksheets(); + } + + /** + * @return array>, int}> + */ + public static function providerRowEdgeCases(): array + { + return [ + 'remove positive rows' => [5, 2, [[1], [2], [3], [4], [7], [8], [9], [10]], 8], + 'remove negative rows' => [5, -2, [[1], [2], [3], [6], [7], [8], [9], [10]], 8], + 'remove zero rows' => [5, 0, [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]], 10], + 'remove rows above highest' => [20, 2, [[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]], 10], + ]; + } } diff --git a/tests/data/Reader/XLSX/misleading.xlsx b/tests/data/Reader/XLSX/misleading.xlsx new file mode 100644 index 000000000..2ef724d98 Binary files /dev/null and b/tests/data/Reader/XLSX/misleading.xlsx differ