diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c3a74ede..e2e66a4a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -34,7 +34,7 @@ If this is an issue with reading a specific spreadsheet file, then it may be app - [ ] Writer - [ ] Styles - [ ] Data Validations -- [ ] Formula Calulations +- [ ] Formula Calculations - [ ] Charts - [ ] AutoFilter - [ ] Form Elements diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b36bf6c8..29a55f44 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,10 @@ jobs: - '8.0' - '8.1' + include: + - php-version: '8.2' + experimental: true + name: PHP ${{ matrix.php-version }} steps: @@ -39,7 +43,7 @@ jobs: - name: Delete composer lock file id: composer-lock - if: ${{ matrix.php-version == '8.1' }} + if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' }} run: | rm composer.lock echo "::set-output name=flags::--ignore-platform-reqs" diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index ca2feb42..16809b84 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -55,7 +55,7 @@ $config 'function_declaration' => true, 'function_to_constant' => true, 'function_typehint_space' => true, - 'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright', 'throws']], + 'general_phpdoc_annotation_remove' => ['annotations' => ['access', 'category', 'copyright']], 'global_namespace_import' => true, 'header_comment' => false, // We don't use common header in all our files 'heredoc_indentation' => false, // Requires PHP >= 7.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index a7082cf9..3de5250b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,139 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Xlsx Reader Accept Palette of Fewer than 64 Colors [Issue #3093](https://github.com/PHPOffice/PhpSpreadsheet/issues/3093) [PR #3096](https://github.com/PHPOffice/PhpSpreadsheet/pull/3096) +- Use Locale-Independent Float Conversion for Xlsx Writer Custom Property [Issue #3095](https://github.com/PHPOffice/PhpSpreadsheet/issues/3095) [PR #3099](https://github.com/PHPOffice/PhpSpreadsheet/pull/3099) +- Allow setting AutoFilter range on a single cell or row [Issue #3102](https://github.com/PHPOffice/PhpSpreadsheet/issues/3102) [PR #3111](https://github.com/PHPOffice/PhpSpreadsheet/pull/3111) +- Xlsx Reader External Data Validations Flag Missing [Issue #2677](https://github.com/PHPOffice/PhpSpreadsheet/issues/2677) [PR #3078](https://github.com/PHPOffice/PhpSpreadsheet/pull/3078) +- Reduces extra memory usage on `__destruct()` calls + + +## 1.25.2 - 2022-09-25 + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Composer dependency clash with ezyang/htmlpurifier + + +## 1.25.0 - 2022-09-25 + +### Added + +- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions +- Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions +- Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of + JpGraph library to render charts added. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes. + +### Changed + +- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) + - Merge methods now allow an additional `$behaviour` argument. Permitted values are: + - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) + - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell + +### Deprecated + +- Axis getLineProperty deprecated in favor of getLineColorProperty. +- Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated. +- Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated. +- ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed). +- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT. +- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead. + +### Removed + +- Nothing + +### Fixed + +- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077) +- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) +- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) +- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) +- cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) +- Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) +- Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960) +- Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971) +- Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950) +- Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976) +- Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) +- Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) +- Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) +- Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016) +- Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018) +- Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048) +- R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052) +- Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053) + +## 1.24.1 - 2022-07-18 + +### Added + +- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0 +- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) +- Additional Chart fixes + - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) + - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) + - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) +- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) + +## 1.24.0 - 2022-07-09 + +Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. + +### Added + +- Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) - Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) - Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) @@ -18,14 +151,20 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) - Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) - Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) +- Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty": + - Treat rows/columns containing no cell records as empty (default) + - Treat cells containing a null value as empty + - Treat cells containing an empty string as empty ### Changed -- Better enforcement of value modification to match specified datatype when using setValueExplicit() +- Modify `rangeBoundaries()`, `rangeDimension()` and `getRangeBoundaries()` Coordinate methods to work with row/column ranges as well as with cell ranges and cells [PR #2926](https://github.com/PHPOffice/PhpSpreadsheet/pull/2926) +- Better enforcement of value modification to match specified datatype when using `setValueExplicit()` - Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776) - Memory and speed improvements, particularly for the Cell Collection, and the Writers. See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions +- Improved performance for removing rows/columns from a worksheet ### Deprecated @@ -43,7 +182,12 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) - Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) - Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) -- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) [PR #2923](https://github.com/PHPOffice/PhpSpreadsheet/pull/2923) +- Adjust both coordinates for two-cell anchors when rows/columns are added/deleted. [Issue #2908](https://github.com/PHPOffice/PhpSpreadsheet/issues/2908) [PR #2909](https://github.com/PHPOffice/PhpSpreadsheet/pull/2909) +- Keep calculated string results below 32K. [PR #2921](https://github.com/PHPOffice/PhpSpreadsheet/pull/2921) +- Filter out illegal Unicode char values FFFE/FFFF. [Issue #2897](https://github.com/PHPOffice/PhpSpreadsheet/issues/2897) [PR #2910](https://github.com/PHPOffice/PhpSpreadsheet/pull/2910) +- Better handling of REF errors and propagation of all errors in Calculation engine. [PR #2902](https://github.com/PHPOffice/PhpSpreadsheet/pull/2902) +- Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) ## 1.23.0 - 2022-04-24 diff --git a/README.md b/README.md index 40b025e7..ef04cd07 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ composer require phpoffice/phpspreadsheet ``` If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: -```json lines +```json { "require": { "phpoffice/phpspreadsheet": "^1.23" @@ -49,6 +49,42 @@ to ensure that the correct dependencies are retrieved to match your deployment e See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. +### Additional Installation Options + +If you want to write to PDF, or to include Charts when you write to HTML or PDF, then you will need to install additional libraries: + +#### PDF + +For PDF Generation, you can install any of the following, and then configure PhpSpreadsheet to indicate which library you are going to use: + - mpdf/mpdf + - dompdf/dompdf + - tecnickcom/tcpdf + +and configure PhpSpreadsheet using: + +```php +// Dompdf, Mpdf or Tcpdf (as appropriate) +$className = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class; +IOFactory::registerWriter('Pdf', $className); +``` +or the appropriate PDF Writer wrapper for the library that you have chosen to install. + +#### Chart Export + +For Chart export, we support following packages, which you will also need to install yourself using `composer require` + - [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0. + You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) + - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) + +and then configure PhpSpreadsheet using: +```php +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph +//or +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph +``` + +One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). diff --git a/composer.json b/composer.json index 4cf5e77d..4f44b8bd 100644 --- a/composer.json +++ b/composer.json @@ -69,32 +69,33 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", - "ezyang/htmlpurifier": "^4.13", + "ezyang/htmlpurifier": "^4.15", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0 || ^2.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "dompdf/dompdf": "^1.0", + "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "jpgraph/jpgraph": "^4.0", - "mpdf/mpdf": "8.1.1", + "mitoteam/jpgraph": "^10.2.4", + "mpdf/mpdf": "^8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.6", - "tecnickcom/tcpdf": "^6.4" + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" }, "suggest": { + "ext-intl": "PHP Internationalization Functions", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index ee0d79d1..9a425e87 100644 --- a/composer.lock +++ b/composer.lock @@ -4,33 +4,43 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2e09b120ad90fc0fbbc055e1f341910e", + "content-hash": "60da78cdd66329fbd4e0d50324363072", "packages": [ { "name": "ezyang/htmlpurifier", - "version": "v4.14.0", + "version": "v4.16.0", "source": { "type": "git", "url": "https://github.com/ezyang/htmlpurifier.git", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", - "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/523407fb06eb9e5f3d59889b3978d5bfe94299c8", + "reference": "523407fb06eb9e5f3d59889b3978d5bfe94299c8", "shasum": "" }, "require": { - "php": ">=5.2" + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" }, "type": "library", "autoload": { - "psr-0": { - "HTMLPurifier": "library/" - }, "files": [ "library/HTMLPurifier.composer.php" ], + "psr-0": { + "HTMLPurifier": "library/" + }, "exclude-from-classmap": [ "/library/HTMLPurifier/Language/" ] @@ -53,35 +63,37 @@ ], "support": { "issues": "https://github.com/ezyang/htmlpurifier/issues", - "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.16.0" }, - "time": "2021-12-25T01:21:49+00:00" + "time": "2022-09-18T07:06:19+00:00" }, { "name": "maennchen/zipstream-php", - "version": "2.1.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", - "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/211e9ba1530ea5260b45d90c9ea252f56ec52729", + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729", "shasum": "" }, "require": { "myclabs/php-enum": "^1.5", - "php": ">= 7.1", + "php": "^7.4 || ^8.0", "psr/http-message": "^1.0", "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { "ext-zip": "*", - "guzzlehttp/guzzle": ">= 6.3", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": ">= 7.5" + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^4.1" }, "type": "library", "autoload": { @@ -118,7 +130,7 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/master" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.1" }, "funding": [ { @@ -126,7 +138,7 @@ "type": "open_collective" } ], - "time": "2020-05-30T13:11:16+00:00" + "time": "2022-05-18T15:52:06+00:00" }, { "name": "markbaker/complex", @@ -237,16 +249,16 @@ }, { "name": "myclabs/php-enum", - "version": "1.8.3", + "version": "1.8.4", "source": { "type": "git", "url": "https://github.com/myclabs/php-enum.git", - "reference": "b942d263c641ddb5190929ff840c68f78713e937" + "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", - "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/a867478eae49c9f59ece437ae7f9506bfaa27483", + "reference": "a867478eae49c9f59ece437ae7f9506bfaa27483", "shasum": "" }, "require": { @@ -262,7 +274,10 @@ "autoload": { "psr-4": { "MyCLabs\\Enum\\": "src/" - } + }, + "classmap": [ + "stubs/Stringable.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -281,7 +296,7 @@ ], "support": { "issues": "https://github.com/myclabs/php-enum/issues", - "source": "https://github.com/myclabs/php-enum/tree/1.8.3" + "source": "https://github.com/myclabs/php-enum/tree/1.8.4" }, "funding": [ { @@ -293,7 +308,7 @@ "type": "tidelift" } ], - "time": "2021-07-05T08:18:36+00:00" + "time": "2022-08-04T09:53:51+00:00" }, { "name": "psr/http-client", @@ -508,28 +523,31 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -537,12 +555,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -568,7 +586,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -584,36 +602,36 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [ { "name": "composer/pcre", - "version": "1.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2" + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/3d322d715c43a1ac36c7fe215fa59336265500f2", - "reference": "3d322d715c43a1ac36c7fe215fa59336265500f2", + "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd", + "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd", "shasum": "" }, "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1", + "phpstan/phpstan": "^1.3", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" + "symfony/phpunit-bridge": "^5" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.x-dev" + "dev-main": "3.x-dev" } }, "autoload": { @@ -641,7 +659,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.0" + "source": "https://github.com/composer/pcre/tree/3.0.0" }, "funding": [ { @@ -657,27 +675,27 @@ "type": "tidelift" } ], - "time": "2021-12-06T15:17:27+00:00" + "time": "2022-02-25T20:21:48+00:00" }, { "name": "composer/semver", - "version": "3.2.6", + "version": "3.3.2", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "83e511e247de329283478496f7a1e114c9517506" + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/83e511e247de329283478496f7a1e114c9517506", - "reference": "83e511e247de329283478496f7a1e114c9517506", + "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", + "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.54", + "phpstan/phpstan": "^1.4", "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", @@ -722,7 +740,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.2.6" + "source": "https://github.com/composer/semver/tree/3.3.2" }, "funding": [ { @@ -738,31 +756,31 @@ "type": "tidelift" } ], - "time": "2021-10-25T11:34:17+00:00" + "time": "2022-04-01T19:23:25+00:00" }, { "name": "composer/xdebug-handler", - "version": "2.0.3", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e" + "reference": "ced299686f41dce890debac69273b47ffe98a40c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6555461e76962fd0379c444c46fd558a0fcfb65e", - "reference": "6555461e76962fd0379c444c46fd558a0fcfb65e", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", + "reference": "ced299686f41dce890debac69273b47ffe98a40c", "shasum": "" }, "require": { - "composer/pcre": "^1", - "php": "^5.3.2 || ^7.0 || ^8.0", + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" + "symfony/phpunit-bridge": "^6.0" }, "type": "library", "autoload": { @@ -788,7 +806,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" }, "funding": [ { @@ -804,31 +822,34 @@ "type": "tidelift" } ], - "time": "2021-12-08T13:07:32+00:00" + "time": "2022-02-25T21:32:43+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7" + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "231b4e82eee01f16537c8ac3fce31c1f83320c80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", - "reference": "7d5cb8826ed72d4ca4c07acf005bba2282e5a7c7", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/231b4e82eee01f16537c8ac3fce31c1f83320c80", + "reference": "231b4e82eee01f16537c8ac3fce31c1f83320c80", "shasum": "" }, "require": { "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.0 || ^3.0 || ^4.0" + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" }, "require-dev": { "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" }, "default-branch": true, "type": "composer-plugin", @@ -850,6 +871,10 @@ "email": "franck.nijhof@dealerdirect.com", "homepage": "http://www.frenck.nl", "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", @@ -861,6 +886,7 @@ "codesniffer", "composer", "installer", + "phpcbf", "phpcs", "plugin", "qa", @@ -872,23 +898,23 @@ "tests" ], "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" }, - "time": "2021-08-16T14:43:41+00:00" + "time": "2022-07-26T12:51:47+00:00" }, { "name": "doctrine/annotations", - "version": "1.13.2", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08" + "reference": "648b0343343565c4a056bfc8392201385e8d89f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5b668aef16090008790395c02c893b1ba13f7e08", - "reference": "5b668aef16090008790395c02c893b1ba13f7e08", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0", + "reference": "648b0343343565c4a056bfc8392201385e8d89f0", "shasum": "" }, "require": { @@ -900,9 +926,10 @@ "require-dev": { "doctrine/cache": "^1.11 || ^2.0", "doctrine/coding-standard": "^6.0 || ^8.1", - "phpstan/phpstan": "^0.12.20", + "phpstan/phpstan": "^1.4.10 || ^1.8.0", "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5", - "symfony/cache": "^4.4 || ^5.2" + "symfony/cache": "^4.4 || ^5.2", + "vimeo/psalm": "^4.10" }, "type": "library", "autoload": { @@ -945,9 +972,9 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.13.2" + "source": "https://github.com/doctrine/annotations/tree/1.13.3" }, - "time": "2021-08-05T19:00:23+00:00" + "time": "2022-07-02T10:48:51+00:00" }, { "name": "doctrine/instantiator", @@ -1021,32 +1048,28 @@ }, { "name": "doctrine/lexer", - "version": "1.2.1", + "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" @@ -1081,7 +1104,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" + "source": "https://github.com/doctrine/lexer/tree/1.2.3" }, "funding": [ { @@ -1097,27 +1120,28 @@ "type": "tidelift" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2022-02-28T11:07:21+00:00" }, { "name": "dompdf/dompdf", - "version": "v1.2.2", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "5031045d9640b38cfc14aac9667470df09c9e090" + "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/5031045d9640b38cfc14aac9667470df09c9e090", - "reference": "5031045d9640b38cfc14aac9667470df09c9e090", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c5310df0e22c758c85ea5288175fc6cd777bc085", + "reference": "c5310df0e22c758c85ea5288175fc6cd777bc085", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "phenx/php-font-lib": "^0.5.4", - "phenx/php-svg-lib": "^0.3.3 || ^0.4.0", + "masterminds/html5": "^2.0", + "phenx/php-font-lib": ">=0.5.4 <1.0.0", + "phenx/php-svg-lib": ">=0.3.3 <1.0.0", "php": "^7.1 || ^8.0" }, "require-dev": { @@ -1148,74 +1172,66 @@ ], "authors": [ { - "name": "Fabien Ménager", - "email": "fabien.menager@gmail.com" - }, - { - "name": "Brian Sweeney", - "email": "eclecticgeek@gmail.com" - }, - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com" + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" } ], "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v1.2.2" + "source": "https://github.com/dompdf/dompdf/tree/v2.0.1" }, - "time": "2022-04-27T13:50:54+00:00" + "time": "2022-09-22T13:43:41+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.4.0", + "version": "v3.11.0", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad" + "reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", - "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/7dcdea3f2f5f473464e835be9be55283ff8cfdc3", + "reference": "7dcdea3f2f5f473464e835be9be55283ff8cfdc3", "shasum": "" }, "require": { "composer/semver": "^3.2", - "composer/xdebug-handler": "^2.0", - "doctrine/annotations": "^1.12", + "composer/xdebug-handler": "^3.0.3", + "doctrine/annotations": "^1.13", "ext-json": "*", "ext-tokenizer": "*", - "php": "^7.2.5 || ^8.0", - "php-cs-fixer/diff": "^2.0", - "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", - "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", - "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", - "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", - "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", + "php": "^7.4 || ^8.0", + "sebastian/diff": "^4.0", + "symfony/console": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/filesystem": "^5.4 || ^6.0", + "symfony/finder": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4 || ^6.0", "symfony/polyfill-mbstring": "^1.23", - "symfony/polyfill-php80": "^1.23", - "symfony/polyfill-php81": "^1.23", - "symfony/process": "^4.4.20 || ^5.0 || ^6.0", - "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php81": "^1.25", + "symfony/process": "^5.4 || ^6.0", + "symfony/stopwatch": "^5.4 || ^6.0" }, "require-dev": { "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^1.5", - "mikey179/vfsstream": "^1.6.8", + "mikey179/vfsstream": "^1.6.10", "php-coveralls/php-coveralls": "^2.5.2", "php-cs-fixer/accessible-object": "^1.1", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpspec/prophecy": "^1.15", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^8.5.21 || ^9.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "phpunitgoodpractices/polyfill": "^1.5", "phpunitgoodpractices/traits": "^1.9.1", - "symfony/phpunit-bridge": "^5.2.4 || ^6.0", - "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" + "symfony/phpunit-bridge": "^6.0", + "symfony/yaml": "^5.4 || ^6.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -1247,7 +1263,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.4.0" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.11.0" }, "funding": [ { @@ -1255,65 +1271,135 @@ "type": "github" } ], - "time": "2021-12-11T16:25:08+00:00" + "time": "2022-09-01T18:24:51+00:00" }, { - "name": "jpgraph/jpgraph", - "version": "4.0.2", + "name": "masterminds/html5", + "version": "2.7.6", "source": { "type": "git", - "url": "https://github.com/ztec/JpGraph.git", - "reference": "e82db7da6a546d3926c24c9a346226da7aa49094" + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "897eb517a343a2281f11bc5556d6548db7d93947" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ztec/JpGraph/zipball/e82db7da6a546d3926c24c9a346226da7aa49094", - "reference": "e82db7da6a546d3926c24c9a346226da7aa49094", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/897eb517a343a2281f11bc5556d6548db7d93947", + "reference": "897eb517a343a2281f11bc5556d6548db7d93947", "shasum": "" }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.7.6" + }, + "time": "2022-08-18T16:18:26+00:00" + }, + { + "name": "mitoteam/jpgraph", + "version": "10.2.4", + "source": { + "type": "git", + "url": "https://github.com/mitoteam/jpgraph.git", + "reference": "9ce4d106a89f120c7e220ea22205ef7956a7027b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/9ce4d106a89f120c7e220ea22205ef7956a7027b", + "reference": "9ce4d106a89f120c7e220ea22205ef7956a7027b", + "shasum": "" + }, + "require": { + "php": ">=5.5 <=8.2" + }, + "replace": { + "jpgraph/jpgraph": "4.0.2" + }, "type": "library", "autoload": { "classmap": [ - "lib/JpGraph.php" + "src/MtJpGraph.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "QPL 1.0" + "QPL-1.0" ], "authors": [ { "name": "JpGraph team" } ], - "description": "jpGraph, library to make graphs and charts", - "homepage": "http://jpgraph.net/", + "description": "JpGraph library composer package with PHP 8.2 support", + "homepage": "https://github.com/mitoteam/jpgraph", "keywords": [ - "chart", - "data", - "graph", - "jpgraph", - "pie" + "jpgraph" ], "support": { - "issues": "https://github.com/ztec/JpGraph/issues", - "source": "https://github.com/ztec/JpGraph/tree/4.x" + "issues": "https://github.com/mitoteam/jpgraph/issues", + "source": "https://github.com/mitoteam/jpgraph/tree/10.2.4" }, - "abandoned": true, - "time": "2017-02-23T09:44:15+00:00" + "time": "2022-09-15T05:57:43+00:00" }, { "name": "mpdf/mpdf", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/mpdf/mpdf.git", - "reference": "e511e89a66bdb066e3fbf352f00f4734d5064cbf" + "reference": "a8a22f4874157e490d41b486053a20bec42e182c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mpdf/mpdf/zipball/e511e89a66bdb066e3fbf352f00f4734d5064cbf", - "reference": "e511e89a66bdb066e3fbf352f00f4734d5064cbf", + "url": "https://api.github.com/repos/mpdf/mpdf/zipball/a8a22f4874157e490d41b486053a20bec42e182c", + "reference": "a8a22f4874157e490d41b486053a20bec42e182c", "shasum": "" }, "require": { @@ -1377,7 +1463,7 @@ "type": "custom" } ], - "time": "2022-04-18T11:50:28+00:00" + "time": "2022-08-15T08:15:09+00:00" }, { "name": "myclabs/deep-copy", @@ -1440,16 +1526,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.13.2", + "version": "v4.15.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077" + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077", - "reference": "210577fe3cf7badcc5814d99455df46564f3c077", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", "shasum": "" }, "require": { @@ -1490,9 +1576,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" }, - "time": "2021-11-30T19:35:32+00:00" + "time": "2022-09-04T07:30:47+00:00" }, { "name": "paragonie/random_compat", @@ -1701,21 +1787,21 @@ }, { "name": "phenx/php-svg-lib", - "version": "0.4.1", + "version": "0.5.0", "source": { "type": "git", "url": "https://github.com/dompdf/php-svg-lib.git", - "reference": "4498b5df7b08e8469f0f8279651ea5de9626ed02" + "reference": "76876c6cf3080bcb6f249d7d59705108166a6685" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/4498b5df7b08e8469f0f8279651ea5de9626ed02", - "reference": "4498b5df7b08e8469f0f8279651ea5de9626ed02", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/76876c6cf3080bcb6f249d7d59705108166a6685", + "reference": "76876c6cf3080bcb6f249d7d59705108166a6685", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^7.1 || ^7.2 || ^7.3 || ^7.4 || ^8.0", + "php": "^7.1 || ^8.0", "sabberworm/php-css-parser": "^8.4" }, "require-dev": { @@ -1741,61 +1827,9 @@ "homepage": "https://github.com/PhenX/php-svg-lib", "support": { "issues": "https://github.com/dompdf/php-svg-lib/issues", - "source": "https://github.com/dompdf/php-svg-lib/tree/0.4.1" + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.0" }, - "time": "2022-03-07T12:52:04+00:00" - }, - { - "name": "php-cs-fixer/diff", - "version": "v2.0.2", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", - "shasum": "" - }, - "require": { - "php": "^5.6 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", - "symfony/process": "^3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "sebastian/diff v3 backport support for PHP 5.6+", - "homepage": "https://github.com/PHP-CS-Fixer", - "keywords": [ - "diff" - ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" - }, - "time": "2020-10-14T08:32:19+00:00" + "time": "2022-09-06T12:16:56+00:00" }, { "name": "php-http/message-factory", @@ -1913,245 +1947,18 @@ }, "time": "2019-12-27T09:44:58+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "5.3.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170", - "reference": "622548b623e81ca6d78b721c5e029f4ce664f170", - "shasum": "" - }, - "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" - }, - "require-dev": { - "mockery/mockery": "~1.3.2", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" - } - ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0" - }, - "time": "2021-10-19T17:43:47+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.6.1", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "77a32518733312af16a44300404e945338981de3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3", - "reference": "77a32518733312af16a44300404e945338981de3", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*", - "psalm/phar": "^4.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1" - }, - "time": "2022-03-15T21:29:03+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "v1.15.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.2", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0 || ^7.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/v1.15.0" - }, - "time": "2021-12-08T12:19:24+00:00" - }, { "name": "phpstan/phpstan", - "version": "1.7.7", + "version": "1.8.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "cadad14ac63d8a432e01ae89ac5d0ff6fc3b16ab" + "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cadad14ac63d8a432e01ae89ac5d0ff6fc3b16ab", - "reference": "cadad14ac63d8a432e01ae89ac5d0ff6fc3b16ab", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c386ab2741e64cc9e21729f891b28b2b10fe6618", + "reference": "c386ab2741e64cc9e21729f891b28b2b10fe6618", "shasum": "" }, "require": { @@ -2175,9 +1982,13 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.7.7" + "source": "https://github.com/phpstan/phpstan/tree/1.8.6" }, "funding": [ { @@ -2188,16 +1999,12 @@ "url": "https://github.com/phpstan", "type": "github" }, - { - "url": "https://www.patreon.com/phpstan", - "type": "patreon" - }, { "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", "type": "tidelift" } ], - "time": "2022-05-31T13:58:21+00:00" + "time": "2022-09-23T09:54:39+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -2253,23 +2060,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.15", + "version": "9.2.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f" + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f", - "reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8", + "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.13.0", + "nikic/php-parser": "^4.14", "php": ">=7.3", "phpunit/php-file-iterator": "^3.0.3", "phpunit/php-text-template": "^2.0.2", @@ -2318,7 +2125,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17" }, "funding": [ { @@ -2326,7 +2133,7 @@ "type": "github" } ], - "time": "2022-03-07T09:28:20+00:00" + "time": "2022-08-30T12:24:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -2571,16 +2378,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.20", + "version": "9.5.25", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba" + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba", - "reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", + "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d", "shasum": "" }, "require": { @@ -2595,7 +2402,6 @@ "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", "phpunit/php-code-coverage": "^9.2.13", "phpunit/php-file-iterator": "^3.0.5", "phpunit/php-invoker": "^3.1.1", @@ -2603,20 +2409,16 @@ "phpunit/php-timer": "^5.0.2", "sebastian/cli-parser": "^1.0.1", "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", + "sebastian/comparator": "^4.0.8", "sebastian/diff": "^4.0.3", "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", + "sebastian/exporter": "^4.0.5", "sebastian/global-state": "^5.0.1", "sebastian/object-enumerator": "^4.0.3", "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.0", + "sebastian/type": "^3.2", "sebastian/version": "^3.0.2" }, - "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" - }, "suggest": { "ext-soap": "*", "ext-xdebug": "*" @@ -2658,7 +2460,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25" }, "funding": [ { @@ -2668,9 +2470,13 @@ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2022-04-01T12:37:26+00:00" + "time": "2022-09-25T03:44:45+00:00" }, { "name": "psr/cache", @@ -2723,20 +2529,20 @@ }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -2765,9 +2571,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.1" + "source": "https://github.com/php-fig/container/tree/1.1.2" }, - "time": "2021-03-05T17:36:06+00:00" + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/event-dispatcher", @@ -3091,16 +2897,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", "shasum": "" }, "require": { @@ -3153,7 +2959,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" }, "funding": [ { @@ -3161,7 +2967,7 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" + "time": "2022-09-14T12:41:17+00:00" }, { "name": "sebastian/complexity", @@ -3351,16 +3157,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9" + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9", - "reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", + "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", "shasum": "" }, "require": { @@ -3416,7 +3222,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" }, "funding": [ { @@ -3424,7 +3230,7 @@ "type": "github" } ], - "time": "2021-11-11T14:18:36+00:00" + "time": "2022-09-14T06:03:37+00:00" }, { "name": "sebastian/global-state", @@ -3779,16 +3585,16 @@ }, { "name": "sebastian/type", - "version": "3.0.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad" + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", - "reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", "shasum": "" }, "require": { @@ -3800,7 +3606,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -3823,7 +3629,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.0.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" }, "funding": [ { @@ -3831,7 +3637,7 @@ "type": "github" } ], - "time": "2022-03-15T09:54:48+00:00" + "time": "2022-09-12T14:47:03+00:00" }, { "name": "sebastian/version", @@ -3960,16 +3766,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.6.2", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", - "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", "shasum": "" }, "require": { @@ -4012,20 +3818,20 @@ "source": "https://github.com/squizlabs/PHP_CodeSniffer", "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" }, - "time": "2021-12-12T21:44:58+00:00" + "time": "2022-06-18T07:21:10+00:00" }, { "name": "symfony/console", - "version": "v5.4.2", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e" + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a2c6b7ced2eb7799a35375fb9022519282b5405e", - "reference": "a2c6b7ced2eb7799a35375fb9022519282b5405e", + "url": "https://api.github.com/repos/symfony/console/zipball/c072aa8f724c3af64e2c7a96b796a4863d24dba1", + "reference": "c072aa8f724c3af64e2c7a96b796a4863d24dba1", "shasum": "" }, "require": { @@ -4095,7 +3901,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.2" + "source": "https://github.com/symfony/console/tree/v5.4.12" }, "funding": [ { @@ -4111,20 +3917,20 @@ "type": "tidelift" } ], - "time": "2021-12-20T16:11:12+00:00" + "time": "2022-08-17T13:18:05+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/6f981ee24cf69ee7ce9736146d1c57c2780598a8", - "reference": "6f981ee24cf69ee7ce9736146d1c57c2780598a8", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -4162,7 +3968,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" }, "funding": [ { @@ -4178,20 +3984,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.4.0", + "version": "v5.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "27d39ae126352b9fa3be5e196ccf4617897be3eb" + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/27d39ae126352b9fa3be5e196ccf4617897be3eb", - "reference": "27d39ae126352b9fa3be5e196ccf4617897be3eb", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", + "reference": "8e6ce1cc0279e3ff3c8ff0f43813bc88d21ca1bc", "shasum": "" }, "require": { @@ -4247,7 +4053,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.9" }, "funding": [ { @@ -4263,20 +4069,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-05-05T16:45:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a" + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", - "reference": "66bea3b09be61613cd3b4043a65a8ec48cfa6d2a", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", "shasum": "" }, "require": { @@ -4326,7 +4132,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" }, "funding": [ { @@ -4342,20 +4148,20 @@ "type": "tidelift" } ], - "time": "2021-07-12T14:48:14+00:00" + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.0", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "731f917dc31edcffec2c6a777f3698c33bea8f01" + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/731f917dc31edcffec2c6a777f3698c33bea8f01", - "reference": "731f917dc31edcffec2c6a777f3698c33bea8f01", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/2d67c1f9a1937406a9be3171b4b22250c0a11447", + "reference": "2d67c1f9a1937406a9be3171b4b22250c0a11447", "shasum": "" }, "require": { @@ -4390,7 +4196,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.0" + "source": "https://github.com/symfony/filesystem/tree/v5.4.12" }, "funding": [ { @@ -4406,20 +4212,20 @@ "type": "tidelift" } ], - "time": "2021-10-28T13:39:27+00:00" + "time": "2022-08-02T13:48:16+00:00" }, { "name": "symfony/finder", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e77046c252be48c48a40816187ed527703c8f76c" + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e77046c252be48c48a40816187ed527703c8f76c", - "reference": "e77046c252be48c48a40816187ed527703c8f76c", + "url": "https://api.github.com/repos/symfony/finder/zipball/7872a66f57caffa2916a584db1aa7f12adc76f8c", + "reference": "7872a66f57caffa2916a584db1aa7f12adc76f8c", "shasum": "" }, "require": { @@ -4453,7 +4259,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.2" + "source": "https://github.com/symfony/finder/tree/v5.4.11" }, "funding": [ { @@ -4469,20 +4275,20 @@ "type": "tidelift" } ], - "time": "2021-12-15T11:06:13+00:00" + "time": "2022-07-29T07:37:50+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.4.0", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "b0fb78576487af19c500aaddb269fd36701d4847" + "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b0fb78576487af19c500aaddb269fd36701d4847", - "reference": "b0fb78576487af19c500aaddb269fd36701d4847", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/54f14e36aa73cb8f7261d7686691fd4d75ea2690", + "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690", "shasum": "" }, "require": { @@ -4522,7 +4328,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v5.4.11" }, "funding": [ { @@ -4538,20 +4344,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-07-20T13:00:38+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -4566,7 +4372,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4604,7 +4410,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" }, "funding": [ { @@ -4620,20 +4426,20 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535" + "reference": "433d05519ce6990bf3530fba6957499d327395c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2", + "reference": "433d05519ce6990bf3530fba6957499d327395c2", "shasum": "" }, "require": { @@ -4645,7 +4451,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4653,12 +4459,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4685,7 +4491,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0" }, "funding": [ { @@ -4701,20 +4507,20 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -4726,7 +4532,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4734,12 +4540,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4769,7 +4575,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" }, "funding": [ { @@ -4785,20 +4591,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010" + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fba8933c384d6476ab14fb7b8526e5287ca7e010", - "reference": "fba8933c384d6476ab14fb7b8526e5287ca7e010", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85", + "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85", "shasum": "" }, "require": { @@ -4807,7 +4613,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4815,12 +4621,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4848,7 +4654,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0" }, "funding": [ { @@ -4864,20 +4670,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.23.1", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be" + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/1100343ed1a92e3a38f9ae122fc0eb21602547be", - "reference": "1100343ed1a92e3a38f9ae122fc0eb21602547be", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace", + "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace", "shasum": "" }, "require": { @@ -4886,7 +4692,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4894,12 +4700,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -4931,7 +4737,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0" }, "funding": [ { @@ -4947,20 +4753,20 @@ "type": "tidelift" } ], - "time": "2021-07-28T13:41:28+00:00" + "time": "2022-05-10T07:21:04+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.23.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "e66119f3de95efc359483f810c4c3e6436279436" + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436", - "reference": "e66119f3de95efc359483f810c4c3e6436279436", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1", + "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1", "shasum": "" }, "require": { @@ -4969,7 +4775,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4977,12 +4783,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -5010,7 +4816,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0" }, "funding": [ { @@ -5026,20 +4832,20 @@ "type": "tidelift" } ], - "time": "2021-05-21T13:25:03+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/process", - "version": "v5.4.2", + "version": "v5.4.11", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4" + "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", - "reference": "2b3ba8722c4aaf3e88011be5e7f48710088fb5e4", + "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", + "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", "shasum": "" }, "require": { @@ -5072,7 +4878,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.2" + "source": "https://github.com/symfony/process/tree/v5.4.11" }, "funding": [ { @@ -5088,26 +4894,26 @@ "type": "tidelift" } ], - "time": "2021-12-27T21:01:00+00:00" + "time": "2022-06-27T16:58:25+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.0", + "version": "v2.5.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc" + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", - "reference": "1ab11b933cd6bc5464b08e81e2c5b07dec58b0fc", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", "shasum": "" }, "require": { "php": ">=7.2.5", "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -5155,7 +4961,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.0" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" }, "funding": [ { @@ -5171,20 +4977,20 @@ "type": "tidelift" } ], - "time": "2021-11-04T16:48:04+00:00" + "time": "2022-05-30T19:17:29+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.4.0", + "version": "v5.4.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "208ef96122bfed82a8f3a61458a07113a08bdcfe" + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/208ef96122bfed82a8f3a61458a07113a08bdcfe", - "reference": "208ef96122bfed82a8f3a61458a07113a08bdcfe", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", + "reference": "4d04b5c24f3c9a1a168a131f6cbe297155bc0d30", "shasum": "" }, "require": { @@ -5217,7 +5023,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.0" + "source": "https://github.com/symfony/stopwatch/tree/v5.4.5" }, "funding": [ { @@ -5233,20 +5039,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T10:19:22+00:00" + "time": "2022-02-18T16:06:09+00:00" }, { "name": "symfony/string", - "version": "v5.4.2", + "version": "v5.4.12", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d" + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", - "reference": "e6a5d5ecf6589c5247d18e0e74e30b11dfd51a3d", + "url": "https://api.github.com/repos/symfony/string/zipball/2fc515e512d721bf31ea76bd02fe23ada4640058", + "reference": "2fc515e512d721bf31ea76bd02fe23ada4640058", "shasum": "" }, "require": { @@ -5268,12 +5074,12 @@ }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -5303,7 +5109,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.2" + "source": "https://github.com/symfony/string/tree/v5.4.12" }, "funding": [ { @@ -5319,20 +5125,20 @@ "type": "tidelift" } ], - "time": "2021-12-16T21:52:00+00:00" + "time": "2022-08-12T17:03:11+00:00" }, { "name": "tecnickcom/tcpdf", - "version": "6.4.4", + "version": "6.5.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "42cd0f9786af7e5db4fcedaa66f717b0d0032320" + "reference": "cc54c1503685e618b23922f53635f46e87653662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/42cd0f9786af7e5db4fcedaa66f717b0d0032320", - "reference": "42cd0f9786af7e5db4fcedaa66f717b0d0032320", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/cc54c1503685e618b23922f53635f46e87653662", + "reference": "cc54c1503685e618b23922f53635f46e87653662", "shasum": "" }, "require": { @@ -5383,7 +5189,7 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.4.4" + "source": "https://github.com/tecnickcom/TCPDF/tree/6.5.0" }, "funding": [ { @@ -5391,7 +5197,7 @@ "type": "custom" } ], - "time": "2021-12-31T08:39:24+00:00" + "time": "2022-08-12T07:50:54+00:00" }, { "name": "theseer/tokenizer", @@ -5442,64 +5248,6 @@ } ], "time": "2021-07-28T10:34:58+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" - }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "support": { - "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" - }, - "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], @@ -5526,5 +5274,5 @@ "ext-zlib": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.2.0" } diff --git a/docs/index.md b/docs/index.md index ff137c26..63d71655 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,7 +53,7 @@ composer require phpoffice/phpspreadsheet --prefer-source ``` If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: -```json lines +```json { "require": { "phpoffice/phpspreadsheet": "^1.23" diff --git a/docs/topics/calculation-engine.md b/docs/topics/calculation-engine.md index 4fd300e8..7dc838f7 100644 --- a/docs/topics/calculation-engine.md +++ b/docs/topics/calculation-engine.md @@ -22,6 +22,13 @@ with PhpSpreadsheet, it evaluates to the value "64": ![09-command-line-calculation.png](./images/09-command-line-calculation.png) +When writing a formula to a cell, formulae should always be set as they would appear in an English version of Microsoft Office Excel, and PhpSpreadsheet handles all formulae internally in this format. This means that the following rules hold: + + - Decimal separator is `.` (period) + - Function argument separator is `,` (comma) + - Matrix row separator is `;` (semicolon) + - English function names must be used + Another nice feature of PhpSpreadsheet's formula parser, is that it can automatically adjust a formula when inserting/removing rows/columns. Here's an example: @@ -43,6 +50,11 @@ inserted 2 new rows), changed to "SUM(E4:E11)". Also, the inserted cells duplicate style information of the previous cell, just like Excel's behaviour. Note that you can both insert rows and columns. +If you want to "anchor" a specific cell for a formula, then you prefix the column and/or the row with a `$` symbol, exactly as you would in MS Excel itself. +So if a formula contains "SUM(E$4:E9)", and you insert 2 new rows after row 1, the formula will be adjusted to read "SUM(E$4:E11)", with the `$` fixing row 4 as the start of the range. + + + ## Calculation Cache Once the Calculation engine has evaluated the formula in a cell, the result diff --git a/docs/topics/images/12-01-MergeCells-Options-2.png b/docs/topics/images/12-01-MergeCells-Options-2.png new file mode 100644 index 00000000..5e745fc9 Binary files /dev/null and b/docs/topics/images/12-01-MergeCells-Options-2.png differ diff --git a/docs/topics/images/12-01-MergeCells-Options-3.png b/docs/topics/images/12-01-MergeCells-Options-3.png new file mode 100644 index 00000000..30ad346e Binary files /dev/null and b/docs/topics/images/12-01-MergeCells-Options-3.png differ diff --git a/docs/topics/images/12-01-MergeCells-Options.png b/docs/topics/images/12-01-MergeCells-Options.png new file mode 100644 index 00000000..34c8f5e3 Binary files /dev/null and b/docs/topics/images/12-01-MergeCells-Options.png differ diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 0bcc1909..55929e85 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -282,6 +282,7 @@ versions of Microsoft Excel. **Excel 2003 XML limitations** Please note that Excel 2003 XML format has some limits regarding to styling cells and handling large spreadsheets via PHP. +Also, only files using charset UTF-8 are supported. ### \PhpOffice\PhpSpreadsheet\Reader\Xml @@ -701,6 +702,7 @@ extension. **HTML limitations** Please note that HTML file format has some limits regarding to styling cells, number formatting, ... +Also, only files using charset UTF-8 are supported. ### \PhpOffice\PhpSpreadsheet\Reader\Html diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index f25a9119..04a4e6aa 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1161,7 +1161,7 @@ A column's width can be set using the following code: $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(12); ``` -If you want to set a column width using a different unit of measure, +If you want to set a column width using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the width value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1258,7 +1258,7 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. -If you want to set a row height using a different unit of measure, +If you want to set a row height using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the height value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1332,23 +1332,73 @@ rows (default), or above. The following code adds the summary above: $spreadsheet->getActiveSheet()->setShowSummaryBelow(false); ``` -## Merge/unmerge cells +## Merge/Unmerge cells -If you have a big piece of data you want to display in a worksheet, you -can merge two or more cells together, to become one cell. This can be -done using the following code: +If you have a big piece of data you want to display in a worksheet, or a +heading that needs to span multiple sub-heading columns, you can merge +two or more cells together, to become one cell. This can be done using +the following code: ```php $spreadsheet->getActiveSheet()->mergeCells('A18:E22'); ``` -Removing a merge can be done using the unmergeCells method: +Removing a merge can be done using the `unmergeCells()` method: ```php $spreadsheet->getActiveSheet()->unmergeCells('A18:E22'); ``` -## Inserting rows/columns +MS Excel itself doesn't yet offer the functionality to simply hide the merged cells, or to merge the content of cells into a single cell, but it is available in Open/Libre Office. + +### Merge with MERGE_CELL_CONTENT_EMPTY + +The default behaviour is to empty all cells except for the top-left corner cell in the merge range; and this is also the default behaviour for the `mergeCells()` method in PhpSpreadsheet. +When this behaviour is applied, those cell values will be set to null; and if they are subsequently Unmerged, they will be empty cells. + +Passing an extra flag value to the `mergeCells()` method in PhpSpreadsheet can change this behaviour. + +![12-01-MergeCells-Options.png](./images/12-01-MergeCells-Options.png) + +Possible flag values are: +- Worksheet::MERGE_CELL_CONTENT_EMPTY (the default) +- Worksheet::MERGE_CELL_CONTENT_HIDE +- Worksheet::MERGE_CELL_CONTENT_MERGE + +### Merge with MERGE_CELL_CONTENT_HIDE + +The first alternative, available only in OpenOffice, is to hide those cells, but to leave their content intact. +When a file saved as `Xlsx` in those applications is opened in MS Excel, and those cells are unmerged, the original content will still be present. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_HIDE); +``` + +Will replicate that behaviour. + +### Merge with MERGE_CELL_CONTENT_MERGE + +The second alternative, available in both OpenOffice and LibreOffice is to merge the content of every cell in the merge range into the top-left cell, while setting those hidden cells to empty. + +```php +$spreadsheet->getActiveSheet()->mergeCells('A1:C3', Worksheet::MERGE_CELL_CONTENT_MERGE); +``` + +Particularly when the merged cells contain formulae, the logic for this merge seems strange: +walking through the merge range, each cell is calculated in turn, and appended to the "master" cell, then it is emptied, so any subsequent calculations that reference the cell see an empty cell, not the pre-merge value. +For example, suppose our spreadsheet contains + +![12-01-MergeCells-Options-2.png](./images/12-01-MergeCells-Options-2.png) + +where `B2` is the formula `=5-B1` and `C2` is the formula `=A2/B2`, +and we want to merge cells `A2` to `C2` with all the cell values merged. +The result is: + +![12-01-MergeCells-Options-3.png](./images/12-01-MergeCells-Options-3.png) + +The cell value `12` from cell `A2` is fixed; the value from `B2` is the result of the formula `=5-B1` (`4`, which is appended to our merged value), and cell `B2` is then emptied, so when we evaluate cell `C2` with the formula `=A2/B2` it gives us `12 / 0` which results in a `#DIV/0!` error (so the error `#DIV/0!` is appended to our merged value rather than the original calculation result of `3`). + +## Inserting or Removing rows/columns You can insert/remove rows/columns at a specific position. The following code inserts 2 new rows, right before row 7: @@ -1356,6 +1406,23 @@ code inserts 2 new rows, right before row 7: ```php $spreadsheet->getActiveSheet()->insertNewRowBefore(7, 2); ``` +while +```php +$spreadsheet->getActiveSheet()->removeRow(7, 2); +``` +will remove 2 rows starting at row number 7 (ie. rows 7 and 8). + +Equivalent methods exist for inserting/removing columns: + +```php +$spreadsheet->getActiveSheet()->removeColumn('C', 2); +``` + +All subsequent rows (or columns) will be moved to allow the insertion (or removal) with all formulae referencing thise cells adjusted accordingly. + +Note that this is a fairly intensive process, particularly with large worksheets, and especially if you are inserting/removing rows/columns from near beginning of the worksheet. + +If you need to insert/remove several consecutive rows/columns, always use the second argument rather than making multiple calls to insert/remove a single row/column if possible. ## Add a drawing to a worksheet @@ -1653,7 +1720,7 @@ $spreadsheet->getActiveSheet()->getDefaultColumnDimension()->setWidth(12); Excel measures column width in its own proprietary units, based on the number of characters that will be displayed in the default font. -If you want to set the default column width using a different unit of measure, +If you want to set the default column width using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the width value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), @@ -1676,7 +1743,7 @@ Excel measures row height in points, where 1 pt is 1/72 of an inch (or about 0.35mm). The default value is 12.75 pts; and the permitted range of values is between 0 and 409 pts, where 0 pts is a hidden row. -If you want to set a row height using a different unit of measure, +If you want to set a row height using a different UoM (Unit of Measure), then you can do so by telling PhpSpreadsheet what UoM the height value that you are setting is measured in. Valid units are `pt` (points), `px` (pixels), `pc` (pica), `in` (inches), diff --git a/infra/DocumentGenerator.php b/infra/DocumentGenerator.php index e2c3c86c..8a6be076 100644 --- a/infra/DocumentGenerator.php +++ b/infra/DocumentGenerator.php @@ -41,7 +41,7 @@ class DocumentGenerator private static function tableRow(array $lengths, ?array $values = null): string { $result = ''; - foreach (array_map(null, $lengths, $values ?? []) as $i => [$length, $value]) { + foreach (array_map(/** @scrutinizer ignore-type */ null, $lengths, $values ?? []) as $i => [$length, $value]) { $pad = $value === null ? '-' : ' '; if ($i > 0) { $result .= '|' . $pad; diff --git a/infra/LocaleGenerator.php b/infra/LocaleGenerator.php index bb97754d..992bde17 100644 --- a/infra/LocaleGenerator.php +++ b/infra/LocaleGenerator.php @@ -146,7 +146,7 @@ class LocaleGenerator $translationValue = $translationCell->getValue(); if ($this->isFunctionCategoryEntry($translationCell)) { $this->writeFileSectionHeader($functionFile, "{$translationValue} ({$functionName})"); - } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions)) { + } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions) && substr($functionName, 0, 1) !== '*') { $this->log("Function {$functionName} is not defined in PhpSpreadsheet"); } elseif (!empty($translationValue)) { $functionTranslation = "{$functionName} = {$translationValue}" . self::EOL; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 974bb3d0..2b5c91e6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,11 +5,6 @@ parameters: count: 3 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Cannot call method attach\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Cannot call method cellExists\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" count: 4 @@ -30,71 +25,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToEnglish\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToEnglish\\(\\) has parameter \\$formula with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToLocale\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_translateFormulaToLocale\\(\\) has parameter \\$formula with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:dataTestReference\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:dataTestReference\\(\\) has parameter \\$operandData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:getTokensAsString\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:getTokensAsString\\(\\) has parameter \\$tokens with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:localeFunc\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:localeFunc\\(\\) has parameter \\$function with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:validateBinaryOperand\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:validateBinaryOperand\\(\\) has parameter \\$operand with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:validateBinaryOperand\\(\\) has parameter \\$stack with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Offset 'value' does not exist on array\\|null\\.$#" count: 5 @@ -110,81 +40,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Parameter \\#1 \\$str of function preg_quote expects string, int\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Parameter \\#2 \\$worksheet of static method PhpOffice\\\\PhpSpreadsheet\\\\DefinedName\\:\\:resolveName\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/Calculation.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$cellStack has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$comparisonOperators has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$controlFunctions has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$cyclicFormulaCell has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceFromExcel has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceFromLocale has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceToExcel has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$functionReplaceToLocale has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$localeFunctions has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$operatorAssociativity has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$operatorPrecedence has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$phpSpreadsheetFunctions has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$returnArrayAsType has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:\\$spreadsheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\|null\\.$#" count: 1 @@ -515,11 +375,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/FormulaParser.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Functions\\:\\:ifCondition\\(\\) has no return type specified\\.$#" count: 1 @@ -1065,11 +920,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/TextData/Text.php - - - message: "#^Elseif branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Cell\\:\\:getFormulaAttributes\\(\\) has no return type specified\\.$#" count: 1 @@ -1110,451 +960,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Cell/Coordinate.php - - - message: "#^Call to an undefined method object\\:\\:render\\(\\)\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$legend \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$majorGridlines \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$minorGridlines \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$plotArea \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$title \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$worksheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$xAxis \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$xAxisLabel \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$yAxis \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Axis\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\:\\:\\$yAxisLabel \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Chart.php - - - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\DataSeriesValues and null will always evaluate to false\\.$#" - count: 2 - path: src/PhpSpreadsheet/Chart/DataSeries.php - - - - message: "#^Parameter \\#1 \\$color of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Parameter \\#2 \\$alpha of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects int, int\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Parameter \\#3 \\$colorType of method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:setGlowColor\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$glowProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$lineProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$objectState has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$shadowProperties has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\GridLines\\:\\:\\$softEdges has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/GridLines.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Legend.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Legend\\:\\:\\$positionXLref has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Legend.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\PlotArea\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/PlotArea.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getArrayElementsValue\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getArrayElementsValue\\(\\) has parameter \\$elements with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getArrayElementsValue\\(\\) has parameter \\$properties with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getLineStyleArrowSize\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getLineStyleArrowSize\\(\\) has parameter \\$arrayKaySelector with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getLineStyleArrowSize\\(\\) has parameter \\$arraySelector with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getShadowPresetsMap\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Properties\\:\\:getShadowPresetsMap\\(\\) has parameter \\$presetsOption with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Properties.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$datasetLabels with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$labelCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatDataSetLabels\\(\\) has parameter \\$rotation with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatPointMarker\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatPointMarker\\(\\) has parameter \\$markerID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:formatPointMarker\\(\\) has parameter \\$seriesPlot with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:getCaption\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:getCaption\\(\\) has parameter \\$captionElement with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageAdjustValues\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageAdjustValues\\(\\) has parameter \\$dataValues with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageAdjustValues\\(\\) has parameter \\$sumValues with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageSumCalculation\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageSumCalculation\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:percentageSumCalculation\\(\\) has parameter \\$seriesCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderAreaChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderAreaChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderBarChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderBarChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderBubbleChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCartesianPlotArea\\(\\) has parameter \\$type with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderCombinationChart\\(\\) has parameter \\$outputDestination with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderContourChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderContourChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderLineChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderLineChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$doughnut with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPieChart\\(\\) has parameter \\$multiplePlots with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotBar\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotBar\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotContour\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$combination with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$dimensions with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$filled with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotLine\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotRadar\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotScatter\\(\\) has parameter \\$bubble with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotScatter\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderPlotStock\\(\\) has parameter \\$groupID with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderRadarChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderScatterChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:renderStockChart\\(\\) has parameter \\$groupCount with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$chart has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$colourSet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$graph has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$height has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$markSet has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$plotColour has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$plotMark has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Renderer\\\\JpGraph\\:\\:\\$width has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Renderer/JpGraph.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Title\\:\\:\\$layout \\(PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Layout\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Chart/Title.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Memory\\:\\:\\$cache has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Collection/Memory.php - - message: "#^Parameter \\#1 \\$namedRange of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:addNamedRange\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\NamedRange, \\$this\\(PhpOffice\\\\PhpSpreadsheet\\\\DefinedName\\) given\\.$#" count: 1 @@ -1570,161 +975,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/DefinedName.php - - - message: "#^Cannot use array destructuring on array\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Dimension.php - - - - message: "#^Cannot call method setBold\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setColor\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setItalic\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setName\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setSize\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setStrikethrough\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Cannot call method setUnderline\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:startFontTag\\(\\) has parameter \\$tag with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$bold has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$color has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$colourMap has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$endTagCallbacks has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$face has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$italic has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$size has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$stack has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$startTagCallbacks has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$strikethrough has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$stringData has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$subscript has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$superscript has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Html\\:\\:\\$underline has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Sample\\:\\:getSamples\\(\\) should return array\\\\> but returns array\\\\>\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Helper\\\\Sample\\:\\:log\\(\\) has parameter \\$message with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#1 \\$directory of class RecursiveDirectoryIterator constructor expects string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#1 \\$filename of function unlink expects string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#1 \\$path of function pathinfo expects string, array\\|string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Helper/Sample.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\IOFactory\\:\\:createReader\\(\\) should return PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\IReader but returns object\\.$#" count: 1 @@ -2085,51 +1335,6 @@ parameters: count: 8 path: src/PhpSpreadsheet/Reader/Xls.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\Color\\\\BIFF5\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\Color\\\\BIFF8\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\Color\\\\BuiltIn\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\ErrorCode\\:\\:\\$map has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/ErrorCode.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\RC4\\:\\:\\$i has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\RC4\\:\\:\\$j has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xls\\\\RC4\\:\\:\\$s has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xls/RC4.php - - - - message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - - - message: "#^Cannot access offset 0 on array\\\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Cannot access property \\$r on SimpleXMLElement\\|null\\.$#" count: 2 @@ -2700,51 +1905,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php - - - message: "#^Cannot access offset 0 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 2 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 4 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Cannot access offset 6 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Parameter \\#1 \\$size of function imagettfbbox expects float, float\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Parameter \\#2 \\$defaultFont of static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Drawing\\:\\:pixelsToCellDimension\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Font\\:\\:\\$autoSizeMethods has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - - - message: "#^Variable \\$cellText on left side of \\?\\? always exists and is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/Font.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\EigenvalueDecomposition\\:\\:\\$cdivi has no type specified\\.$#" count: 1 @@ -2765,11 +1925,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php - - - message: "#^Call to function is_string\\(\\) with float\\|int will always evaluate to false\\.$#" - count: 5 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:__construct\\(\\) has parameter \\$args with no type specified\\.$#" count: 1 @@ -2850,11 +2005,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - message: "#^Result of && is always false\\.$#" - count: 10 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 19 @@ -3095,66 +2245,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLERead.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:formatNumber\\(\\) should return string but returns array\\|string\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbIsUpper\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbIsUpper\\(\\) has parameter \\$character with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbStrSplit\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:mbStrSplit\\(\\) has parameter \\$string with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:sanitizeUTF8\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Parameter \\#1 \\$string of function strlen expects string, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, float given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:\\$decimalSeparator \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Static property PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\StringHelper\\:\\:\\$thousandsSeparator \\(string\\) in isset\\(\\) is not nullable\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Variable \\$textValue on left side of \\?\\? always exists and is not nullable\\.$#" - count: 3 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - - - message: "#^Static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:validateTimeZone\\(\\) is unused\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/TimeZone.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\Trend\\\\BestFit\\:\\:calculateGoodnessOfFit\\(\\) has parameter \\$const with no type specified\\.$#" count: 1 @@ -3345,11 +2435,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Comparison operation \"\\<\\=\" between int\\ and 1000 is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Parameter \\#1 \\$worksheet of method PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet\\:\\:getIndex\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet, PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null given\\.$#" count: 1 @@ -3360,21 +2445,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Result of \\|\\| is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Strict comparison using \\=\\=\\= between PhpOffice\\\\PhpSpreadsheet\\\\Spreadsheet and null will always evaluate to false\\.$#" count: 1 path: src/PhpSpreadsheet/Spreadsheet.php - - - message: "#^Strict comparison using \\=\\=\\= between string and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Spreadsheet.php - - message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 @@ -3600,11 +2675,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/CellIterator.php - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Column\\:\\:\\$parent \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Column.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\:\\:\\$color \\(PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Color\\|null\\.$#" count: 1 @@ -3615,26 +2685,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - message: "#^Parameter \\#1 \\$value of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\PageSetup\\:\\:setFirstPageNumber\\(\\) expects int, null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\PageSetup\\:\\:\\$pageOrder has no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Strict comparison using \\=\\=\\= between int\\ and null will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/PageSetup.php - - - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Row\\:\\:\\$worksheet \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Worksheet/Row.php - - message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\SheetView\\:\\:\\$sheetViewTypes has no type specified\\.$#" count: 1 @@ -3735,321 +2785,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Worksheet/Worksheet.php - - - message: "#^Call to function array_key_exists\\(\\) with int and array\\{none\\: 'none', dashDot\\: '1px dashed', dashDotDot\\: '1px dotted', dashed\\: '1px dashed', dotted\\: '1px dotted', double\\: '3px double', hair\\: '1px solid', medium\\: '2px solid', \\.\\.\\.\\} will always evaluate to false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 'mime' on array\\|false\\.$#" - count: 2 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 0 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot call method getSubscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Cannot call method getSuperscript\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$candidateSpannedRow with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$sheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:calculateSpansOmitRows\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateHTMLFooter\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has parameter \\$desc with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateMeta\\(\\) has parameter \\$val with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$cellAddress with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$columnNumber with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellCss\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cellType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellData\\(\\) has parameter \\$cssClass with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValue\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValue\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValueRich\\(\\) has parameter \\$cell with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowCellDataValueRich\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowIncludeCharts\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowIncludeCharts\\(\\) has parameter \\$coordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$colSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowSpans\\(\\) has parameter \\$rowSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cellData with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cellType with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$colNum with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$colSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$coordinate with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$cssClass with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$rowSpan with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateRowWriteCell\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetPrep\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has parameter \\$rowMin with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetStarts\\(\\) has parameter \\$sheet with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$row with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$tbodyStart with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$theadEnd with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateSheetTags\\(\\) has parameter \\$theadStart with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableFooter\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$html with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$id with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTag\\(\\) has parameter \\$sheetIndex with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTagInline\\(\\) has no return type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:generateTableTagInline\\(\\) has parameter \\$id with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$borderStyle of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapBorderStyle\\(\\) expects int, string given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$font of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:createCSSStyleFont\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font, PhpOffice\\\\PhpSpreadsheet\\\\Style\\\\Font\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$hAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapHAlign\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#1 \\$vAlign of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Html\\:\\:mapVAlign\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#2 \\$length of function fread expects int\\<0, max\\>, int\\<0, max\\>\\|false given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Parameter \\#3 \\$use_include_path of function fopen expects bool, int given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - - - message: "#^Ternary operator condition is always true\\.$#" - count: 1 - path: src/PhpSpreadsheet/Writer/Html.php - - message: "#^Negated boolean expression is always false\\.$#" count: 1 @@ -4272,7 +3007,7 @@ parameters: - message: "#^Cannot access offset 1 on array\\|false\\.$#" - count: 2 + count: 1 path: src/PhpSpreadsheet/Writer/Xls/Worksheet.php - diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0979eaed..30bd6c2f 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,23 +9,15 @@ parameters: paths: - src/ - tests/ + excludePaths: + - src/PhpSpreadsheet/Chart/Renderer/JpGraph.php + - src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php parallel: processTimeout: 300.0 checkMissingIterableValueType: false ignoreErrors: - - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy) expects (GdImage|resource), GdImage\|resource given\.$~' + - '~^Parameter \#1 \$im(age)? of function (imagedestroy|imageistruecolor|imagealphablending|imagesavealpha|imagecolortransparent|imagecolorsforindex|imagesavealpha|imagesx|imagesy|imagepng) expects (GdImage|resource), GdImage\|resource given\.$~' - '~^Parameter \#2 \$src_im(age)? of function imagecopy expects (GdImage|resource), GdImage\|resource given\.$~' # Accept a bit anything for assert methods - '~^Parameter \#2 .* of static method PHPUnit\\Framework\\Assert\:\:assert\w+\(\) expects .*, .* given\.$~' - '~^Method PhpOffice\\PhpSpreadsheetTests\\.*\:\:test.*\(\) has parameter \$args with no type specified\.$~' - - # Ignore all JpGraph issues - - '~^Constant (MARK_CIRCLE|MARK_CROSS|MARK_DIAMOND|MARK_DTRIANGLE|MARK_FILLEDCIRCLE|MARK_SQUARE|MARK_STAR|MARK_UTRIANGLE|MARK_X|SIDE_RIGHT) not found\.$~' - - '~^Instantiated class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot) not found\.$~' - - '~^Call to method .*\(\) on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' - - '~^Access to property .* on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' - - # Some issues in Xls/Parser between 1.6.3 and 1.7.7 - - - message: "#^Offset '(left|right|value)' does not exist on (non-empty-array\\|string|array\\|null)\\.$#" - path: src/PhpSpreadsheet/Writer/Xls/Parser.php diff --git a/samples/Basic/26_Utf8.php b/samples/Basic/26_Utf8.php index 52953251..52a64509 100644 --- a/samples/Basic/26_Utf8.php +++ b/samples/Basic/26_Utf8.php @@ -12,12 +12,10 @@ $spreadsheet = $reader->load(__DIR__ . '/../templates/26template.xlsx'); // at this point, we could do some manipulations with the template, but we skip this step $helper->write($spreadsheet, __FILE__, ['Xlsx', 'Xls', 'Html']); -if (\PHP_VERSION_ID < 80000) { - // Export to PDF (.pdf) - $helper->log('Write to PDF format'); - IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class); - $helper->write($spreadsheet, __FILE__, ['Pdf']); -} +// Export to PDF (.pdf) +$helper->log('Write to PDF format'); +IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class); +$helper->write($spreadsheet, __FILE__, ['Pdf']); // Remove first two rows with field headers before exporting to CSV $helper->log('Removing first two heading rows for CSV export'); diff --git a/samples/Basic/49_alignment.php b/samples/Basic/49_alignment.php new file mode 100644 index 00000000..83fdb3d4 --- /dev/null +++ b/samples/Basic/49_alignment.php @@ -0,0 +1,80 @@ +log('Create new Spreadsheet object'); +$spreadsheet = new Spreadsheet(); +$spreadsheet->getProperties()->setTitle('Alignment'); +$sheet = $spreadsheet->getActiveSheet(); +$hi = 'Hi There'; +$ju = 'This is a longer than normal sentence'; +$sheet->fromArray([ + ['', 'default', 'bottom', 'top', 'center', 'justify', 'distributed'], + ['default', $hi, $hi, $hi, $hi, $hi, $hi], + ['left', $hi, $hi, $hi, $hi, $hi, $hi], + ['right', $hi, $hi, $hi, $hi, $hi, $hi], + ['center', $hi, $hi, $hi, $hi, $hi, $hi], + ['justify', $ju, $ju, $ju, $ju, $ju, $ju], + ['distributed', $ju, $ju, $ju, $ju, $ju, $ju], +]); +$sheet->getColumnDimension('B')->setWidth(20); +$sheet->getColumnDimension('C')->setWidth(20); +$sheet->getColumnDimension('D')->setWidth(20); +$sheet->getColumnDimension('E')->setWidth(20); +$sheet->getColumnDimension('F')->setWidth(20); +$sheet->getColumnDimension('G')->setWidth(20); +$sheet->getRowDimension(2)->setRowHeight(30); +$sheet->getRowDimension(3)->setRowHeight(30); +$sheet->getRowDimension(4)->setRowHeight(30); +$sheet->getRowDimension(5)->setRowHeight(30); +$sheet->getRowDimension(6)->setRowHeight(40); +$sheet->getRowDimension(7)->setRowHeight(40); +$minRow = 2; +$maxRow = 7; +$minCol = 'B'; +$maxCol = 'g'; +$sheet->getStyle("C$minRow:C$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_BOTTOM); +$sheet->getStyle("D$minRow:D$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_TOP); +$sheet->getStyle("E$minRow:E$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); +$sheet->getStyle("F$minRow:F$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_JUSTIFY); +$sheet->getStyle("G$minRow:G$maxRow") + ->getAlignment() + ->setVertical(Alignment::VERTICAL_DISTRIBUTED); +$sheet->getStyle("{$minCol}3:{$maxCol}3") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_LEFT); +$sheet->getStyle("{$minCol}4:{$maxCol}4") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_RIGHT); +$sheet->getStyle("{$minCol}5:{$maxCol}5") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER); +$sheet->getStyle("{$minCol}6:{$maxCol}6") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_JUSTIFY); +$sheet->getStyle("{$minCol}7:{$maxCol}7") + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_DISTRIBUTED); + +$sheet->getCell('A9')->setValue('Center Continuous A9-C9'); +$sheet->getStyle('A9:C9') + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER_CONTINUOUS); +$sheet->getCell('A10')->setValue('Fill'); +$sheet->getStyle('A10') + ->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_FILL); +$sheet->setSelectedCells('A1'); + +$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Html', 'Xls']); diff --git a/samples/Calculations/Database/DAVERAGE.php b/samples/Calculations/Database/DAVERAGE.php index 92d84014..fa388c8b 100644 --- a/samples/Calculations/Database/DAVERAGE.php +++ b/samples/Calculations/Database/DAVERAGE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the average of selected database entries.'); +$category = 'Database'; +$functionName = 'DAVERAGE'; +$description = 'Returns the average of selected database entries that match criteria'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DAVERAGE(A4:E10,3,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DAVERAGE() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DAVERAGE() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DCOUNT.php b/samples/Calculations/Database/DCOUNT.php index d869a4bc..68d6be18 100644 --- a/samples/Calculations/Database/DCOUNT.php +++ b/samples/Calculations/Database/DCOUNT.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Counts the cells that contain numbers in a database.'); + +$category = 'Database'; +$functionName = 'DCOUNT'; +$description = 'Counts the cells that contain numbers in a set of database records that match criteria'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -14,9 +19,9 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ['Apple', 18, 20, 14, 105.00], ['Pear', 12, 12, 10, 96.00], ['Cherry', 13, 14, 9, 105.00], - ['Apple', 14, 15, 10, 75.00], - ['Pear', 9, 8, 8, 76.80], - ['Apple', 8, 9, 6, 45.00], + ['Apple', 14, 'N/A', 10, 75.00], + ['Pear', 9, 8, 8, 77.00], + ['Apple', 12, 11, 6, 45.00], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], ['="=Apple"', '>10', null, null, null, '<16'], @@ -26,30 +31,28 @@ $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], $worksheet->fromArray($criteria, null, 'A1'); $worksheet->fromArray($database, null, 'A4'); -$worksheet->setCellValue('A12', 'The Number of Apple trees over 10\' in height'); -$worksheet->setCellValue('B12', '=DCOUNT(A4:E10,"Yield",A1:B2)'); +$worksheet->setCellValue('A12', 'The Number of Apple trees between 10\' and 16\' in height whose age is known'); +$worksheet->setCellValue('B12', '=DCOUNT(A4:E10,"Age",A1:F2)'); -$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard'); +$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard with a numeric value in column 3 ("Age")'); $worksheet->setCellValue('B13', '=DCOUNT(A4:E10,3,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); -var_dump($criteriaData); +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DCOUNT() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DCOUNT() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DCOUNTA.php b/samples/Calculations/Database/DCOUNTA.php new file mode 100644 index 00000000..3b4cc16e --- /dev/null +++ b/samples/Calculations/Database/DCOUNTA.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], + ['Apple', 18, 20, 14, 105.00], + ['Pear', 12, 12, 10, 96.00], + ['Cherry', 13, 14, 9, 105.00], + ['Apple', 14, 'N/A', 10, 75.00], + ['Pear', 9, 8, 8, 77.00], + ['Apple', 12, 11, 6, 45.00], +]; +$criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], + ['="=Apple"', '>10', null, null, null, '<16'], + ['="=Pear"', null, null, null, null, null], +]; + +$worksheet->fromArray($criteria, null, 'A1'); +$worksheet->fromArray($database, null, 'A4'); + +$worksheet->setCellValue('A12', 'The Number of Apple trees between 10\' and 16\' in height'); +$worksheet->setCellValue('B12', '=DCOUNTA(A4:E10,"Age",A1:F2)'); + +$worksheet->setCellValue('A13', 'The Number of Apple and Pear trees in the orchard'); +$worksheet->setCellValue('B13', '=DCOUNTA(A4:E10,3,A1:A3)'); + +$helper->log('Database'); + +$databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); +$helper->displayGrid($databaseData); + +// Test the formulae +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); + +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DGET.php b/samples/Calculations/Database/DGET.php index 9f543c91..1b41b777 100644 --- a/samples/Calculations/Database/DGET.php +++ b/samples/Calculations/Database/DGET.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Extracts a single value from a column of a list or database that matches conditions that you specify.'); +$category = 'Database'; +$functionName = 'DGET'; +$description = 'Extracts a single value from a column of a list or database that matches criteria that you specify'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -21,7 +25,7 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], ['="=Apple"', '>10', null, null, null, '<16'], - ['="=Pear"', null, null, null, null, null], + ['="=Pear"', '>12', null, null, null, null], ]; $worksheet->fromArray($criteria, null, 'A1'); @@ -30,23 +34,25 @@ $worksheet->fromArray($database, null, 'A4'); $worksheet->setCellValue('A12', 'The height of the Apple tree between 10\' and 16\' tall'); $worksheet->setCellValue('B12', '=DGET(A4:E10,"Height",A1:F2)'); +$worksheet->setCellValue('A13', 'The height of the Apple tree (will return an Excel error, because there is more than one apple tree)'); +$worksheet->setCellValue('B13', '=DGET(A4:E10,"Height",A1:A2)'); + $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$helper->log('ALL'); +$criteriaData = $worksheet->rangeToArray('A1:F2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DMAX.php b/samples/Calculations/Database/DMAX.php index c48928d4..0998904b 100644 --- a/samples/Calculations/Database/DMAX.php +++ b/samples/Calculations/Database/DMAX.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the maximum value from selected database entries.'); +$category = 'Database'; +$functionName = 'DMAX'; +$description = 'Returns the maximum value from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,20 +40,18 @@ $worksheet->setCellValue('B13', '=DMAX(A4:E10,3,A1:A2)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $helper->log('ALL'); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DMIN.php b/samples/Calculations/Database/DMIN.php index 7bcaa206..c0e0a8d8 100644 --- a/samples/Calculations/Database/DMIN.php +++ b/samples/Calculations/Database/DMIN.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the minimum value from selected database entries.'); +$category = 'Database'; +$functionName = 'DMIN'; +$description = 'Returns the minimum value from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,20 +40,18 @@ $worksheet->setCellValue('B13', '=DMIN(A4:E10,3,A1:A2)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $helper->log('ALL'); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMIN() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMIN() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DPRODUCT.php b/samples/Calculations/Database/DPRODUCT.php index 7c14ded6..fa666ffe 100644 --- a/samples/Calculations/Database/DPRODUCT.php +++ b/samples/Calculations/Database/DPRODUCT.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Multiplies the values in a column of a list or database that match conditions that you specify.'); +$category = 'Database'; +$functionName = 'DPRODUCT'; +$description = 'Multiplies the values in a column of a list or database that match conditions that you specify'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -16,7 +20,7 @@ $database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], ['Pear', 12, 12, 10, 96.00], ['Cherry', 13, 14, 9, 105.00], ['Apple', 14, 15, 10, 75.00], - ['Pear', 9, 8, 8, 76.80], + ['Pear', 9, 8, 8, 77.00], ['Apple', 8, 9, 6, 45.00], ]; $criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], @@ -30,23 +34,25 @@ $worksheet->fromArray($database, null, 'A4'); $worksheet->setCellValue('A12', 'The product of the yields of all Apple trees over 10\' in the orchard'); $worksheet->setCellValue('B12', '=DPRODUCT(A4:E10,"Yield",A1:B2)'); +$worksheet->setCellValue('A13', 'The product of the yields of all Apple trees in the orchard'); +$worksheet->setCellValue('B13', '=DPRODUCT(A4:E10,"Yield",A1:A2)'); + $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); -$helper->log('ALL'); +$criteriaData = $worksheet->rangeToArray('A1:B2', null, true, true, true); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DMAX() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSTDEV.php b/samples/Calculations/Database/DSTDEV.php index 7f09fa59..b6499741 100644 --- a/samples/Calculations/Database/DSTDEV.php +++ b/samples/Calculations/Database/DSTDEV.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Estimates the standard deviation based on a sample of selected database entries.'); +$category = 'Database'; +$functionName = 'DSTDEV'; +$description = 'Estimates the standard deviation based on a sample of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DSTDEV(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DSTDEV() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DSTDEV() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSTDEVP.php b/samples/Calculations/Database/DSTDEVP.php index 9e999a80..d8bcec4b 100644 --- a/samples/Calculations/Database/DSTDEVP.php +++ b/samples/Calculations/Database/DSTDEVP.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Calculates the standard deviation based on the entire population of selected database entries.'); + +$category = 'Database'; +$functionName = 'DSTDEVP'; +$description = 'Calculates the standard deviation based on the entire population of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -35,21 +40,19 @@ $worksheet->setCellValue('B13', '=DSTDEVP(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DSTDEVP() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DSTDEVP() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DSUM.php b/samples/Calculations/Database/DSUM.php new file mode 100644 index 00000000..d2d773c9 --- /dev/null +++ b/samples/Calculations/Database/DSUM.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$database = [['Tree', 'Height', 'Age', 'Yield', 'Profit'], + ['Apple', 18, 20, 14, 105.00], + ['Pear', 12, 12, 10, 96.00], + ['Cherry', 13, 14, 9, 105.00], + ['Apple', 14, 15, 10, 75.00], + ['Pear', 9, 8, 8, 76.80], + ['Apple', 8, 9, 6, 45.00], +]; +$criteria = [['Tree', 'Height', 'Age', 'Yield', 'Profit', 'Height'], + ['="=Apple"', '>10', null, null, null, '<16'], + ['="=Pear"', null, null, null, null, null], +]; + +$worksheet->fromArray($criteria, null, 'A1'); +$worksheet->fromArray($database, null, 'A4'); + +$worksheet->setCellValue('A12', 'The total profit from apple trees'); +$worksheet->setCellValue('B12', '=DSUM(A4:E10,"Profit",A1:A2)'); + +$worksheet->setCellValue('A13', 'Total profit from apple trees with a height between 10 and 16 feet, and all pear trees'); +$worksheet->setCellValue('B13', '=DSUM(A4:E10,"Profit",A1:F3)'); + +$helper->log('Database'); + +$databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); +$helper->displayGrid($databaseData); + +// Test the formulae +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:A2', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); + +$helper->log('Criteria'); + +$criteriaData = $worksheet->rangeToArray('A1:F3', null, true, true, true); +$helper->displayGrid($criteriaData); + +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DVAR.php b/samples/Calculations/Database/DVAR.php index 2a5f8749..4165d076 100644 --- a/samples/Calculations/Database/DVAR.php +++ b/samples/Calculations/Database/DVAR.php @@ -3,7 +3,12 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Estimates variance based on a sample from selected database entries.'); + +$category = 'Database'; +$functionName = 'DVAR'; +$description = 'Estimates variance based on a sample from selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -35,21 +40,19 @@ $worksheet->setCellValue('B13', '=DVAR(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DVAR() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DVAR() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/Database/DVARP.php b/samples/Calculations/Database/DVARP.php index 4f57113b..5a17415e 100644 --- a/samples/Calculations/Database/DVARP.php +++ b/samples/Calculations/Database/DVARP.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Calculates variance based on the entire population of selected database entries,'); +$category = 'Database'; +$functionName = 'DVARP'; +$description = 'Calculates variance based on the entire population of selected database entries'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -36,21 +40,19 @@ $worksheet->setCellValue('B13', '=DVARP(A4:E10,2,A1:A3)'); $helper->log('Database'); $databaseData = $worksheet->rangeToArray('A4:E10', null, true, true, true); -var_dump($databaseData); +$helper->displayGrid($databaseData); // Test the formulae $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A12')->getValue()); -$helper->log('DVARP() Result is ' . $worksheet->getCell('B12')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B12', 'A12'); $helper->log('Criteria'); $criteriaData = $worksheet->rangeToArray('A1:A3', null, true, true, true); -var_dump($criteriaData); +$helper->displayGrid($criteriaData); -$helper->log($worksheet->getCell('A13')->getValue()); -$helper->log('DVARP() Result is ' . $worksheet->getCell('B13')->getCalculatedValue()); +$helper->logCalculationResult($worksheet, $functionName, 'B13', 'A13'); diff --git a/samples/Calculations/DateTime/DATE.php b/samples/Calculations/DateTime/DATE.php index 5d758f76..d526cbdb 100644 --- a/samples/Calculations/DateTime/DATE.php +++ b/samples/Calculations/DateTime/DATE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the serial number of a particular date.'); +$category = 'Date/Time'; +$functionName = 'DATE'; +$description = 'Returns the Excel serial number of a particular date'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -27,15 +31,15 @@ for ($row = 1; $row <= $testDateCount; ++$row) { } $worksheet->getStyle('E1:E' . $testDateCount) ->getNumberFormat() - ->setFormatCode('yyyy-mmm-dd'); + ->setFormatCode('yyyy-mm-dd'); // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Year: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); - $helper->log('Month: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Day: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Year: " . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(B{$row}) Month: " . $worksheet->getCell('B' . $row)->getFormattedValue()); + $helper->log("(C{$row}) Day: " . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('D' . $row)->getValue()); - $helper->log('Excel DateStamp: ' . $worksheet->getCell('D' . $row)->getFormattedValue()); + $helper->log('Excel DateStamp: ' . $worksheet->getCell('D' . $row)->getCalculatedValue()); $helper->log('Formatted DateStamp: ' . $worksheet->getCell('E' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/DATEDIF.php b/samples/Calculations/DateTime/DATEDIF.php new file mode 100644 index 00000000..3e035f5a --- /dev/null +++ b/samples/Calculations/DateTime/DATEDIF.php @@ -0,0 +1,61 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2020, 2, 29], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=TODAY()'); + $worksheet->setCellValue('G' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "Y")'); + $worksheet->setCellValue('H' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "M")'); + $worksheet->setCellValue('I' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "D")'); + $worksheet->setCellValue('J' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "MD")'); + $worksheet->setCellValue('K' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "YM")'); + $worksheet->setCellValue('L' . $row, '=DATEDIF(D' . $row . ', F' . $row . ', "YD")'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log('In years ("Y"): ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); + $helper->log('In months ("M"): ' . $worksheet->getCell('H' . $row)->getCalculatedValue()); + $helper->log('In days ("D"): ' . $worksheet->getCell('I' . $row)->getCalculatedValue()); + $helper->log('In days ignoring months and years ("MD"): ' . $worksheet->getCell('J' . $row)->getCalculatedValue()); + $helper->log('In months ignoring days and years ("YM"): ' . $worksheet->getCell('K' . $row)->getCalculatedValue()); + $helper->log('In days ignoring years ("YD"): ' . $worksheet->getCell('L' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DATEVALUE.php b/samples/Calculations/DateTime/DATEVALUE.php index 5cdb936d..c506c6f2 100644 --- a/samples/Calculations/DateTime/DATEVALUE.php +++ b/samples/Calculations/DateTime/DATEVALUE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Converts a date in the form of text to a serial number.'); +$category = 'Date/Time'; +$functionName = 'DATEVALUE'; +$description = 'Converts a date in the form of text to an Excel serial number'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -13,8 +17,8 @@ $worksheet = $spreadsheet->getActiveSheet(); // Add some data $testDates = ['26 March 2012', '29 Feb 2012', 'April 1, 2012', '25/12/2012', '2012-Oct-31', '5th November', 'January 1st', 'April 2012', - '17-03', '03-2012', '29 Feb 2011', '03-05-07', - '03-MAY-07', '03-13-07', + '17-03', '03-17', '03-2012', '29 Feb 2011', '03-05-07', + '03-MAY-07', '03-13-07', '13-03-07', '03/13/07', '13/03/07', ]; $testDateCount = count($testDates); @@ -26,14 +30,14 @@ for ($row = 1; $row <= $testDateCount; ++$row) { $worksheet->getStyle('C1:C' . $testDateCount) ->getNumberFormat() - ->setFormatCode('yyyy-mmm-dd'); + ->setFormatCode('yyyy-mm-dd'); // Test the formulae $helper->log('Warning: The PhpSpreadsheet DATEVALUE() function accepts a wider range of date formats than MS Excel DATEFORMAT() function.'); for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Date String: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Date String: " . $worksheet->getCell('A' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('B' . $row)->getValue()); - $helper->log('Excel DateStamp: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Formatted DateStamp' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log('Excel DateStamp: ' . $worksheet->getCell('B' . $row)->getCalculatedValue()); + $helper->log('Formatted DateStamp: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/DAY.php b/samples/Calculations/DateTime/DAY.php new file mode 100644 index 00000000..3a4da7e6 --- /dev/null +++ b/samples/Calculations/DateTime/DAY.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DAY(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Day is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DAYS.php b/samples/Calculations/DateTime/DAYS.php new file mode 100644 index 00000000..15e9a58f --- /dev/null +++ b/samples/Calculations/DateTime/DAYS.php @@ -0,0 +1,53 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2020, 2, 29], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=TODAY()'); + $worksheet->setCellValue('G' . $row, '=DAYS(D' . $row . ', F' . $row . ')'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log('Days: ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/DAYS360.php b/samples/Calculations/DateTime/DAYS360.php new file mode 100644 index 00000000..b0e2fdbb --- /dev/null +++ b/samples/Calculations/DateTime/DAYS360.php @@ -0,0 +1,57 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DATE(2022,12,31)'); + $worksheet->setCellValue('G' . $row, '=DAYS360(D' . $row . ', F' . $row . ', FALSE)'); + $worksheet->setCellValue('H' . $row, '=DAYS360(D' . $row . ', F' . $row . ', TRUE)'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log(sprintf( + 'Days: %d (US) %d (European)', + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/EDATE.php b/samples/Calculations/DateTime/EDATE.php new file mode 100644 index 00000000..be6e4d19 --- /dev/null +++ b/samples/Calculations/DateTime/EDATE.php @@ -0,0 +1,43 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$months = range(-12, 12); +$testDateCount = count($months); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=DATE(2020,12,31)'); + $worksheet->setCellValue('B' . $row, '=A' . $row); + $worksheet->setCellValue('C' . $row, $months[$row - 1]); + $worksheet->setCellValue('D' . $row, '=EDATE(B' . $row . ', C' . $row . ')'); +} +$worksheet->getStyle('B1:B' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('D1:D' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + '%s and %d months is %d (%s)', + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getFormattedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/EOMONTH.php b/samples/Calculations/DateTime/EOMONTH.php new file mode 100644 index 00000000..e0b7568a --- /dev/null +++ b/samples/Calculations/DateTime/EOMONTH.php @@ -0,0 +1,43 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$months = range(-12, 12); +$testDateCount = count($months); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=DATE(2020,1,1)'); + $worksheet->setCellValue('B' . $row, '=A' . $row); + $worksheet->setCellValue('C' . $row, $months[$row - 1]); + $worksheet->setCellValue('D' . $row, '=EOMONTH(B' . $row . ', C' . $row . ')'); +} +$worksheet->getStyle('B1:B' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('D1:D' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + '%s and %d months is %d (%s)', + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getFormattedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/HOUR.php b/samples/Calculations/DateTime/HOUR.php new file mode 100644 index 00000000..53c21644 --- /dev/null +++ b/samples/Calculations/DateTime/HOUR.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=HOUR(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Hour is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/ISOWEEKNUM.php b/samples/Calculations/DateTime/ISOWEEKNUM.php new file mode 100644 index 00000000..4a989fbd --- /dev/null +++ b/samples/Calculations/DateTime/ISOWEEKNUM.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=ISOWEEKNUM(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('ISO Week number is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/MINUTE.php b/samples/Calculations/DateTime/MINUTE.php new file mode 100644 index 00000000..11e90e13 --- /dev/null +++ b/samples/Calculations/DateTime/MINUTE.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=MINUTE(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Minute is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/MONTH.php b/samples/Calculations/DateTime/MONTH.php new file mode 100644 index 00000000..8cceaf4c --- /dev/null +++ b/samples/Calculations/DateTime/MONTH.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=MONTH(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Month is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/NETWORKDAYS.php b/samples/Calculations/DateTime/NETWORKDAYS.php new file mode 100644 index 00000000..585c0438 --- /dev/null +++ b/samples/Calculations/DateTime/NETWORKDAYS.php @@ -0,0 +1,66 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$publicHolidays = [ + [2022, 1, 3, '=DATE(G1, H1, I1)', 'New Year'], + [2022, 4, 15, '=DATE(G2, H2, I2)', 'Good Friday'], + [2022, 4, 18, '=DATE(G3, H3, I3)', 'Easter Monday'], + [2022, 5, 2, '=DATE(G4, H4, I4)', 'Early May Bank Holiday'], + [2022, 6, 2, '=DATE(G5, H5, I5)', 'Spring Bank Holiday'], + [2022, 6, 3, '=DATE(G6, H6, I6)', 'Platinum Jubilee Bank Holiday'], + [2022, 8, 29, '=DATE(G7, H7, I7)', 'Summer Bank Holiday'], + [2022, 12, 26, '=DATE(G8, H8, I8)', 'Boxing Day'], + [2022, 12, 27, '=DATE(G9, H9, I9)', 'Christmas Day'], +]; + +$holidayCount = count($publicHolidays); +$worksheet->fromArray($publicHolidays, null, 'G1', true); + +$worksheet->getStyle('J1:J' . $holidayCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->setCellValue('A1', '=DATE(2022,1,1)'); + +for ($numberOfMonths = 0; $numberOfMonths < 12; ++$numberOfMonths) { + $worksheet->setCellValue('B' . ($numberOfMonths + 1), '=EOMONTH(A1, ' . $numberOfMonths . ')'); + $worksheet->setCellValue('C' . ($numberOfMonths + 1), '=NETWORKDAYS(A1, B' . ($numberOfMonths + 1) . ')'); + $worksheet->setCellValue('D' . ($numberOfMonths + 1), '=NETWORKDAYS(A1, B' . ($numberOfMonths + 1) . ', J1:J' . $holidayCount . ')'); +} + +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('B1:B12') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log('UK Public Holidays'); +$holidayData = $worksheet->rangeToArray('J1:K' . $holidayCount, null, true, true, true); +$helper->displayGrid($holidayData); + +for ($row = 1; $row <= 12; ++$row) { + $helper->log(sprintf( + 'Between %s and %s is %d working days; %d with public holidays', + $worksheet->getCell('A1')->getFormattedValue(), + $worksheet->getCell('B' . $row)->getFormattedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/NOW.php b/samples/Calculations/DateTime/NOW.php new file mode 100644 index 00000000..858a3162 --- /dev/null +++ b/samples/Calculations/DateTime/NOW.php @@ -0,0 +1,27 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->setCellValue('A1', '=NOW()'); +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd hh:mm:ss'); + +// Test the formulae +$helper->log(sprintf( + 'Today is %f (%s)', + $worksheet->getCell('A1')->getCalculatedValue(), + $worksheet->getCell('A1')->getFormattedValue() +)); diff --git a/samples/Calculations/DateTime/SECOND.php b/samples/Calculations/DateTime/SECOND.php new file mode 100644 index 00000000..33806fd5 --- /dev/null +++ b/samples/Calculations/DateTime/SECOND.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testTimes = [ + [0, 6, 0], + [1, 12, 15], + [3, 30, 12], + [5, 17, 31], + [8, 15, 45], + [12, 45, 11], + [14, 0, 30], + [17, 55, 50], + [19, 21, 8], + [21, 10, 10], + [23, 59, 59], +]; +$testTimeCount = count($testTimes); + +$worksheet->fromArray($testTimes, null, 'A1', true); + +for ($row = 1; $row <= $testTimeCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=TIME(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=SECOND(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testTimeCount) + ->getNumberFormat() + ->setFormatCode('hh:mm:ss'); + +// Test the formulae +for ($row = 1; $row <= $testTimeCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Second is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/TIME.php b/samples/Calculations/DateTime/TIME.php index 3d4208ad..42c45488 100644 --- a/samples/Calculations/DateTime/TIME.php +++ b/samples/Calculations/DateTime/TIME.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Returns the serial number of a particular time.'); +$category = 'Date/Time'; +$functionName = 'TIME'; +$description = 'Returns the Excel serial number of a particular time'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -29,11 +33,11 @@ $worksheet->getStyle('E1:E' . $testDateCount) // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Hour: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); - $helper->log('Minute: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); - $helper->log('Second: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Hour: " . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(B{$row}) Minute: " . $worksheet->getCell('B' . $row)->getFormattedValue()); + $helper->log("(C{$row}) Second: " . $worksheet->getCell('C' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('D' . $row)->getValue()); - $helper->log('Excel TimeStamp: ' . $worksheet->getCell('D' . $row)->getFormattedValue()); + $helper->log('Excel TimeStamp: ' . $worksheet->getCell('D' . $row)->getCalculatedValue()); $helper->log('Formatted TimeStamp: ' . $worksheet->getCell('E' . $row)->getFormattedValue()); $helper->log(''); } diff --git a/samples/Calculations/DateTime/TIMEVALUE.php b/samples/Calculations/DateTime/TIMEVALUE.php index f75393cd..15ea8cd9 100644 --- a/samples/Calculations/DateTime/TIMEVALUE.php +++ b/samples/Calculations/DateTime/TIMEVALUE.php @@ -4,7 +4,11 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; require __DIR__ . '/../../Header.php'; -$helper->log('Converts a time in the form of text to a serial number.'); +$category = 'Date/Time'; +$functionName = 'DATEVALUE'; +$description = 'Converts a time in the form of text to an Excel serial number'; + +$helper->titles($category, $functionName, $description); // Create new PhpSpreadsheet object $spreadsheet = new Spreadsheet(); @@ -27,7 +31,7 @@ $worksheet->getStyle('C1:C' . $testDateCount) // Test the formulae for ($row = 1; $row <= $testDateCount; ++$row) { - $helper->log('Time String: ' . $worksheet->getCell('A' . $row)->getFormattedValue()); + $helper->log("(A{$row}) Time String: " . $worksheet->getCell('A' . $row)->getFormattedValue()); $helper->log('Formula: ' . $worksheet->getCell('B' . $row)->getValue()); $helper->log('Excel TimeStamp: ' . $worksheet->getCell('B' . $row)->getFormattedValue()); $helper->log('Formatted TimeStamp: ' . $worksheet->getCell('C' . $row)->getFormattedValue()); diff --git a/samples/Calculations/DateTime/TODAY.php b/samples/Calculations/DateTime/TODAY.php new file mode 100644 index 00000000..031149d5 --- /dev/null +++ b/samples/Calculations/DateTime/TODAY.php @@ -0,0 +1,27 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->setCellValue('A1', '=TODAY()'); +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log(sprintf( + 'Today is %d (%s)', + $worksheet->getCell('A1')->getCalculatedValue(), + $worksheet->getCell('A1')->getFormattedValue() +)); diff --git a/samples/Calculations/DateTime/WEEKDAY.php b/samples/Calculations/DateTime/WEEKDAY.php new file mode 100644 index 00000000..7d4b4288 --- /dev/null +++ b/samples/Calculations/DateTime/WEEKDAY.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=WEEKDAY(D' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=WEEKDAY(D' . $row . ', 2)'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log(sprintf( + 'Weekday is: %d (1-7 = Sun-Sat)', + $worksheet->getCell('F' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Weekday is: %d (1-7 = Mon-Sun)', + $worksheet->getCell('G' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/DateTime/WEEKNUM.php b/samples/Calculations/DateTime/WEEKNUM.php new file mode 100644 index 00000000..1d4c4a12 --- /dev/null +++ b/samples/Calculations/DateTime/WEEKNUM.php @@ -0,0 +1,52 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=WEEKNUM(D' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=WEEKNUM(D' . $row . ', 21)'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('System 1 Week number is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); + $helper->log('System 2 (ISO-8601) Week number is: ' . $worksheet->getCell('G' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/WORKDAY.php b/samples/Calculations/DateTime/WORKDAY.php new file mode 100644 index 00000000..d18e3463 --- /dev/null +++ b/samples/Calculations/DateTime/WORKDAY.php @@ -0,0 +1,67 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$publicHolidays = [ + [2022, 1, 3, '=DATE(G1, H1, I1)', 'New Year'], + [2022, 4, 15, '=DATE(G2, H2, I2)', 'Good Friday'], + [2022, 4, 18, '=DATE(G3, H3, I3)', 'Easter Monday'], + [2022, 5, 2, '=DATE(G4, H4, I4)', 'Early May Bank Holiday'], + [2022, 6, 2, '=DATE(G5, H5, I5)', 'Spring Bank Holiday'], + [2022, 6, 3, '=DATE(G6, H6, I6)', 'Platinum Jubilee Bank Holiday'], + [2022, 8, 29, '=DATE(G7, H7, I7)', 'Summer Bank Holiday'], + [2022, 12, 26, '=DATE(G8, H8, I8)', 'Boxing Day'], + [2022, 12, 27, '=DATE(G9, H9, I9)', 'Christmas Day'], +]; + +$holidayCount = count($publicHolidays); +$worksheet->fromArray($publicHolidays, null, 'G1', true); + +$worksheet->getStyle('J1:J' . $holidayCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->setCellValue('A1', '=DATE(2022,1,1)'); + +$workdayStep = 10; +for ($days = $workdayStep; $days <= 366; $days += $workdayStep) { + $worksheet->setCellValue('B' . ((int) $days / $workdayStep + 1), $days); + $worksheet->setCellValue('C' . ((int) $days / $workdayStep + 1), '=WORKDAY(A1, B' . ((int) $days / $workdayStep + 1) . ')'); + $worksheet->setCellValue('D' . ((int) $days / $workdayStep + 1), '=WORKDAY(A1, B' . ((int) $days / $workdayStep + 1) . ', J1:J' . $holidayCount . ')'); +} + +$worksheet->getStyle('A1') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +$worksheet->getStyle('C1:D50') + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +$helper->log('UK Public Holidays'); +$holidayData = $worksheet->rangeToArray('J1:K' . $holidayCount, null, true, true, true); +$helper->displayGrid($holidayData); + +for ($days = $workdayStep; $days <= 366; $days += $workdayStep) { + $helper->log(sprintf( + '%d workdays from %s is %s; %s with public holidays', + $worksheet->getCell('B' . ((int) $days / $workdayStep + 1))->getFormattedValue(), + $worksheet->getCell('A1')->getFormattedValue(), + $worksheet->getCell('C' . ((int) $days / $workdayStep + 1))->getFormattedValue(), + $worksheet->getCell('D' . ((int) $days / $workdayStep + 1))->getFormattedValue() + )); +} diff --git a/samples/Calculations/DateTime/YEAR.php b/samples/Calculations/DateTime/YEAR.php new file mode 100644 index 00000000..f7bfa6ea --- /dev/null +++ b/samples/Calculations/DateTime/YEAR.php @@ -0,0 +1,50 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 2, 14], + [1936, 3, 17], + [1964, 4, 29], + [1999, 5, 18], + [2000, 6, 21], + [2019, 7, 4], + [2020, 8, 31], + [1956, 9, 10], + [2010, 10, 10], + [1982, 11, 30], + [1960, 12, 19], + ['=YEAR(TODAY())', '=MONTH(TODAY())', '=DAY(TODAY())'], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=YEAR(D' . $row . ')'); +} +$worksheet->getStyle('E1:E' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf('(E%d): %s', $row, $worksheet->getCell('E' . $row)->getFormattedValue())); + $helper->log('Year is: ' . $worksheet->getCell('F' . $row)->getCalculatedValue()); +} diff --git a/samples/Calculations/DateTime/YEARFRAC.php b/samples/Calculations/DateTime/YEARFRAC.php new file mode 100644 index 00000000..81b36435 --- /dev/null +++ b/samples/Calculations/DateTime/YEARFRAC.php @@ -0,0 +1,76 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testDates = [ + [1900, 1, 1], + [1904, 1, 1], + [1936, 3, 17], + [1960, 12, 19], + [1999, 12, 31], + [2000, 1, 1], + [2019, 2, 14], + [2020, 7, 4], + [2020, 2, 29], + [2029, 12, 31], + [2525, 1, 1], +]; +$testDateCount = count($testDates); + +$worksheet->fromArray($testDates, null, 'A1', true); + +for ($row = 1; $row <= $testDateCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=DATE(A' . $row . ',B' . $row . ',C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=D' . $row); + $worksheet->setCellValue('F' . $row, '=DATE(2022,12,31)'); + $worksheet->setCellValue('G' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ')'); + $worksheet->setCellValue('H' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 1)'); + $worksheet->setCellValue('I' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 2)'); + $worksheet->setCellValue('J' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 3)'); + $worksheet->setCellValue('K' . $row, '=YEARFRAC(D' . $row . ', F' . $row . ', 4)'); +} +$worksheet->getStyle('E1:F' . $testDateCount) + ->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + +// Test the formulae +for ($row = 1; $row <= $testDateCount; ++$row) { + $helper->log(sprintf( + 'Between: %s and %s', + $worksheet->getCell('E' . $row)->getFormattedValue(), + $worksheet->getCell('F' . $row)->getFormattedValue() + )); + $helper->log(sprintf( + 'Days: %f - US (NASD) 30/360', + $worksheet->getCell('G' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual', + $worksheet->getCell('H' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual/360', + $worksheet->getCell('I' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - Actual/365', + $worksheet->getCell('J' . $row)->getCalculatedValue() + )); + $helper->log(sprintf( + 'Days: %f - European 30/360', + $worksheet->getCell('K' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/Engineering/BESSELI.php b/samples/Calculations/Engineering/BESSELI.php new file mode 100644 index 00000000..bd5d9b71 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELI.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELI({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELJ.php b/samples/Calculations/Engineering/BESSELJ.php new file mode 100644 index 00000000..3aa47886 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELJ.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELJ({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELK.php b/samples/Calculations/Engineering/BESSELK.php new file mode 100644 index 00000000..ee8698e9 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELK.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELK({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BESSELY.php b/samples/Calculations/Engineering/BESSELY.php new file mode 100644 index 00000000..750b7204 --- /dev/null +++ b/samples/Calculations/Engineering/BESSELY.php @@ -0,0 +1,29 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +for ($n = 0; $n <= 5; ++$n) { + for ($x = 0; $x <= 5; $x = $x + 0.25) { + Calculation::getInstance($spreadsheet)->flushInstance(); + $worksheet->setCellValue('A1', "=BESSELY({$x}, {$n})"); + + $helper->log(sprintf( + '%s = %f', + $worksheet->getCell('A1')->getValue(), + $worksheet->getCell('A1')->getCalculatedValue() + )); + } +} diff --git a/samples/Calculations/Engineering/BIN2DEC.php b/samples/Calculations/Engineering/BIN2DEC.php new file mode 100644 index 00000000..0c4c4532 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2DEC.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BIN2HEX.php b/samples/Calculations/Engineering/BIN2HEX.php new file mode 100644 index 00000000..51a11199 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2HEX.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BIN2OCT.php b/samples/Calculations/Engineering/BIN2OCT.php new file mode 100644 index 00000000..c320d360 --- /dev/null +++ b/samples/Calculations/Engineering/BIN2OCT.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [101], + [110110], + [1000000], + [11111111], + [100010101], + [110001100], + [111111111], + [1111111111], + [1100110011], + [1000000000], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=BIN2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Binary %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITAND.php b/samples/Calculations/Engineering/BITAND.php new file mode 100644 index 00000000..2a8f7a3c --- /dev/null +++ b/samples/Calculations/Engineering/BITAND.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITAND(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise AND of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITLSHIFT.php b/samples/Calculations/Engineering/BITLSHIFT.php new file mode 100644 index 00000000..872c8098 --- /dev/null +++ b/samples/Calculations/Engineering/BITLSHIFT.php @@ -0,0 +1,65 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1], + [3], + [9], + [15], + [26], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); + $worksheet->setCellValue('C' . $row, '=BITLSHIFT(A' . $row . ',1)'); + $worksheet->setCellValue('D' . $row, '=DEC2BIN(C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=BITLSHIFT(A' . $row . ',2)'); + $worksheet->setCellValue('F' . $row, '=DEC2BIN(E' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=BITLSHIFT(A' . $row . ',3)'); + $worksheet->setCellValue('H' . $row, '=DEC2BIN(G' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 1 bit is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 2 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Left Shift of %d (%s) by 3 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITOR.php b/samples/Calculations/Engineering/BITOR.php new file mode 100644 index 00000000..1bf7f71d --- /dev/null +++ b/samples/Calculations/Engineering/BITOR.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITOR(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise OR of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITRSHIFT.php b/samples/Calculations/Engineering/BITRSHIFT.php new file mode 100644 index 00000000..3e7f3a88 --- /dev/null +++ b/samples/Calculations/Engineering/BITRSHIFT.php @@ -0,0 +1,63 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [9], + [15], + [26], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); + $worksheet->setCellValue('C' . $row, '=BITRSHIFT(A' . $row . ',1)'); + $worksheet->setCellValue('D' . $row, '=DEC2BIN(C' . $row . ')'); + $worksheet->setCellValue('E' . $row, '=BITRSHIFT(A' . $row . ',2)'); + $worksheet->setCellValue('F' . $row, '=DEC2BIN(E' . $row . ')'); + $worksheet->setCellValue('G' . $row, '=BITRSHIFT(A' . $row . ',3)'); + $worksheet->setCellValue('H' . $row, '=DEC2BIN(G' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 1 bit is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 2 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); + $helper->log(sprintf( + '(E%d): Bitwise Right Shift of %d (%s) by 3 bits is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + $worksheet->getCell('G' . $row)->getCalculatedValue(), + $worksheet->getCell('H' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/BITXOR.php b/samples/Calculations/Engineering/BITXOR.php new file mode 100644 index 00000000..482662cd --- /dev/null +++ b/samples/Calculations/Engineering/BITXOR.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [1, 5], + [3, 5], + [1, 6], + [9, 6], + [13, 25], + [23, 10], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=TEXT(DEC2BIN(A' . $row . '), "00000")'); + $worksheet->setCellValue('D' . $row, '=TEXT(DEC2BIN(B' . $row . '), "00000")'); + $worksheet->setCellValue('E' . $row, '=BITXOR(A' . $row . ',B' . $row . ')'); + $worksheet->setCellValue('F' . $row, '=TEXT(DEC2BIN(E' . $row . '), "00000")'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Bitwise XOR of %d (%s) and %d (%s) is %d (%s)', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + $worksheet->getCell('E' . $row)->getCalculatedValue(), + $worksheet->getCell('F' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/COMPLEX.php b/samples/Calculations/Engineering/COMPLEX.php new file mode 100644 index 00000000..c58a1797 --- /dev/null +++ b/samples/Calculations/Engineering/COMPLEX.php @@ -0,0 +1,41 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3, 4], + [3, 4, '"j"'], + [3.5, 4.75], + [0, 1], + [1, 0], + [0, -1], + [0, 2], + [2, 0], +]; +$testDataCount = count($testData); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('A' . $row, '=COMPLEX(' . implode(',', $testData[$row - 1]) . ')'); +} + +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(A%d): Formula %s result is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getCalculatedValue() + )); +} diff --git a/samples/Calculations/Engineering/CONVERT.php b/samples/Calculations/Engineering/CONVERT.php new file mode 100644 index 00000000..bce56ba5 --- /dev/null +++ b/samples/Calculations/Engineering/CONVERT.php @@ -0,0 +1,58 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$conversions = [ + [1, '"lbm"', '"kg"'], + [1, '"gal"', '"l"'], + [24, '"in"', '"ft"'], + [100, '"yd"', '"m"'], + [500, '"mi"', '"km"'], + [7.5, '"min"', '"sec"'], + [5, '"F"', '"C"'], + [32, '"C"', '"K"'], + [100, '"m2"', '"ft2"'], +]; +$testDataCount = count($conversions); + +$worksheet->fromArray($conversions, null, 'A1'); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('D' . $row, '=CONVERT(' . implode(',', $conversions[$row - 1]) . ')'); +} + +$worksheet->setCellValue('H1', '=CONVERT(CONVERT(100,"m","ft"),"m","ft")'); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(A%d): Unit of Measure Conversion Formula %s - %d %s is %f %s', + $row, + $worksheet->getCell('D' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + trim($worksheet->getCell('B' . $row)->getValue(), '"'), + $worksheet->getCell('D' . $row)->getCalculatedValue(), + trim($worksheet->getCell('C' . $row)->getValue(), '"') + )); +} + +$helper->log('Old method for area conversions, before MS Excel introduced area Units of Measure'); + +$helper->log(sprintf( + '(A%d): Unit of Measure Conversion Formula %s result is %s', + $row, + $worksheet->getCell('H1')->getValue(), + $worksheet->getCell('H1')->getCalculatedValue() +)); diff --git a/samples/Calculations/Engineering/DEC2BIN.php b/samples/Calculations/Engineering/DEC2BIN.php new file mode 100644 index 00000000..2a064c06 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2BIN.php @@ -0,0 +1,47 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DEC2HEX.php b/samples/Calculations/Engineering/DEC2HEX.php new file mode 100644 index 00000000..0a19ae54 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2HEX.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], + [12345678], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DEC2OCT.php b/samples/Calculations/Engineering/DEC2OCT.php new file mode 100644 index 00000000..fc11a832 --- /dev/null +++ b/samples/Calculations/Engineering/DEC2OCT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [-255], + [-123], + [-15], + [-1], + [5], + [7], + [19], + [51], + [121], + [256], + [511], + [12345678], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=DEC2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Decimal %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/DELTA.php b/samples/Calculations/Engineering/DELTA.php new file mode 100644 index 00000000..cd51b161 --- /dev/null +++ b/samples/Calculations/Engineering/DELTA.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [4, 5], + [3, 3], + [0.5, 0], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=DELTA(A' . $row . ',B' . $row . ')'); +} + +$comparison = [ + 0 => 'The values are not equal', + 1 => 'The values are equal', +]; + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Compare values %d and %d - Result is %d - %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + $comparison[$worksheet->getCell('C' . $row)->getCalculatedValue()] + )); +} diff --git a/samples/Calculations/Engineering/ERF.php b/samples/Calculations/Engineering/ERF.php new file mode 100644 index 00000000..e6505888 --- /dev/null +++ b/samples/Calculations/Engineering/ERF.php @@ -0,0 +1,67 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData1 = [ + [0.745], + [1], + [1.5], + [-2], +]; + +$testData2 = [ + [0, 1.5], + [1, 2], + [-2, 1], +]; +$testDataCount1 = count($testData1); +$testDataCount2 = count($testData2); +$testData2StartRow = $testDataCount1 + 1; + +$worksheet->fromArray($testData1, null, 'A1', true); +$worksheet->fromArray($testData2, null, "A{$testData2StartRow}", true); + +for ($row = 1; $row <= $testDataCount1; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERF(A' . $row . ')'); +} + +for ($row = $testDataCount1 + 1; $row <= $testDataCount2 + $testDataCount1; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERF(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +$helper->log('ERF() With a single argument'); +for ($row = 1; $row <= $testDataCount1; ++$row) { + $helper->log(sprintf( + '(C%d): %s The error function integrated between 0 and %f is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} + +$helper->log('ERF() With two arguments'); +for ($row = $testDataCount1 + 1; $row <= $testDataCount2 + $testDataCount1; ++$row) { + $helper->log(sprintf( + '(C%d): %s The error function integrated between %f and %f is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/ERFC.php b/samples/Calculations/Engineering/ERFC.php new file mode 100644 index 00000000..5e7bcc6d --- /dev/null +++ b/samples/Calculations/Engineering/ERFC.php @@ -0,0 +1,41 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [0], + [0.5], + [1], + [-1], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=ERFC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): %s The complementary error function integrated by %f and infinity is %f', + $row, + $worksheet->getCell('C' . $row)->getValue(), + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/GESTEP.php b/samples/Calculations/Engineering/GESTEP.php new file mode 100644 index 00000000..73f0a31c --- /dev/null +++ b/samples/Calculations/Engineering/GESTEP.php @@ -0,0 +1,53 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [5, 4], + [5, 5], + [4, 5], + [-4, -5], + [-5, -4], + [1], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=GESTEP(A' . $row . ',B' . $row . ')'); +} + +$comparison = [ + 0 => 'Value %d is less than step %d', + 1 => 'Value %d is greater than or equal to step %d', +]; + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): Compare value %d and step %d - Result is %d - %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + sprintf( + $comparison[$worksheet->getCell('C' . $row)->getCalculatedValue()], + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + ) + )); +} diff --git a/samples/Calculations/Engineering/HEX2BIN.php b/samples/Calculations/Engineering/HEX2BIN.php new file mode 100644 index 00000000..2ad08925 --- /dev/null +++ b/samples/Calculations/Engineering/HEX2BIN.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [8], + [42], + [99], + ['A2'], + ['F0'], + ['100'], + ['128'], + ['1AB'], + ['1FF'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/HEX2DEC.php b/samples/Calculations/Engineering/HEX2DEC.php new file mode 100644 index 00000000..745d4110 --- /dev/null +++ b/samples/Calculations/Engineering/HEX2DEC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['08'], + ['42'], + ['A2'], + ['400'], + ['1000'], + ['1234'], + ['ABCD'], + ['C3B0'], + ['FFFFFFFFF'], + ['FFFFFFFFFF'], + ['FFFFFFF800'], + ['FEDCBA9876'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/HEX2OCT.php b/samples/Calculations/Engineering/HEX2OCT.php new file mode 100644 index 00000000..3608c1bb --- /dev/null +++ b/samples/Calculations/Engineering/HEX2OCT.php @@ -0,0 +1,46 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['08'], + ['42'], + ['A2'], + ['400'], + ['100'], + ['1234'], + ['ABCD'], + ['C3B0'], + ['FFFFFFFFFF'], + ['FFFFFFF800'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=HEX2OCT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Hexadecimal %s is octal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMABS.php b/samples/Calculations/Engineering/IMABS.php new file mode 100644 index 00000000..9c6b843c --- /dev/null +++ b/samples/Calculations/Engineering/IMABS.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMABS(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The absolute value of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMAGINARY.php b/samples/Calculations/Engineering/IMAGINARY.php new file mode 100644 index 00000000..99138938 --- /dev/null +++ b/samples/Calculations/Engineering/IMAGINARY.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMAGINARY(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The imaginary component of %s is %f', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMARGUMENT.php b/samples/Calculations/Engineering/IMARGUMENT.php new file mode 100644 index 00000000..e559e951 --- /dev/null +++ b/samples/Calculations/Engineering/IMARGUMENT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMARGUMENT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Theta Argument of %s is %f radians', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCONJUGATE.php b/samples/Calculations/Engineering/IMCONJUGATE.php new file mode 100644 index 00000000..3b4429ed --- /dev/null +++ b/samples/Calculations/Engineering/IMCONJUGATE.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCONJUGATE(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Conjugate of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOS.php b/samples/Calculations/Engineering/IMCOS.php new file mode 100644 index 00000000..5b8f81ea --- /dev/null +++ b/samples/Calculations/Engineering/IMCOS.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOS(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cosine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOSH.php b/samples/Calculations/Engineering/IMCOSH.php new file mode 100644 index 00000000..9a393766 --- /dev/null +++ b/samples/Calculations/Engineering/IMCOSH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOSH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Cosine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCOT.php b/samples/Calculations/Engineering/IMCOT.php new file mode 100644 index 00000000..e3d980cd --- /dev/null +++ b/samples/Calculations/Engineering/IMCOT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCOT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cotangent of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCSC.php b/samples/Calculations/Engineering/IMCSC.php new file mode 100644 index 00000000..ab6695d0 --- /dev/null +++ b/samples/Calculations/Engineering/IMCSC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCSC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Cosecant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMCSCH.php b/samples/Calculations/Engineering/IMCSCH.php new file mode 100644 index 00000000..4513d9e9 --- /dev/null +++ b/samples/Calculations/Engineering/IMCSCH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMCSCH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Cosecant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMDIV.php b/samples/Calculations/Engineering/IMDIV.php new file mode 100644 index 00000000..9512be57 --- /dev/null +++ b/samples/Calculations/Engineering/IMDIV.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMDIV(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Quotient of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMEXP.php b/samples/Calculations/Engineering/IMEXP.php new file mode 100644 index 00000000..7f5837b2 --- /dev/null +++ b/samples/Calculations/Engineering/IMEXP.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMEXP(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Exponential of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLN.php b/samples/Calculations/Engineering/IMLN.php new file mode 100644 index 00000000..95618257 --- /dev/null +++ b/samples/Calculations/Engineering/IMLN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Natural Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLOG10.php b/samples/Calculations/Engineering/IMLOG10.php new file mode 100644 index 00000000..d501c3de --- /dev/null +++ b/samples/Calculations/Engineering/IMLOG10.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLOG10(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Base-10 Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMLOG2.php b/samples/Calculations/Engineering/IMLOG2.php new file mode 100644 index 00000000..25986b39 --- /dev/null +++ b/samples/Calculations/Engineering/IMLOG2.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMLOG2(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Base-2 Logarithm of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMPOWER.php b/samples/Calculations/Engineering/IMPOWER.php new file mode 100644 index 00000000..c6674fbe --- /dev/null +++ b/samples/Calculations/Engineering/IMPOWER.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', 2], + ['5-12i', 2], + ['3.25+7.5i', 3], + ['3.25-12.5i', 2], + ['-3.25+7.5i', 3], + ['-3.25-7.5i', 4], + ['0-j', 5], + ['0-2.5j', 3], + ['0+j', 2.5], + ['0+1.25j', 2], + [4, 3], + [-2.5, 2], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMPOWER(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): %s raised to the power of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMPRODUCT.php b/samples/Calculations/Engineering/IMPRODUCT.php new file mode 100644 index 00000000..f81bc666 --- /dev/null +++ b/samples/Calculations/Engineering/IMPRODUCT.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMPRODUCT(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Product of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMREAL.php b/samples/Calculations/Engineering/IMREAL.php new file mode 100644 index 00000000..4e537c0f --- /dev/null +++ b/samples/Calculations/Engineering/IMREAL.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMREAL(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The real component of %s is %f radians', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSEC.php b/samples/Calculations/Engineering/IMSEC.php new file mode 100644 index 00000000..e6c524b3 --- /dev/null +++ b/samples/Calculations/Engineering/IMSEC.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Secant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSECH.php b/samples/Calculations/Engineering/IMSECH.php new file mode 100644 index 00000000..e07b6e08 --- /dev/null +++ b/samples/Calculations/Engineering/IMSECH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSECH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Secant of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSIN.php b/samples/Calculations/Engineering/IMSIN.php new file mode 100644 index 00000000..d3b8c281 --- /dev/null +++ b/samples/Calculations/Engineering/IMSIN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Sine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSINH.php b/samples/Calculations/Engineering/IMSINH.php new file mode 100644 index 00000000..ac0a9039 --- /dev/null +++ b/samples/Calculations/Engineering/IMSINH.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSINH(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Hyperbolic Sine of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSQRT.php b/samples/Calculations/Engineering/IMSQRT.php new file mode 100644 index 00000000..c2573c91 --- /dev/null +++ b/samples/Calculations/Engineering/IMSQRT.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMSQRT(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Square Root of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSUB.php b/samples/Calculations/Engineering/IMSUB.php new file mode 100644 index 00000000..90bd27a4 --- /dev/null +++ b/samples/Calculations/Engineering/IMSUB.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMSUB(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Difference between %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMSUM.php b/samples/Calculations/Engineering/IMSUM.php new file mode 100644 index 00000000..2a8be320 --- /dev/null +++ b/samples/Calculations/Engineering/IMSUM.php @@ -0,0 +1,42 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i', '5-3i'], + ['3+4i', '5+3i'], + ['-238+240i', '10+24i'], + ['1+2i', 30], + ['1+2i', '2i'], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('C' . $row, '=IMSUM(A' . $row . ', B' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Sum of %s and %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getValue(), + $worksheet->getCell('C' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/IMTAN.php b/samples/Calculations/Engineering/IMTAN.php new file mode 100644 index 00000000..ffaa53b2 --- /dev/null +++ b/samples/Calculations/Engineering/IMTAN.php @@ -0,0 +1,48 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + ['3+4i'], + ['5-12i'], + ['3.25+7.5i'], + ['3.25-12.5i'], + ['-3.25+7.5i'], + ['-3.25-7.5i'], + ['0-j'], + ['0-2.5j'], + ['0+j'], + ['0+1.25j'], + [4], + [-2.5], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=IMTAN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(E%d): The Tangent of %s is %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2BIN.php b/samples/Calculations/Engineering/OCT2BIN.php new file mode 100644 index 00000000..9c4bbf86 --- /dev/null +++ b/samples/Calculations/Engineering/OCT2BIN.php @@ -0,0 +1,47 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [7], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [567], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2BIN(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is binary %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2DEC.php b/samples/Calculations/Engineering/OCT2DEC.php new file mode 100644 index 00000000..ea6afb2f --- /dev/null +++ b/samples/Calculations/Engineering/OCT2DEC.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [7], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [4567], + [7777700001], + [7777776543], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2DEC(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is decimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Engineering/OCT2HEX.php b/samples/Calculations/Engineering/OCT2HEX.php new file mode 100644 index 00000000..47e9b6e1 --- /dev/null +++ b/samples/Calculations/Engineering/OCT2HEX.php @@ -0,0 +1,49 @@ +titles($category, $functionName, $description); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +// Add some data +$testData = [ + [3], + [12], + [42], + [70], + [72], + [77], + [100], + [127], + [177], + [456], + [4567], + [7777700001], + [7777776543], +]; +$testDataCount = count($testData); + +$worksheet->fromArray($testData, null, 'A1', true); + +for ($row = 1; $row <= $testDataCount; ++$row) { + $worksheet->setCellValue('B' . $row, '=OCT2HEX(A' . $row . ')'); +} + +// Test the formulae +for ($row = 1; $row <= $testDataCount; ++$row) { + $helper->log(sprintf( + '(B%d): Octal %s is hexadecimal %s', + $row, + $worksheet->getCell('A' . $row)->getValue(), + $worksheet->getCell('B' . $row)->getCalculatedValue(), + )); +} diff --git a/samples/Calculations/Financial/FV.php b/samples/Calculations/Financial/FV.php index 31ddb630..40b3366f 100644 --- a/samples/Calculations/Financial/FV.php +++ b/samples/Calculations/Financial/FV.php @@ -32,5 +32,5 @@ $worksheet->getStyle('B8:C8')->getNumberFormat()->setFormatCode('$#,##0.00'); $helper->log($worksheet->getCell('B8')->getValue()); $helper->log('FV() Result is ' . $worksheet->getCell('B8')->getFormattedValue()); -$helper->log($worksheet->getCell('C6')->getValue()); +$helper->log($worksheet->getCell('C8')->getValue()); $helper->log('FV() Result is ' . $worksheet->getCell('C8')->getFormattedValue()); diff --git a/samples/Calculations/LookupRef/VLOOKUP.php b/samples/Calculations/LookupRef/VLOOKUP.php index 3e7eaa71..16a911e8 100644 --- a/samples/Calculations/LookupRef/VLOOKUP.php +++ b/samples/Calculations/LookupRef/VLOOKUP.php @@ -14,8 +14,8 @@ $worksheet = $spreadsheet->getActiveSheet(); $data = [ ['ID', 'First Name', 'Last Name', 'Salary'], - [72, 'Emily', 'Smith', 64901, null, 'ID', 53, 66, 56], - [66, 'James', 'Anderson', 70855, null, 'Salary'], + [72, 'Emily', 'Smith', 64901], + [66, 'James', 'Anderson', 70855], [14, 'Mia', 'Clark', 188657], [30, 'John', 'Lewis', 97566], [53, 'Jessica', 'Walker', 58339], @@ -25,11 +25,24 @@ $data = [ $worksheet->fromArray($data, null, 'B2'); -$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); -$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); -$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); +$lookupFields = [ + ['ID', 53, 66, 56], + ['Name'], + ['Salary'], +]; + +$worksheet->fromArray($lookupFields, null, 'G3'); + +$worksheet->getCell('H4')->setValue('=VLOOKUP(H3, B3:E9, 2, FALSE) & " " & VLOOKUP(H3, B3:E9, 3, FALSE)'); +$worksheet->getCell('I4')->setValue('=VLOOKUP(I3, B3:E9, 2, FALSE) & " " & VLOOKUP(I3, B3:E9, 3, FALSE)'); +$worksheet->getCell('J4')->setValue('=VLOOKUP(J3, B3:E9, 2, FALSE) & " " & VLOOKUP(J3, B3:E9, 3, FALSE)'); +$worksheet->getCell('H5')->setValue('=VLOOKUP(H3, B3:E9, 4, FALSE)'); +$worksheet->getCell('I5')->setValue('=VLOOKUP(I3, B3:E9, 4, FALSE)'); +$worksheet->getCell('J5')->setValue('=VLOOKUP(J3, B3:E9, 4, FALSE)'); for ($column = 'H'; $column !== 'K'; ++$column) { - $cell = $worksheet->getCell("{$column}4"); - $helper->log("{$column}4: {$cell->getValue()} => {$cell->getCalculatedValue()}"); + for ($row = 4; $row <= 5; ++$row) { + $cell = $worksheet->getCell("{$column}{$row}"); + $helper->log("{$column}{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); + } } diff --git a/samples/Chart/32_Chart_read_write_HTML.php b/samples/Chart/32_Chart_read_write_HTML.php index 5febbf93..90d61c5d 100644 --- a/samples/Chart/32_Chart_read_write_HTML.php +++ b/samples/Chart/32_Chart_read_write_HTML.php @@ -6,7 +6,8 @@ use PhpOffice\PhpSpreadsheet\Settings; require __DIR__ . '/../Header.php'; // Change these values to select the Rendering library that you wish to use -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +//Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; $inputFileNames = __DIR__ . '/../templates/36write*.xlsx'; diff --git a/samples/Chart/32_Chart_read_write_PDF.php b/samples/Chart/32_Chart_read_write_PDF.php index ee3ad0e0..214c3d38 100644 --- a/samples/Chart/32_Chart_read_write_PDF.php +++ b/samples/Chart/32_Chart_read_write_PDF.php @@ -8,7 +8,8 @@ require __DIR__ . '/../Header.php'; IOFactory::registerWriter('Pdf', \PhpOffice\PhpSpreadsheet\Writer\Pdf\Mpdf::class); // Change these values to select the Rendering library that you wish to use -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +//Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; $inputFileNames = __DIR__ . '/../templates/36write*.xlsx'; diff --git a/samples/Chart/33_Chart_create_bar_custom_colors.php b/samples/Chart/33_Chart_create_bar_custom_colors.php new file mode 100644 index 00000000..75f2306b --- /dev/null +++ b/samples/Chart/33_Chart_create_bar_custom_colors.php @@ -0,0 +1,183 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Custom colors for dataSeries (gray, blue, red, orange) +$colors = [ + 'cccccc', '00abb8', 'b8292f', 'eb8500', +]; + +// 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 +$dataSeriesLabels1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues1 = [ + 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 +// Custom colors +$dataSeriesValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4, [], null, $colors), +]; + +// Build the dataseries +$series1 = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues +); + +// Set up a layout object for the Pie chart +$layout1 = new Layout(); +$layout1->setShowVal(true); +$layout1->setShowPercent(true); + +// Set the series in the plot area +$plotArea1 = new PlotArea($layout1, [$series1]); +// Set the chart legend +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title1 = new Title('Test Bar Chart'); + +// Create the chart +$chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Pie charts don't have a Y-Axis +); + +// Set the position where the chart should appear in the worksheet +$chart1->setTopLeftPosition('A7'); +$chart1->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart1); + +// 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 +$dataSeriesLabels2 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues2 = [ + 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 +// Custom colors +$dataSeriesValues2 = [ + $dataSeriesValues2Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), +]; +$dataSeriesValues2Element->setFillColor($colors); + +// Build the dataseries +$series2 = new DataSeries( + DataSeries::TYPE_DONUTCHART, // plotType + null, // plotGrouping (Donut charts don't have any grouping) + range(0, count($dataSeriesValues2) - 1), // plotOrder + $dataSeriesLabels2, // plotLabel + $xAxisTickValues2, // plotCategory + $dataSeriesValues2 // plotValues +); + +// Set up a layout object for the Pie chart +$layout2 = new Layout(); +$layout2->setShowVal(true); +$layout2->setShowCatName(true); + +// Set the series in the plot area +$plotArea2 = new PlotArea($layout2, [$series2]); + +$title2 = new Title('Test Donut Chart'); + +// Create the chart +$chart2 = new Chart( + 'chart2', // name + $title2, // title + null, // legend + $plotArea2, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Like Pie charts, Donut charts don't have a Y-Axis +); + +// Set the position where the chart should appear in the worksheet +$chart2->setTopLeftPosition('I7'); +$chart2->setBottomRightPosition('P20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart2); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_bar_labels_lines.php b/samples/Chart/33_Chart_create_bar_labels_lines.php new file mode 100644 index 00000000..970d632c --- /dev/null +++ b/samples/Chart/33_Chart_create_bar_labels_lines.php @@ -0,0 +1,209 @@ +getActiveSheet(); +$worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] +); + +// Custom colors for dataSeries (gray, blue, red, orange) +$colors = [ + 'cccccc', '00abb8', 'b8292f', 'eb8500', +]; + +// 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 +$dataSeriesLabels1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues1 = [ + 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 +// Custom colors +$dataSeriesValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4, [], null, $colors), +]; +$labelLayout = new Layout(); +$labelLayout + ->setShowVal(true) + ->setLabelFontColor(new ChartColor('FFFF00')) + ->setLabelFillColor(new ChartColor('accent2', null, 'schemeClr')); +$dataSeriesValues1[0]->setLabelLayout($labelLayout); + +// Build the dataseries +$series1 = new DataSeries( + DataSeries::TYPE_BARCHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues +); + +// Set up a layout object for the Pie chart +$layout1 = new Layout(); +$layout1->setShowVal(true); +$layout1->setShowPercent(true); + +// Set the series in the plot area +$plotArea1 = new PlotArea($layout1, [$series1]); +// Set the chart legend +$legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title1 = new Title('Test Bar Chart'); + +// Create the chart +$chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Pie charts don't have a Y-Axis +); +$majorGridlinesY = new GridLines(); +$majorGridlinesY->setLineColorProperties('FF0000'); +$minorGridlinesY = new GridLines(); +$minorGridlinesY->setLineStyleProperty('dash', Properties::LINE_STYLE_DASH_ROUND_DOT); +$chart1 + ->getChartAxisY() + ->setMajorGridlines($majorGridlinesY) + ->setMinorGridlines($minorGridlinesY); +$majorGridlinesX = new GridLines(); +$majorGridlinesX->setLineColorProperties('FF00FF'); +$minorGridlinesX = new GridLines(); +$minorGridlinesX->activateObject(); +$chart1 + ->getChartAxisX() + ->setMajorGridlines($majorGridlinesX) + ->setMinorGridlines($minorGridlinesX); + +// Set the position where the chart should appear in the worksheet +$chart1->setTopLeftPosition('A7'); +$chart1->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart1); + +// 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 +$dataSeriesLabels2 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 +]; +// Set the X-Axis Labels +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +// Data values +// Data Marker +$xAxisTickValues2 = [ + 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 +// Custom colors +$dataSeriesValues2 = [ + $dataSeriesValues2Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4), +]; +$dataSeriesValues2Element->setFillColor($colors); + +// Build the dataseries +$series2 = new DataSeries( + DataSeries::TYPE_DONUTCHART, // plotType + null, // plotGrouping (Donut charts don't have any grouping) + range(0, count($dataSeriesValues2) - 1), // plotOrder + $dataSeriesLabels2, // plotLabel + $xAxisTickValues2, // plotCategory + $dataSeriesValues2 // plotValues +); + +// Set up a layout object for the Pie chart +$layout2 = new Layout(); +$layout2->setShowVal(true); +$layout2->setShowCatName(true); +$layout2->setLabelFillColor(new ChartColor('FFFF00')); + +// Set the series in the plot area +$plotArea2 = new PlotArea($layout2, [$series2]); + +$title2 = new Title('Test Donut Chart'); + +// Create the chart +$chart2 = new Chart( + 'chart2', // name + $title2, // title + null, // legend + $plotArea2, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // yAxisLabel - Like Pie charts, Donut charts don't have a Y-Axis +); + +// Set the position where the chart should appear in the worksheet +$chart2->setTopLeftPosition('I7'); +$chart2->setBottomRightPosition('P20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart2); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_bubble.php b/samples/Chart/33_Chart_create_bubble.php index 33feea62..5efcda71 100644 --- a/samples/Chart/33_Chart_create_bubble.php +++ b/samples/Chart/33_Chart_create_bubble.php @@ -89,7 +89,8 @@ $series = new DataSeries( $series->setPlotBubbleSizes($dataSeriesBubbles); // Set the series in the plot area -$plotArea = new PlotArea(null, [$series]); +$plotArea = new PlotArea(); +$plotArea->setPlotSeries([$series]); // Set the chart legend $legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); diff --git a/samples/Chart/33_Chart_create_line.php b/samples/Chart/33_Chart_create_line.php index fee2a284..8cb87e30 100644 --- a/samples/Chart/33_Chart_create_line.php +++ b/samples/Chart/33_Chart_create_line.php @@ -5,6 +5,7 @@ use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -35,6 +36,7 @@ $dataSeriesLabels = [ new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2011 new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2012 ]; +$dataSeriesLabels[0]->setFillColor('FF0000'); // Set the X-Axis Labels // Datatype // Cell reference for data @@ -57,7 +59,7 @@ $dataSeriesValues = [ 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), ]; -$dataSeriesValues[2]->setLineWidth(60000); +$dataSeriesValues[2]->setLineWidth(60000 / Properties::POINTS_WIDTH_MULTIPLIER); // Build the dataseries $series = new DataSeries( diff --git a/samples/Chart/33_Chart_create_line_dateaxis.php b/samples/Chart/33_Chart_create_line_dateaxis.php new file mode 100644 index 00000000..413866e2 --- /dev/null +++ b/samples/Chart/33_Chart_create_line_dateaxis.php @@ -0,0 +1,375 @@ +getActiveSheet(); +$dataSheet->setTitle('Data'); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +// Dates changed not to fall on exact quarter start +$dataSheet->fromArray( + [ + ['', 'date', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE(B2)', '2021-01-10', 12.1, 15.1, 21.1], + ['=DATEVALUE(B3)', '2021-04-21', 56.2, 73.2, 86.2], + ['=DATEVALUE(B4)', '2021-07-31', 52.2, 61.2, 69.2], + ['=DATEVALUE(B5)', '2021-10-11', 30.2, 22.2, 0.2], + ['=DATEVALUE(B6)', '2022-01-21', 40.1, 38.1, 65.1], + ['=DATEVALUE(B7)', '2022-04-11', 45.2, 44.2, 96.2], + ['=DATEVALUE(B8)', '2022-07-01', 52.2, 51.2, 55.2], + ['=DATEVALUE(B9)', '2022-10-31', 41.2, 72.2, 56.2], + ] +); + +$dataSheet->getStyle('A2:A9')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$dataSheet->getColumnDimension('A')->setAutoSize(true); +$dataSheet->getColumnDimension('B')->setAutoSize(true); +$dataSheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$C$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$E$1', null, 1), +]; +// Set the X-Axis Labels +// NUMBER, not STRING +// added x-axis values for each of the 3 metrics +// added FORMATE_CODE_NUMBER +// Number of datapoints in series +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; +// 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 +// Data Marker Color fill/[fill,Border] +// Data Marker size +// Color(s) added +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$C$2:$C$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'diamond', + null, + 5 + ), + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$D$2:$D$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'square', + '*accent1', + 6 + ), + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$E$2:$E$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + null, + null, + 7 + ), // let Excel choose marker shape +]; +// series 1 - metric1 +// marker details +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - dashed, smooth line (Bezier) with arrows, 40% transparent +$dataSeriesValues[0] + ->setSmoothLine(true) + ->setScatterLines(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - metric2, straight line - no special effects, connected +$dataSeriesValues[1] // square marker border color + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square marker fill color + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - metric3, markers, no line +$dataSeriesValues[2] // triangle? fill + //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart'); +$yAxisLabel = new Title('Value ($k)'); +$yAxis = new Axis(); +$yAxis->setMajorGridlines(new GridLines()); + +// 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 + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('P12'); + +// create a 'Chart' worksheet, add $chart to it +$spreadsheet->createSheet(); +$chartSheet = $spreadsheet->getSheet(1); +$chartSheet->setTitle('Scatter+Line Chart'); + +$chartSheet = $spreadsheet->getSheetByName('Scatter+Line Chart'); +// Add the chart to the worksheet +$chartSheet->addChart($chart); + +// ------- Demonstrate Date Xaxis in Line Chart, not possible using Scatter Chart ------------ + +// Set the Labels (Column header) for each data series we want to plot +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$E$1', null, 1), +]; + +// Set the X-Axis Labels - dates, N.B. 01/10/2021 === Jan 10, NOT Oct 1 !! +// x-axis values are the Excel numeric representation of the date - so set +// formatCode=General for the xAxis VALUES, but we want the labels to be +// DISPLAYED as 'yyyy-mm-dd' That is, read a number, display a date. +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; + +// X axis (date) settings +$xAxisLabel = new Title('Date'); +$xAxis = new Axis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // yyyy-mm-dd + +// Set the Data values for each data series we want to plot +$dataSeriesValues = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_NUMBER, + 'Data!$E$2:$E$9', + Properties::FORMAT_CODE_NUMBER, + 8, + null, + 'triangle', + null, + 7 + ), +]; + +// series - metric3, markers, no line +$dataSeriesValues[0] + ->setScatterlines(false); // disable connecting lines +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); + +// Build the dataseries +// must now use LineChart instead of ScatterChart, since ScatterChart does not +// support "dateAx" axis type. +$series = new DataSeries( + DataSeries::TYPE_LINECHART, // plotType + 'standard', // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + // DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + +$title = new Title('Test Line-Chart with Date Axis - metric3 values'); + +// X axis (date) settings +$xAxisLabel = new Title('Game Date'); +$xAxis = new Axis(); +// date axis values are Excel numbers, not yyyy-mm-dd Date strings +$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 +// change xAxis tick marks to match Qtr boundaries + +$nQtrs = sprintf('%3.2f', (($dateMinMax['max'] - $dateMinMax['min']) / 30.5) / 4); +$tickMarkInterval = ($nQtrs > 20) ? 6 : 3; // tick marks every ? months + +$xAxis->setAxisOptionsProperties( + Properties::AXIS_LABELS_NEXT_TO, // axis_label pos + null, // horizontalCrossesValue + null, // horizontalCrosses + null, // axisOrientation + 'in', // major_tick_mark + null, // minor_tick_mark + $dateMinMax['min'], // minimum calculate this from the earliest data: 'Data!$A$2' + $dateMinMax['max'], // maximum calculate this from the last data: 'Data!$A$'.($nrows+1) + $tickMarkInterval, // majorUnit determines tickmarks & Gridlines ? + null, // minorUnit + null, // textRotation + null, // hidden + 'days', // baseTimeUnit + 'months', // majorTimeUnit, + 'months', // minorTimeUnit +); + +$yAxisLabel = new Title('Value ($k)'); +$yAxis = new Axis(); +$yAxis->setMajorGridlines(new GridLines()); +$xAxis->setMajorGridlines(new GridLines()); +$minorGridLines = new GridLines(); +$minorGridLines->activateObject(); +$xAxis->setMinorGridlines($minorGridLines); + +// Create the chart +$chart = new Chart( + 'chart2', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet below the first chart +$chart->setTopLeftPosition('A13'); +$chart->setBottomRightPosition('P25'); +$chart->setRoundedCorners(true); // Rounded corners in Chart Outline + +// Add the chart to the worksheet $chartSheet +$chartSheet->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); +$spreadsheet->disconnectWorksheets(); + +function dateRange(int $nrows, Spreadsheet $wrkbk): array +{ + $dataSheet = $wrkbk->getSheetByName('Data'); + + // start the xaxis at the beginning of the quarter of the first date + $startDateStr = $dataSheet->getCell('B2')->getValue(); // yyyy-mm-dd date string + $startDate = DateTime::createFromFormat('Y-m-d', $startDateStr); // php date obj + + // get date of first day of the quarter of the start date + $startMonth = (int) $startDate->format('n'); // suppress leading zero + $startYr = (int) $startDate->format('Y'); + $qtr = intdiv($startMonth, 3) + (($startMonth % 3 > 0) ? 1 : 0); + $qtrStartMonth = sprintf('%02d', 1 + (($qtr - 1) * 3)); + $qtrStartStr = "$startYr-$qtrStartMonth-01"; + $ExcelQtrStartDateVal = SharedDate::convertIsoDate($qtrStartStr); + + // end the xaxis at the end of the quarter of the last date + $lastDateStr = $dataSheet->getCellByColumnAndRow(2, $nrows + 1)->getValue(); + $lastDate = DateTime::createFromFormat('Y-m-d', $lastDateStr); + $lastMonth = (int) $lastDate->format('n'); + $lastYr = (int) $lastDate->format('Y'); + $qtr = intdiv($lastMonth, 3) + (($lastMonth % 3 > 0) ? 1 : 0); + $qtrEndMonth = 3 + (($qtr - 1) * 3); + $lastDOM = cal_days_in_month(CAL_GREGORIAN, $qtrEndMonth, $lastYr); + $qtrEndMonth = sprintf('%02d', $qtrEndMonth); + $qtrEndStr = "$lastYr-$qtrEndMonth-$lastDOM"; + $ExcelQtrEndDateVal = SharedDate::convertIsoDate($qtrEndStr); + + $minMaxDates = ['min' => $ExcelQtrStartDateVal, 'max' => $ExcelQtrEndDateVal]; + + return $minMaxDates; +} diff --git a/samples/Chart/33_Chart_create_scatter2.php b/samples/Chart/33_Chart_create_scatter2.php index 1d6e331c..cbc3ec1a 100644 --- a/samples/Chart/33_Chart_create_scatter2.php +++ b/samples/Chart/33_Chart_create_scatter2.php @@ -1,7 +1,8 @@ setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new ChartAxis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); + +$yAxis = new ChartAxis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); // Build the dataseries $series = new DataSeries( @@ -79,8 +146,7 @@ $series = new DataSeries( $dataSeriesValues, // plotValues null, // plotDirection false, // smooth line - //DataSeries::STYLE_LINEMARKER // plotStyle - DataSeries::STYLE_MARKER // plotStyle + DataSeries::STYLE_SMOOTHMARKER // plotStyle ); // Set the series in the plot area @@ -103,6 +169,7 @@ $chart = new Chart( $yAxisLabel, // yAxisLabel // added xAxis for correct date display $xAxis, // xAxis + $yAxis, // yAxis ); // Set the position where the chart should appear in the worksheet diff --git a/samples/Chart/33_Chart_create_scatter3.php b/samples/Chart/33_Chart_create_scatter3.php new file mode 100644 index 00000000..899fca79 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter3.php @@ -0,0 +1,192 @@ +getActiveSheet(); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$worksheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-01-04")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-01-07")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], + ] +); +$worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$worksheet->getColumnDimension('A')->setAutoSize(true); +$worksheet->setSelectedCells('A1'); + +// 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), // was 2010 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // was 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // was 2012 +]; +// Set the X-Axis Labels +// changed from STRING to NUMBER +// added 2 additional x-axis values associated with each of the 3 metrics +// added FORMATE_CODE_NUMBER +$xAxisTickValues = [ + //new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$A$2:$A$5', null, 4), // Q1 to Q4 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$A$2:$A$5', Properties::FORMAT_CODE_DATE, 4), +]; +// 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 +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', Properties::FORMAT_CODE_NUMBER, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', Properties::FORMAT_CODE_NUMBER, 4), +]; + +// series 1 +// marker details +$dataSeriesValues[0] + ->setPointMarker('diamond') + ->setPointSize(5) + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - smooth line, connected +$dataSeriesValues[0] + ->setScatterLines(true) + ->setSmoothLine(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - straight line - no special effects, connected, straight line +$dataSeriesValues[1] // square fill + ->setPointMarker('square') + ->setPointSize(6) + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square border + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - markers, no line +$dataSeriesValues[2] // triangle fill + //->setPointMarker('triangle') // let Excel choose shape + ->setPointSize(7) + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected + +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new ChartAxis(); +//$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); +$xAxis->setAxisOption('textRotation', '45'); +$xAxis->setAxisOption('hidden', '1'); + +$yAxis = new ChartAxis(); +$yAxis->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_SIMPLE, + Properties::LINE_STYLE_DASH_DASH_DOT, + Properties::LINE_STYLE_CAP_FLAT, + Properties::LINE_STYLE_JOIN_BEVEL +); +$yAxis->setLineColorProperties('ffc000', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$yAxis->setAxisOption('hidden', '1'); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + false, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +$plotArea->setNoFill(true); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Trend 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 + null, //$yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + $yAxis, // yAxis +); +$chart->setNoFill(true); + +// Set the position where the chart should appear in the worksheet +$chart->setTopLeftPosition('A7'); +$chart->setBottomRightPosition('P20'); +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$spreadsheet->disconnectWorksheets(); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_scatter4.php b/samples/Chart/33_Chart_create_scatter4.php new file mode 100644 index 00000000..d42933b6 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter4.php @@ -0,0 +1,130 @@ +getActiveSheet(); +$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 +$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_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have any grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); + +$pos1 = 0; // pos = 0% (extreme low side or lower left corner) +$brightness1 = 0; // 0% +$gsColor1 = new ChartColor(); +$gsColor1->setColorProperties('FF0000', 75, 'srgbClr', $brightness1); // red +$gradientStop1 = [$pos1, $gsColor1]; + +$pos2 = 0.5; // pos = 50% (middle) +$brightness2 = 0.5; // 50% +$gsColor2 = new ChartColor(); +$gsColor2->setColorProperties('FFFF00', 50, 'srgbClr', $brightness2); // yellow +$gradientStop2 = [$pos2, $gsColor2]; + +$pos3 = 1.0; // pos = 100% (extreme high side or upper right corner) +$brightness3 = 0.5; // 50% +$gsColor3 = new ChartColor(); +$gsColor3->setColorProperties('00B050', 50, 'srgbClr', $brightness3); // green +$gradientStop3 = [$pos3, $gsColor3]; + +$gradientFillStops = [ + $gradientStop1, + $gradientStop2, + $gradientStop3, +]; +$gradientFillAngle = 315.0; // 45deg above horiz + +$plotArea->setGradientFillProperties($gradientFillStops, $gradientFillAngle); + +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter 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('A7'); +$chart->setBottomRightPosition('H20'); + +// Add the chart to the worksheet +$worksheet->addChart($chart); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/33_Chart_create_scatter5_trendlines.php b/samples/Chart/33_Chart_create_scatter5_trendlines.php new file mode 100644 index 00000000..dcee3e14 --- /dev/null +++ b/samples/Chart/33_Chart_create_scatter5_trendlines.php @@ -0,0 +1,274 @@ +getActiveSheet(); +$dataSheet->setTitle('Data'); +// changed data to simulate a trend chart - Xaxis are dates; Yaxis are 3 meausurements from each date +$dataSheet->fromArray( + [ + ['', 'metric1', 'metric2', 'metric3'], + ['=DATEVALUE("2021-01-01")', 12.1, 15.1, 21.1], + ['=DATEVALUE("2021-04-01")', 56.2, 73.2, 86.2], + ['=DATEVALUE("2021-07-01")', 52.2, 61.2, 69.2], + ['=DATEVALUE("2021-10-01")', 30.2, 22.2, 0.2], + ['=DATEVALUE("2022-01-01")', 40.1, 38.1, 65.1], + ['=DATEVALUE("2022-04-01")', 45.2, 44.2, 96.2], + ['=DATEVALUE("2022-07-01")', 52.2, 51.2, 55.2], + ['=DATEVALUE("2022-10-01")', 41.2, 72.2, 56.2], + ] +); + +$dataSheet->getStyle('A2:A9')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); +$dataSheet->getColumnDimension('A')->setAutoSize(true); +$dataSheet->setSelectedCells('A1'); + +// Set the Labels for each data series we want to plot +// Datatype +// Cell reference for data +// Format Code +// Number of datapoints in series +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$B$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$C$1', null, 1), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), +]; +// Set the X-Axis Labels +// NUMBER, not STRING +// added x-axis values for each of the 3 metrics +// added FORMATE_CODE_NUMBER +// Number of datapoints in series +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; +// 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 +// Data Marker Color fill/[fill,Border] +// Data Marker size +// Color(s) added +// added FORMAT_CODE_NUMBER +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$B$2:$B$9', Properties::FORMAT_CODE_NUMBER, 8, null, 'diamond', null, 5), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$C$2:$C$9', Properties::FORMAT_CODE_NUMBER, 8, null, 'square', '*accent1', 6), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$D$2:$D$9', Properties::FORMAT_CODE_NUMBER, 8, null, null, null, 7), // let Excel choose marker shape +]; +// series 1 - metric1 +// marker details +$dataSeriesValues[0] + ->getMarkerFillColor() + ->setColorProperties('0070C0', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0] + ->getMarkerBorderColor() + ->setColorProperties('002060', null, ChartColor::EXCEL_COLOR_TYPE_RGB); + +// line details - dashed, smooth line (Bezier) with arrows, 40% transparent +$dataSeriesValues[0] + ->setSmoothLine(true) + ->setScatterLines(true) + ->setLineColorProperties('accent1', 40, ChartColor::EXCEL_COLOR_TYPE_SCHEME); // value, alpha, type +$dataSeriesValues[0]->setLineStyleProperties( + 2.5, // width in points + Properties::LINE_STYLE_COMPOUND_TRIPLE, // compound + Properties::LINE_STYLE_DASH_SQUARE_DOT, // dash + Properties::LINE_STYLE_CAP_SQUARE, // cap + Properties::LINE_STYLE_JOIN_MITER, // join + Properties::LINE_STYLE_ARROW_TYPE_OPEN, // head type + Properties::LINE_STYLE_ARROW_SIZE_4, // head size preset index + Properties::LINE_STYLE_ARROW_TYPE_ARROW, // end type + Properties::LINE_STYLE_ARROW_SIZE_6 // end size preset index +); + +// series 2 - metric2, straight line - no special effects, connected +$dataSeriesValues[1] // square marker border color + ->getMarkerBorderColor() + ->setColorProperties('accent6', 3, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[1] // square marker fill color + ->getMarkerFillColor() + ->setColorProperties('0FFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1] + ->setScatterLines(true) + ->setSmoothLine(false) + ->setLineColorProperties('FF0000', 80, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[1]->setLineWidth(2.0); + +// series 3 - metric3, markers, no line +$dataSeriesValues[2] // triangle? fill + //->setPointMarker('triangle') // let Excel choose shape, which is predicted to be a triangle + ->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[2] // triangle border + ->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[2]->setScatterLines(false); // points not connected +// Added so that Xaxis shows dates instead of Excel-equivalent-year1900-numbers +$xAxis = new ChartAxis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, true); + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter 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 + // added xAxis for correct date display + $xAxis, // xAxis + // $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet +$chart->setTopLeftPosition('A1'); +$chart->setBottomRightPosition('P12'); + +// create a 'Chart' worksheet, add $chart to it +$spreadsheet->createSheet(); +$chartSheet = $spreadsheet->getSheet(1); +$chartSheet->setTitle('Scatter Chart'); + +$chartSheet = $spreadsheet->getSheetByName('Scatter Chart'); +// Add the chart to the worksheet +$chartSheet->addChart($chart); + +// ------------ Demonstrate Trendlines for metric3 values in a new chart ------------ + +$dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Data!$D$1', null, 1), +]; +$xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$A$2:$A$9', Properties::FORMAT_CODE_DATE_ISO8601, 8), +]; + +$dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Data!$D$2:$D$9', Properties::FORMAT_CODE_NUMBER, 4, null, 'triangle', null, 7), +]; + +// add 3 trendlines: +// 1- linear, double-ended arrow, w=0.5, same color as marker fill; nodispRSqr, nodispEq +// 2- polynomial (order=3) no-arrow trendline, w=1.25, same color as marker fill; dispRSqr, dispEq +// 3- moving Avg (period=2) single-arrow trendline, w=1.5, same color as marker fill; no dispRSqr, no dispEq +$trendLines = [ + new TrendLine(TrendLine::TRENDLINE_LINEAR, null, null, false, false), + new TrendLine(TrendLine::TRENDLINE_POLYNOMIAL, 3, null, true, true, 20.0, 28.0, 44104.5, 'metric3 polynomial fit'), + new TrendLine(TrendLine::TRENDLINE_MOVING_AVG, null, 2, true), +]; +$dataSeriesValues[0]->setTrendLines($trendLines); + +// Suppress connecting lines; instead, add different Trendline algorithms to +// determine how well the data fits the algorithm (Rsquared="goodness of fit") +// Display RSqr plus the eqn just because we can. + +$dataSeriesValues[0]->setScatterLines(false); // points not connected +$dataSeriesValues[0]->getMarkerFillColor() + ->setColorProperties('FFFF00', null, ChartColor::EXCEL_COLOR_TYPE_RGB); +$dataSeriesValues[0]->getMarkerBorderColor() + ->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); + +// add properties to the trendLines - give each a different color +$dataSeriesValues[0]->getTrendLines()[0]->getLineColor()->setColorProperties('accent4', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[0]->setLineStyleProperties(0.5, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_STEALTH, 5, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); + +$dataSeriesValues[0]->getTrendLines()[1]->getLineColor()->setColorProperties('accent3', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[1]->setLineStyleProperties(1.25); + +$dataSeriesValues[0]->getTrendLines()[2]->getLineColor()->setColorProperties('accent2', null, ChartColor::EXCEL_COLOR_TYPE_SCHEME); +$dataSeriesValues[0]->getTrendLines()[2]->setLineStyleProperties(1.5, null, null, null, null, null, null, Properties::LINE_STYLE_ARROW_TYPE_OPEN, 8); + +$xAxis = new ChartAxis(); +$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); // m/d/yyyy + +// Build the dataseries +$series = new DataSeries( + DataSeries::TYPE_SCATTERCHART, // plotType + null, // plotGrouping (Scatter charts don't have grouping) + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues, // plotValues + null, // plotDirection + null, // smooth line + DataSeries::STYLE_SMOOTHMARKER // plotStyle +); + +// Set the series in the plot area +$plotArea = new PlotArea(null, [$series]); +// Set the chart legend +$legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + +$title = new Title('Test Scatter Chart - trendlines for metric3 values'); +$yAxisLabel = new Title('Value ($k)'); + +// Create the chart +$chart = new Chart( + 'chart2', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + // added xAxis for correct date display + $xAxis, // xAxis + // $yAxis, // yAxis +); + +// Set the position of the chart in the chart sheet below the first chart +$chart->setTopLeftPosition('A13'); +$chart->setBottomRightPosition('P25'); + +// Add the chart to the worksheet $chartSheet +$chartSheet->addChart($chart); +$spreadsheet->setActiveSheetIndex(1); + +// Save Excel 2007 file +$filename = $helper->getFilename(__FILE__); +$writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); +$writer->setIncludeCharts(true); +$callStartTime = microtime(true); +$writer->save($filename); +$helper->logWrite($writer, $filename, $callStartTime); diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index ebab16a7..f6dfeb46 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -5,16 +5,13 @@ use PhpOffice\PhpSpreadsheet\Settings; require __DIR__ . '/../Header.php'; -if (PHP_VERSION_ID >= 80000) { - $helper->log('Jpgraph no longer runs against PHP8'); - exit; -} - // Change these values to select the Rendering library that you wish to use -Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +//Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); $inputFileType = 'Xlsx'; $inputFileNames = __DIR__ . '/../templates/32readwrite*[0-9].xlsx'; +//$inputFileNames = __DIR__ . '/../templates/32readwriteStockChart5.xlsx'; if ((isset($argc)) && ($argc > 1)) { $inputFileNames = []; @@ -24,6 +21,16 @@ if ((isset($argc)) && ($argc > 1)) { } else { $inputFileNames = glob($inputFileNames); } +if (count($inputFileNames) === 1) { + $unresolvedErrors = []; +} else { + $unresolvedErrors = [ + // The following spreadsheet was created by 3rd party software, + // and doesn't include the data that usually accompanies a chart. + // That is good enough for Excel, but not for JpGraph. + '32readwriteBubbleChart2.xlsx', + ]; +} foreach ($inputFileNames as $inputFileName) { $inputFileNameShort = basename($inputFileName); @@ -32,6 +39,13 @@ foreach ($inputFileNames as $inputFileName) { continue; } + if (in_array($inputFileNameShort, $unresolvedErrors, true)) { + $helper->log('*****'); + $helper->log('***** File ' . $inputFileNameShort . ' does not yet work with this script'); + $helper->log('*****'); + + continue; + } $helper->log("Load Test from $inputFileType file " . $inputFileNameShort); @@ -59,6 +73,9 @@ foreach ($inputFileNames as $inputFileName) { $helper->log(' ' . $chartName . ' - ' . $caption); $jpegFile = $helper->getFilename('35-' . $inputFileNameShort, 'png'); + if ($i !== 0) { + $jpegFile = substr($jpegFile, 0, -3) . "$i.png"; + } if (file_exists($jpegFile)) { unlink($jpegFile); } @@ -75,6 +92,7 @@ foreach ($inputFileNames as $inputFileName) { $spreadsheet->disconnectWorksheets(); unset($spreadsheet); + gc_collect_cycles(); } $helper->log('Done rendering charts as images'); diff --git a/samples/Pdf/21a_Pdf.php b/samples/Pdf/21a_Pdf.php index b5572afe..33b61c9f 100644 --- a/samples/Pdf/21a_Pdf.php +++ b/samples/Pdf/21a_Pdf.php @@ -12,6 +12,8 @@ $spreadsheet->getActiveSheet()->setShowGridLines(false); $helper->log('Set orientation to landscape'); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); $spreadsheet->setActiveSheetIndex(0)->setPrintGridlines(true); +// Issue 2299 - mpdf can't handle hide rows without kludge +$spreadsheet->getActiveSheet()->getRowDimension(2)->setVisible(false); function changeGridlines(string $html): string { diff --git a/samples/Pdf/21b_Pdf.php b/samples/Pdf/21b_Pdf.php index ad2f609b..c67ff3d2 100644 --- a/samples/Pdf/21b_Pdf.php +++ b/samples/Pdf/21b_Pdf.php @@ -32,13 +32,11 @@ $spreadsheet->getActiveSheet()->setShowGridLines(false); $helper->log('Set orientation to landscape'); $spreadsheet->getActiveSheet()->getPageSetup()->setOrientation(PageSetup::ORIENTATION_LANDSCAPE); -if (\PHP_VERSION_ID < 80000) { - $helper->log('Write to Dompdf'); - $writer = new Dompdf($spreadsheet); - $filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); - $writer->setEditHtmlCallback('replaceBody'); - $writer->save($filename); -} +$helper->log('Write to Dompdf'); +$writer = new Dompdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_dompdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); $helper->log('Write to Mpdf'); $writer = new Mpdf($spreadsheet); @@ -46,10 +44,8 @@ $filename = $helper->getFileName('21b_Pdf_mpdf.xlsx', 'pdf'); $writer->setEditHtmlCallback('replaceBody'); $writer->save($filename); -if (\PHP_VERSION_ID < 80000) { - $helper->log('Write to Tcpdf'); - $writer = new Tcpdf($spreadsheet); - $filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); - $writer->setEditHtmlCallback('replaceBody'); - $writer->save($filename); -} +$helper->log('Write to Tcpdf'); +$writer = new Tcpdf($spreadsheet); +$filename = $helper->getFileName('21b_Pdf_tcpdf.xlsx', 'pdf'); +$writer->setEditHtmlCallback('replaceBody'); +$writer->save($filename); diff --git a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php index 603f6cb8..d984b7b6 100644 --- a/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php +++ b/samples/Reader/16_Handling_loader_exceptions_using_TryCatch.php @@ -11,5 +11,5 @@ $helper->log('Loading file ' . /** @scrutinizer ignore-type */ pathinfo($inputFi try { $spreadsheet = IOFactory::load($inputFileName); } catch (ReaderException $e) { - $helper->log('Error loading file "' . pathinfo($inputFileName, PATHINFO_BASENAME) . '": ' . $e->getMessage()); + $helper->log('Error loading file "' . /** @scrutinizer ignore-type */ pathinfo($inputFileName, PATHINFO_BASENAME) . '": ' . $e->getMessage()); } diff --git a/samples/templates/32readwriteAreaChart1.xlsx b/samples/templates/32readwriteAreaChart1.xlsx index d44ae534..474affbe 100644 Binary files a/samples/templates/32readwriteAreaChart1.xlsx and b/samples/templates/32readwriteAreaChart1.xlsx differ diff --git a/samples/templates/32readwriteAreaChart4.xlsx b/samples/templates/32readwriteAreaChart4.xlsx new file mode 100644 index 00000000..4548bf2e Binary files /dev/null and b/samples/templates/32readwriteAreaChart4.xlsx differ diff --git a/samples/templates/32readwriteBarChart4.xlsx b/samples/templates/32readwriteBarChart4.xlsx new file mode 100644 index 00000000..c7857295 Binary files /dev/null and b/samples/templates/32readwriteBarChart4.xlsx differ diff --git a/samples/templates/32readwriteLineChart4.xlsx b/samples/templates/32readwriteLineChart4.xlsx new file mode 100644 index 00000000..0944fcbb Binary files /dev/null and b/samples/templates/32readwriteLineChart4.xlsx differ diff --git a/samples/templates/32readwriteLineChart5.xlsx b/samples/templates/32readwriteLineChart5.xlsx new file mode 100644 index 00000000..430cc1d7 Binary files /dev/null and b/samples/templates/32readwriteLineChart5.xlsx differ diff --git a/samples/templates/32readwriteLineDateAxisChart1.xlsx b/samples/templates/32readwriteLineDateAxisChart1.xlsx new file mode 100644 index 00000000..42470303 Binary files /dev/null and b/samples/templates/32readwriteLineDateAxisChart1.xlsx differ diff --git a/samples/templates/32readwriteScatterChart10.xlsx b/samples/templates/32readwriteScatterChart10.xlsx new file mode 100644 index 00000000..7d5ac0d2 Binary files /dev/null and b/samples/templates/32readwriteScatterChart10.xlsx differ diff --git a/samples/templates/32readwriteScatterChart8.xlsx b/samples/templates/32readwriteScatterChart8.xlsx new file mode 100644 index 00000000..1d4080d7 Binary files /dev/null and b/samples/templates/32readwriteScatterChart8.xlsx differ diff --git a/samples/templates/32readwriteScatterChart9.xlsx b/samples/templates/32readwriteScatterChart9.xlsx new file mode 100644 index 00000000..3a0b51b2 Binary files /dev/null and b/samples/templates/32readwriteScatterChart9.xlsx differ diff --git a/samples/templates/32readwriteScatterChartTrendlines1.xlsx b/samples/templates/32readwriteScatterChartTrendlines1.xlsx new file mode 100644 index 00000000..c8411d4d Binary files /dev/null and b/samples/templates/32readwriteScatterChartTrendlines1.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2bba56f6..abeebf39 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -7,10 +7,10 @@ use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack; use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger; use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; -use PhpOffice\PhpSpreadsheet\Calculation\Information\Value; use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Shared; @@ -33,17 +33,17 @@ class Calculation // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) - const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative - const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; - const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; - const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'.*?\')|(\".*?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; + const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; + const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative // Cell ranges ensuring absolute/relative const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})'; // Defined Names: Named Range of cells, or Named Formulae - const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'.*?\')|(\".*?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; + const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; @@ -58,6 +58,7 @@ class Calculation const FORMULA_CLOSE_MATRIX_BRACE = '}'; const FORMULA_STRING_QUOTE = '"'; + /** @var string */ private static $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE; /** @@ -136,9 +137,14 @@ class Calculation * If true, then a user error will be triggered * If false, then an exception will be thrown. * - * @var bool + * @var ?bool + * + * @deprecated 1.25.2 use setSuppressFormulaErrors() instead */ - public $suppressFormulaErrors = false; + public $suppressFormulaErrors; + + /** @var bool */ + private $suppressFormulaErrorsNew = false; /** * Error message for any error that was raised/thrown by the calculation engine. @@ -161,6 +167,7 @@ class Calculation */ private $cyclicReferenceStack; + /** @var array */ private $cellStack = []; /** @@ -172,6 +179,7 @@ class Calculation */ private $cyclicFormulaCounter = 1; + /** @var string */ private $cyclicFormulaCell = ''; /** @@ -205,6 +213,7 @@ class Calculation */ private static $localeArgumentSeparator = ','; + /** @var array */ private static $localeFunctions = []; /** @@ -230,7 +239,7 @@ class Calculation 'NULL' => null, ]; - // PhpSpreadsheet functions + /** @var array */ private static $phpSpreadsheetFunctions = [ 'ABS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, @@ -304,8 +313,8 @@ class Calculation ], 'ARRAYTOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Text::class, 'fromArray'], + 'argumentCount' => '1,2', ], 'ASC' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, @@ -1450,7 +1459,7 @@ class Calculation 'INDEX' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, 'functionCall' => [LookupRef\Matrix::class, 'index'], - 'argumentCount' => '1-4', + 'argumentCount' => '2-4', ], 'INDIRECT' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, @@ -2489,13 +2498,13 @@ class Calculation ], 'TEXTAFTER' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2-4', + 'functionCall' => [TextData\Extract::class, 'after'], + 'argumentCount' => '2-6', ], 'TEXTBEFORE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2-4', + 'functionCall' => [TextData\Extract::class, 'before'], + 'argumentCount' => '2-6', ], 'TEXTJOIN' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, @@ -2504,8 +2513,8 @@ class Calculation ], 'TEXTSPLIT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2-5', + 'functionCall' => [TextData\Text::class, 'split'], + 'argumentCount' => '2-6', ], 'THAIDAYOFWEEK' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, @@ -2659,8 +2668,8 @@ class Calculation ], 'VALUETOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Format::class, 'valueToText'], + 'argumentCount' => '1,2', ], 'VAR' => [ 'category' => Category::CATEGORY_STATISTICAL, @@ -2814,7 +2823,11 @@ class Calculation ], ]; - // Internal functions used for special control purposes + /** + * Internal functions used for special control purposes. + * + * @var array + */ private static $controlFunctions = [ 'MKMATRIX' => [ 'argumentCount' => '*', @@ -3088,7 +3101,7 @@ class Calculation } // Test whether we have any language data for this language (any locale) - if (in_array($language, self::$validLocaleLanguages)) { + if (in_array($language, self::$validLocaleLanguages, true)) { // initialise language/locale settings self::$localeFunctions = []; self::$localeArgumentSeparator = ','; @@ -3110,7 +3123,7 @@ class Calculation [$localeFunction] = explode('##', $localeFunction); // Strip out comments if (strpos($localeFunction, '=') !== false) { [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); - if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { + if ((substr($fName, 0, 1) === '*' || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { self::$localeFunctions[$fName] = $lfName; } } @@ -3243,10 +3256,17 @@ class Calculation return $formula; } + /** @var ?array */ private static $functionReplaceFromExcel; + /** @var ?array */ private static $functionReplaceToLocale; + /** + * @param string $formula + * + * @return string + */ public function _translateFormulaToLocale($formula) { // Build list of function names and constants for translation @@ -3279,10 +3299,17 @@ class Calculation ); } + /** @var ?array */ private static $functionReplaceFromLocale; + /** @var ?array */ private static $functionReplaceToExcel; + /** + * @param string $formula + * + * @return string + */ public function _translateFormulaToEnglish($formula) { if (self::$functionReplaceFromLocale === null) { @@ -3298,7 +3325,6 @@ class Calculation if (self::$functionReplaceToExcel === null) { self::$functionReplaceToExcel = []; foreach (array_keys(self::$localeFunctions) as $excelFunctionName) { - // @phpstan-ignore-next-line self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2'; } foreach (array_keys(self::$localeBoolean) as $excelBoolean) { @@ -3309,6 +3335,11 @@ class Calculation return self::translateFormula(self::$functionReplaceFromLocale, self::$functionReplaceToExcel, $formula, self::$localeArgumentSeparator, ','); } + /** + * @param string $function + * + * @return string + */ public static function localeFunc($function) { if (self::$localeLanguage !== 'en_us') { @@ -3937,9 +3968,13 @@ class Calculation return $formula; } - // Binary Operators - // These operators always work on two values - // Array key is the operator, the value indicates whether this is a left or right associative operator + /** + * Binary Operators. + * These operators always work on two values. + * Array key is the operator, the value indicates whether this is a left or right associative operator. + * + * @var array + */ private static $operatorAssociativity = [ '^' => 0, // Exponentiation '*' => 0, '/' => 0, // Multiplication and Division @@ -3949,13 +3984,21 @@ class Calculation '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison ]; - // Comparison (Boolean) Operators - // These operators work on two values, but always return a boolean result + /** + * Comparison (Boolean) Operators. + * These operators work on two values, but always return a boolean result. + * + * @var array + */ private static $comparisonOperators = ['>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true]; - // Operator Precedence - // This list includes all valid operators, whether binary (including boolean) or unary (such as %) - // Array key is the operator, the value is its precedence + /** + * Operator Precedence. + * This list includes all valid operators, whether binary (including boolean) or unary (such as %). + * Array key is the operator, the value is its precedence. + * + * @var array + */ private static $operatorPrecedence = [ ':' => 9, // Range '∩' => 8, // Intersect @@ -4203,7 +4246,7 @@ class Calculation $expectingOperator = false; } $stack->push('Brace', '('); - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) { // Watch for this case-change when modifying to allow cell references in different worksheets... // Should only be applied to the actual cell column, not the worksheet name // If the last entry on the stack was a : operator, then we have a cell range reference @@ -4326,6 +4369,8 @@ class Calculation $val = $rowRangeReference[1]; $length = strlen($rowRangeReference[1]); $stackItemType = 'Row Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); $column = 'A'; if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { $column = $pCellParent->getHighestDataColumn($val); @@ -4338,6 +4383,8 @@ class Calculation $val = $columnRangeReference[1]; $length = strlen($val); $stackItemType = 'Column Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); $row = '1'; if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { $row = $pCellParent->getHighestDataRow($val); @@ -4437,6 +4484,11 @@ class Calculation return $output; } + /** + * @param array $operandData + * + * @return mixed + */ private static function dataTestReference(&$operandData) { $operand = $operandData['value']; @@ -4707,11 +4759,19 @@ class Calculation // Perform the required operation against the operand 1 matrix, passing in operand 2 $matrixResult = $matrix->concat($operand2); $result = $matrixResult->getArray(); + if (isset($result[0][0])) { + $result[0][0] = Shared\StringHelper::substring($result[0][0], 0, DataType::MAX_STRING_LENGTH); + } } catch (\Exception $ex) { $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); $result = '#VALUE!'; } } else { + // In theory, we should truncate here. + // But I can't figure out a formula + // using the concatenation operator + // with literals that fits in 32K, + // so I don't think we can overflow here. $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; } $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); @@ -4744,9 +4804,8 @@ class Calculation break; } - - // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on } elseif (($token === '~') || ($token === '%')) { + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on if (($arg = $stack->pop()) === null) { return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } @@ -4782,7 +4841,7 @@ class Calculation if (isset($matches[8])) { if ($cell === null) { - // We can't access the range, so return a REF error + // We can't access the range, so return a REF error $cellValue = Information\ExcelError::REF(); } else { $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10]; @@ -4830,7 +4889,7 @@ class Calculation $cell->attach($pCellParent); } else { $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef; - $cellValue = null; + $cellValue = ($cellSheet !== null) ? null : Information\ExcelError::REF(); } } else { return $this->raiseFormulaError('Unable to access Cell Reference'); @@ -4853,10 +4912,9 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $cellValue; } - - // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { - if ($pCellParent) { + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on + if ($cell !== null && $pCellParent !== null) { $cell->attach($pCellParent); } @@ -4965,8 +5023,8 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $token; } - // if the token is a named range or formula, evaluate it and push the result onto the stack } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) { + // if the token is a named range or formula, evaluate it and push the result onto the stack $definedName = $matches[6]; if ($cell === null || $pCellWorksheet === null) { return $this->raiseFormulaError("undefined name '$token'"); @@ -4997,6 +5055,12 @@ class Calculation return $output; } + /** + * @param mixed $operand + * @param mixed $stack + * + * @return bool + */ private function validateBinaryOperand(&$operand, &$stack) { if (is_array($operand)) { @@ -5208,14 +5272,11 @@ class Calculation { $this->formulaError = $errorMessage; $this->cyclicReferenceStack->clear(); - if (!$this->suppressFormulaErrors) { + $suppress = $this->suppressFormulaErrors ?? $this->suppressFormulaErrorsNew; + if (!$suppress) { throw new Exception($errorMessage); } - if (strlen($errorMessage) > 0) { - trigger_error($errorMessage, E_USER_ERROR); - } - return false; } @@ -5350,7 +5411,7 @@ class Calculation /** * Get a list of all implemented functions as an array of function objects. */ - public function getFunctions(): array + public static function getFunctions(): array { return self::$phpSpreadsheetFunctions; } @@ -5451,6 +5512,11 @@ class Calculation return $args; } + /** + * @param array $tokens + * + * @return string + */ private function getTokensAsString($tokens) { $tokensStr = array_map(function ($token) { @@ -5517,4 +5583,14 @@ class Calculation return $result; } + + public function setSuppressFormulaErrors(bool $suppressFormulaErrors): void + { + $this->suppressFormulaErrorsNew = $suppressFormulaErrors; + } + + public function getSuppressFormulaErrors(): bool + { + return $this->suppressFormulaErrorsNew; + } } diff --git a/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/src/PhpSpreadsheet/Calculation/Engine/Logger.php index c6ee5969..256c3eff 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/Logger.php +++ b/src/PhpSpreadsheet/Calculation/Engine/Logger.php @@ -98,9 +98,9 @@ class Logger $cellReference = implode(' -> ', $this->cellStack->showStack()); if ($this->echoDebugLog) { echo $cellReference, - ($this->cellStack->count() > 0 ? ' => ' : ''), - $message, - PHP_EOL; + ($this->cellStack->count() > 0 ? ' => ' : ''), + $message, + PHP_EOL; } $this->debugLog[] = $cellReference . ($this->cellStack->count() > 0 ? ' => ' : '') . diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php index 4d4bc07e..6aaf1faa 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php @@ -57,7 +57,7 @@ class Compare * * @param array|float $number the value to test against step * Or can be an array of values - * @param array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. + * @param null|array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. * Or can be an array of values * * @return array|int|string (string in the event of an error) @@ -72,7 +72,7 @@ class Compare try { $number = EngineeringValidations::validateFloat($number); - $step = EngineeringValidations::validateFloat($step); + $step = EngineeringValidations::validateFloat($step ?? 0.0); } catch (Exception $e) { return $e->getMessage(); } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php index 677fb0fb..b7c298db 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php @@ -106,6 +106,7 @@ class ConvertUOM 'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false], + // Magnetism 'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true], 'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true], // Temperature diff --git a/src/PhpSpreadsheet/Calculation/FormulaParser.php b/src/PhpSpreadsheet/Calculation/FormulaParser.php index ddf45b23..f71d96fc 100644 --- a/src/PhpSpreadsheet/Calculation/FormulaParser.php +++ b/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -61,7 +61,7 @@ class FormulaParser /** * Create a new FormulaParser. * - * @param string $formula Formula to parse + * @param ?string $formula Formula to parse */ public function __construct($formula = '') { diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 1cc980b8..172f2022 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -152,7 +152,7 @@ class Functions if ($condition === '') { return '=""'; } - if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) { + if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) { $condition = self::operandSpecialHandling($condition); if (is_bool($condition)) { return '=' . ($condition ? 'TRUE' : 'FALSE'); @@ -573,24 +573,20 @@ class Functions return (array) $array; } - $arrayValues = []; - foreach ($array as $value) { + $flattened = []; + $stack = array_values($array); + + while ($stack) { + $value = array_shift($stack); + if (is_array($value)) { - foreach ($value as $val) { - if (is_array($val)) { - foreach ($val as $v) { - $arrayValues[] = $v; - } - } else { - $arrayValues[] = $val; - } - } + array_unshift($stack, ...array_values($value)); } else { - $arrayValues[] = $value; + $flattened[] = $value; } } - return $arrayValues; + return $flattened; } /** @@ -691,7 +687,7 @@ class Functions $worksheet2 = $defined->getWorkSheet(); if (!$defined->isFormula() && $worksheet2 !== null) { $coordinate = "'" . $worksheet2->getTitle() . "'!" . - (string) preg_replace('/^=/', '', $defined->getValue()); + (string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue())); } } diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php index cffad6a6..4b9f818f 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php +++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -47,7 +47,7 @@ class ErrorValue return false; } - return in_array($value, ExcelError::$errorCodes) || $value === ExcelError::CALC(); + return in_array($value, ExcelError::ERROR_CODES, true); } /** diff --git a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php index 585dfdc8..06f38663 100644 --- a/src/PhpSpreadsheet/Calculation/Information/ExcelError.php +++ b/src/PhpSpreadsheet/Calculation/Information/ExcelError.php @@ -11,20 +11,44 @@ class ExcelError /** * List of error codes. * - * @var array + * @var array */ - public static $errorCodes = [ - 'null' => '#NULL!', - 'divisionbyzero' => '#DIV/0!', - 'value' => '#VALUE!', - 'reference' => '#REF!', - 'name' => '#NAME?', - 'num' => '#NUM!', - 'na' => '#N/A', - 'gettingdata' => '#GETTING_DATA', - 'spill' => '#SPILL!', + public const ERROR_CODES = [ + 'null' => '#NULL!', // 1 + 'divisionbyzero' => '#DIV/0!', // 2 + 'value' => '#VALUE!', // 3 + 'reference' => '#REF!', // 4 + 'name' => '#NAME?', // 5 + 'num' => '#NUM!', // 6 + 'na' => '#N/A', // 7 + 'gettingdata' => '#GETTING_DATA', // 8 + 'spill' => '#SPILL!', // 9 + 'connect' => '#CONNECT!', //10 + 'blocked' => '#BLOCKED!', //11 + 'unknown' => '#UNKNOWN!', //12 + 'field' => '#FIELD!', //13 + 'calculation' => '#CALC!', //14 ]; + /** + * List of error codes. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use ERROR_CODES constant instead. + * + * @var array + */ + public static $errorCodes = self::ERROR_CODES; + + /** + * @param mixed $value + */ + public static function throwError($value): string + { + return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value']; + } + /** * ERROR_TYPE. * @@ -39,17 +63,13 @@ class ExcelError } $i = 1; - foreach (self::$errorCodes as $errorCode) { + foreach (self::ERROR_CODES as $errorCode) { if ($value === $errorCode) { return $i; } ++$i; } - if ($value === self::CALC()) { - return 14; - } - return self::NA(); } @@ -60,9 +80,9 @@ class ExcelError * * @return string #NULL! */ - public static function null() + public static function null(): string { - return self::$errorCodes['null']; + return self::ERROR_CODES['null']; } /** @@ -72,9 +92,9 @@ class ExcelError * * @return string #NUM! */ - public static function NAN() + public static function NAN(): string { - return self::$errorCodes['num']; + return self::ERROR_CODES['num']; } /** @@ -84,9 +104,9 @@ class ExcelError * * @return string #REF! */ - public static function REF() + public static function REF(): string { - return self::$errorCodes['reference']; + return self::ERROR_CODES['reference']; } /** @@ -100,9 +120,9 @@ class ExcelError * * @return string #N/A! */ - public static function NA() + public static function NA(): string { - return self::$errorCodes['na']; + return self::ERROR_CODES['na']; } /** @@ -112,9 +132,9 @@ class ExcelError * * @return string #VALUE! */ - public static function VALUE() + public static function VALUE(): string { - return self::$errorCodes['value']; + return self::ERROR_CODES['value']; } /** @@ -124,9 +144,9 @@ class ExcelError * * @return string #NAME? */ - public static function NAME() + public static function NAME(): string { - return self::$errorCodes['name']; + return self::ERROR_CODES['name']; } /** @@ -134,18 +154,18 @@ class ExcelError * * @return string #DIV/0! */ - public static function DIV0() + public static function DIV0(): string { - return self::$errorCodes['divisionbyzero']; + return self::ERROR_CODES['divisionbyzero']; } /** * CALC. * - * @return string #Not Yet Implemented + * @return string #CALC! */ - public static function CALC() + public static function CALC(): string { - return '#CALC!'; + return self::ERROR_CODES['calculation']; } } diff --git a/src/PhpSpreadsheet/Calculation/Information/Value.php b/src/PhpSpreadsheet/Calculation/Information/Value.php index 0ac6b669..2e524db5 100644 --- a/src/PhpSpreadsheet/Calculation/Information/Value.php +++ b/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -240,7 +240,7 @@ class Value * * @param null|mixed $value The value you want converted * - * @return number N converts values listed in the following table + * @return number|string N converts values listed in the following table * If value is or refers to N returns * A number That number value * A date The Excel serialized number of that date diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index c8cdf2dd..0d2db8b2 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; class Address @@ -72,6 +73,9 @@ class Address $sheetName = self::sheetName($sheetName); + if (is_int($referenceStyle)) { + $referenceStyle = (bool) $referenceStyle; + } if ((!is_bool($referenceStyle)) || $referenceStyle === self::REFERENCE_STYLE_A1) { return self::formatAsA1($row, $column, $relativity, $sheetName); } @@ -113,7 +117,8 @@ class Address if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { $row = "[{$row}]"; } + [$rowChar, $colChar] = AddressHelper::getRowAndColumnChars(); - return "{$sheetName}R{$row}C{$column}"; + return "{$sheetName}$rowChar{$row}$colChar{$column}"; } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index c7e87315..e2d27bde 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -66,14 +66,14 @@ class HLookup extends LookupBase */ private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray[$column] as $rowKey => $rowData) { // break if we have passed possible keys $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData); - $cellDataLower = StringHelper::strToLower($rowData); + $cellDataLower = StringHelper::strToLower((string) $rowData); if ( $notExactMatch && diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 7408a66e..76a194b3 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -13,12 +13,12 @@ class Helpers public const CELLADDRESS_USE_R1C1 = false; - private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1): string + private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1, ?int $baseRow = null, ?int $baseCol = null): string { if ($a1 === self::CELLADDRESS_USE_R1C1) { - $cellAddress1 = AddressHelper::convertToA1($cellAddress1); + $cellAddress1 = AddressHelper::convertToA1($cellAddress1, $baseRow ?? 1, $baseCol ?? 1); if ($cellAddress2) { - $cellAddress2 = AddressHelper::convertToA1($cellAddress2); + $cellAddress2 = AddressHelper::convertToA1($cellAddress2, $baseRow ?? 1, $baseCol ?? 1); } } @@ -35,7 +35,7 @@ class Helpers } } - public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = ''): array + public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = '', ?int $baseRow = null, ?int $baseCol = null): array { $cellAddress1 = $cellAddress; $cellAddress2 = null; @@ -52,7 +52,7 @@ class Helpers if (strpos($cellAddress, ':') !== false) { [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); } - $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol); return [$cellAddress1, $cellAddress2, $cellAddress]; } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index 417a1f79..91a14491 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Indirect @@ -63,6 +64,8 @@ class Indirect */ public static function INDIRECT($cellAddress, $a1fmt, Cell $cell) { + [$baseCol, $baseRow] = Coordinate::indexesFromString($cell->getCoordinate()); + try { $a1 = self::a1Format($a1fmt); $cellAddress = self::validateAddress($cellAddress); @@ -78,7 +81,11 @@ class Indirect $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); } - [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); + try { + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol); + } catch (Exception $e) { + return ExcelError::REF(); + } if ( (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 8e451fe4..a001540c 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -19,8 +19,16 @@ abstract class LookupBase protected static function validateIndexLookup(array $lookup_array, $index_number): int { - // index_number must be a number greater than or equal to 1 - if (!is_numeric($index_number) || $index_number < 1) { + // index_number must be a number greater than or equal to 1. + // Excel results are inconsistent when index is non-numeric. + // VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but + // VLOOKUP(whatever, whatever, cellref) yields REF error + // when cellref is '=SQRT(-1)'. So just try our best here. + // Similar results if string (literal yields VALUE, cellRef REF). + if (!is_numeric($index_number)) { + throw new Exception(ExcelError::throwError($index_number)); + } + if ($index_number < 1) { throw new Exception(ExcelError::VALUE()); } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index a447e203..d7d15d4e 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -76,13 +76,14 @@ class Matrix * If an array of values is passed as the $rowNum and/or $columnNum arguments, then the returned result * will also be an array with the same dimensions */ - public static function index($matrix, $rowNum = 0, $columnNum = 0) + public static function index($matrix, $rowNum = 0, $columnNum = null) { if (is_array($rowNum) || is_array($columnNum)) { return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $matrix, $rowNum, $columnNum); } $rowNum = $rowNum ?? 0; + $originalColumnNum = $columnNum; $columnNum = $columnNum ?? 0; try { @@ -102,6 +103,9 @@ class Matrix if ($columnNum > count($columnKeys)) { return ExcelError::REF(); } + if ($originalColumnNum === null && 1 < count($columnKeys)) { + return ExcelError::REF(); + } if ($columnNum === 0) { return self::extractRowValue($matrix, $rowKeys, $rowNum); diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index 9f3377f6..02a25581 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -99,6 +99,8 @@ class Offset private static function extractWorksheet($cellAddress, Cell $cell): array { + $cellAddress = self::assessCellAddress($cellAddress, $cell); + $sheetName = ''; if (strpos($cellAddress, '!') !== false) { [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); @@ -112,6 +114,15 @@ class Offset return [$cellAddress, $worksheet]; } + private static function assessCellAddress(string $cellAddress, Cell $cell): string + { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $cellAddress) !== false) { + $cellAddress = Functions::expandDefinedName($cellAddress, $cell); + } + + return $cellAddress; + } + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns) { $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index 52d5a191..edeb1aa8 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -68,8 +68,8 @@ class VLookup extends LookupBase { reset($a); $firstColumn = key($a); - $aLower = StringHelper::strToLower($a[$firstColumn]); - $bLower = StringHelper::strToLower($b[$firstColumn]); + $aLower = StringHelper::strToLower((string) $a[$firstColumn]); + $bLower = StringHelper::strToLower((string) $b[$firstColumn]); if ($aLower == $bLower) { return 0; @@ -84,13 +84,13 @@ class VLookup extends LookupBase */ private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray as $rowKey => $rowData) { $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData[$column]); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData[$column]); - $cellDataLower = StringHelper::strToLower($rowData[$column]); + $cellDataLower = StringHelper::strToLower((string) $rowData[$column]); // break if we have passed possible keys if ( diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php index 348aa5b3..f34f159b 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -38,7 +38,7 @@ class Helpers return 0 + $number; } - throw new Exception(ExcelError::VALUE()); + throw new Exception(ExcelError::throwError($number)); } /** @@ -59,7 +59,7 @@ class Helpers return 0 + $number; } - throw new Exception(ExcelError::VALUE()); + throw new Exception(ExcelError::throwError($number)); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php index f26da389..06258451 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -118,7 +118,7 @@ class Operations if (is_numeric($arg)) { $returnValue *= $arg; } else { - return ExcelError::VALUE(); + return ExcelError::throwError($arg); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php index b9fcfc73..22cad2cf 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -17,7 +17,7 @@ class Random */ public static function rand() { - return (mt_rand(0, 10000000)) / 10000000; + return mt_rand(0, 10000000) / 10000000; } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php b/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php index 336bc690..6d8f4723 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php @@ -18,7 +18,11 @@ class Subtotal return array_filter( $args, function ($index) use ($cellReference) { - [, $row, ] = explode('.', $index); + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + if (!is_numeric($row)) { + return true; + } return $cellReference->getWorksheet()->getRowDimension($row)->getVisible(); }, @@ -35,7 +39,9 @@ class Subtotal return array_filter( $args, function ($index) use ($cellReference) { - [, $row, $column] = explode('.', $index); + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + $column = $explodeArray[2] ?? ''; $retVal = true; if ($cellReference->getWorksheet()->cellExists($column . $row)) { //take this cell out if it contains the SUBTOTAL or AGGREGATE functions in a formula @@ -87,7 +93,22 @@ class Subtotal public static function evaluate($functionType, ...$args) { $cellReference = array_pop($args); - $aArgs = Functions::flattenArrayIndexed($args); + $bArgs = Functions::flattenArrayIndexed($args); + $aArgs = []; + // int keys must come before string keys for PHP 8.0+ + // Otherwise, PHP thinks positional args follow keyword + // in the subsequent call to call_user_func_array. + // Fortunately, order of args is unimportant to Subtotal. + foreach ($bArgs as $key => $value) { + if (is_int($key)) { + $aArgs[$key] = $value; + } + } + foreach ($bArgs as $key => $value) { + if (!is_int($key)) { + $aArgs[$key] = $value; + } + } try { $subtotal = (int) Helpers::validateNumericNullBool($functionType); diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php index 41b011a5..85195c88 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php @@ -203,7 +203,7 @@ class Averages extends AggregateBase $args, function ($value) { // Is it a numeric value? - return (is_numeric($value)) && (!is_string($value)); + return is_numeric($value) && (!is_string($value)); } ); } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 8574d58d..c8743364 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -101,7 +101,7 @@ class ChiSquared return 1 - self::distributionRightTail($value, $degrees); } - return (($value ** (($degrees / 2) - 1) * exp(-$value / 2))) / + return ($value ** (($degrees / 2) - 1) * exp(-$value / 2)) / ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2)); } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php index 4413b4a4..7bd60e90 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Concatenate { @@ -23,7 +26,18 @@ class Concatenate $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value)) { + $returnValue = $value; + + break; + } $returnValue .= Helpers::extractString($arg); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + + break; + } } return $returnValue; @@ -56,7 +70,14 @@ class Concatenate // Loop through arguments $aArgs = Functions::flattenArray($args); + $returnValue = ''; foreach ($aArgs as $key => &$arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value)) { + $returnValue = $value; + + break; + } if ($ignoreEmpty === true && is_string($arg) && trim($arg) === '') { unset($aArgs[$key]); } elseif (is_bool($arg)) { @@ -64,7 +85,12 @@ class Concatenate } } - return implode($delimiter, $aArgs); + $returnValue = ($returnValue !== '') ? $returnValue : implode($delimiter, $aArgs); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + } + + return $returnValue; } /** @@ -90,9 +116,16 @@ class Concatenate $stringValue = Helpers::extractString($stringValue); if (!is_numeric($repeatCount) || $repeatCount < 0) { - return ExcelError::VALUE(); + $returnValue = ExcelError::VALUE(); + } elseif (ErrorValue::isError($stringValue)) { + $returnValue = $stringValue; + } else { + $returnValue = str_repeat($stringValue, (int) $repeatCount); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); // note VALUE not CALC + } } - return str_repeat($stringValue, (int) $repeatCount); + return $returnValue; } } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index d29f80ca..ee7e31b7 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -4,6 +4,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Extract { @@ -95,4 +98,183 @@ class Extract return mb_substr($value ?? '', mb_strlen($value ?? '', 'UTF-8') - $chars, $chars, 'UTF-8'); } + + /** + * TEXTBEFORE. + * + * @param mixed $text the text that you're searching + * Or can be an array of values + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param mixed $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function before($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A') + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + $text = Helpers::extractString($text ?? ''); + $instance = (int) $instance; + $matchMode = (int) $matchMode; + $matchEnd = (int) $matchEnd; + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_array($split) === false) { + return $split; + } + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { + return ($instance > 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, 0, max(count($split) - (abs($instance) * 2 - 1) - $adjust - $oddReverseAdjustment, 0)) + : array_slice($split, 0, $instance * 2 - 1 - $adjust); + + return implode('', $split); + } + + /** + * TEXTAFTER. + * + * @param mixed $text the text that you're searching + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param mixed $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function after($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A') + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + $text = Helpers::extractString($text ?? ''); + $instance = (int) $instance; + $matchMode = (int) $matchMode; + $matchEnd = (int) $matchEnd; + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_array($split) === false) { + return $split; + } + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { + return ($instance < 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, count($split) - (abs($instance + 1) * 2) - $adjust - $oddReverseAdjustment) + : array_slice($split, $instance * 2 - $adjust); + + return implode('', $split); + } + + /** + * @param null|array|string $delimiter + * @param int $matchMode + * @param int $matchEnd + * @param mixed $ifNotFound + * + * @return string|string[] + */ + private static function validateTextBeforeAfter(string $text, $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) + { + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + + if (preg_match('/' . $delimiter . "/{$flags}", $text) === 0 && $matchEnd === 0) { + return $ifNotFound; + } + + $split = preg_split('/' . $delimiter . "/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + if ($split === false) { + return ExcelError::NA(); + } + + if ($instance === 0 || abs($instance) > StringHelper::countCharacters($text)) { + return ExcelError::VALUE(); + } + + if ($matchEnd === 0 && (abs($instance) > floor(count($split) / 2))) { + return ExcelError::NA(); + } elseif ($matchEnd !== 0 && (abs($instance) - 1 > ceil(count($split) / 2))) { + return ExcelError::NA(); + } + + return $split; + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + if (is_array($delimiter)) { + $delimiter = Functions::flattenArray($delimiter); + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $delimiter + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote($delimiter ?? '') . ')'; + } + + private static function matchFlags(int $matchMode): string + { + return ($matchMode === 0) ? 'mu' : 'miu'; + } } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index bec11496..03e75d1d 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -4,11 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; @@ -208,6 +210,38 @@ class Format return (float) $value; } + /** + * TEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $format + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function valueToText($value, $format = false) + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + $format = (bool) $format; + + if (is_object($value) && $value instanceof RichText) { + $value = $value->getPlainText(); + } + if (is_string($value)) { + $value = ($format === true) ? Calculation::wrapResult($value) : $value; + $value = str_replace("\n", '', $value); + } elseif (is_bool($value)) { + $value = Calculation::$localeBoolean[$value === true ? 'TRUE' : 'FALSE']; + } + + return (string) $value; + } + /** * @param mixed $decimalSeparator */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php index 0fdf6af8..e7b67a34 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Helpers @@ -21,11 +22,14 @@ class Helpers /** * @param mixed $value String value from which to extract characters */ - public static function extractString($value): string + public static function extractString($value, bool $throwIfError = false): string { if (is_bool($value)) { return self::convertBooleanValue($value); } + if ($throwIfError && is_string($value) && ErrorValue::isError($value)) { + throw new CalcExp($value); + } return (string) $value; } diff --git a/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/src/PhpSpreadsheet/Calculation/TextData/Replace.php index a07ea104..03b66321 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Replace.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Replace { @@ -36,16 +38,20 @@ class Replace try { $start = Helpers::extractInt($start, 1, 0, true); $chars = Helpers::extractInt($chars, 0, 0, true); - $oldText = Helpers::extractString($oldText); - $newText = Helpers::extractString($newText); - $left = mb_substr($oldText, 0, $start - 1, 'UTF-8'); + $oldText = Helpers::extractString($oldText, true); + $newText = Helpers::extractString($newText, true); + $left = StringHelper::substring($oldText, 0, $start - 1); - $right = mb_substr($oldText, $start + $chars - 1, null, 'UTF-8'); + $right = StringHelper::substring($oldText, $start + $chars - 1, null); } catch (CalcExp $e) { return $e->getMessage(); } + $returnValue = $left . $newText . $right; + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } - return $left . $newText . $right; + return $returnValue; } /** @@ -71,24 +77,29 @@ class Replace } try { - $text = Helpers::extractString($text); - $fromText = Helpers::extractString($fromText); - $toText = Helpers::extractString($toText); + $text = Helpers::extractString($text, true); + $fromText = Helpers::extractString($fromText, true); + $toText = Helpers::extractString($toText, true); if ($instance === null) { - return str_replace($fromText, $toText, $text); - } - if (is_bool($instance)) { - if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { - return ExcelError::Value(); + $returnValue = str_replace($fromText, $toText, $text); + } else { + if (is_bool($instance)) { + if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { + return ExcelError::Value(); + } + $instance = 1; } - $instance = 1; + $instance = Helpers::extractInt($instance, 1, 0, true); + $returnValue = self::executeSubstitution($text, $fromText, $toText, $instance); } - $instance = Helpers::extractInt($instance, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); } + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } - return self::executeSubstitution($text, $fromText, $toText, $instance); + return $returnValue; } /** @@ -106,7 +117,7 @@ class Replace } if ($pos !== false) { - return Functions::scalar(self::REPLACE($text, ++$pos, mb_strlen($fromText, 'UTF-8'), $toText)); + return Functions::scalar(self::REPLACE($text, ++$pos, StringHelper::countCharacters($fromText), $toText)); } return $text; diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 490c43c2..83810422 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -3,6 +3,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Text { @@ -77,4 +79,176 @@ class Text return null; } + + /** + * TEXTSPLIT. + * + * @param mixed $text the text that you're searching + * @param null|array|string $columnDelimiter The text that marks the point where to spill the text across columns. + * Multiple delimiters can be passed as an array of string values + * @param null|array|string $rowDelimiter The text that marks the point where to spill the text down rows. + * Multiple delimiters can be passed as an array of string values + * @param bool $ignoreEmpty Specify FALSE to create an empty cell when two delimiters are consecutive. + * true = create empty cells + * false = skip empty cells + * Defaults to TRUE, which creates an empty cell + * @param bool $matchMode Determines whether the match is case-sensitive or not. + * true = case-sensitive + * false = case-insensitive + * By default, a case-sensitive match is done. + * @param mixed $padding The value with which to pad the result. + * The default is #N/A. + * + * @return array the array built from the text, split by the row and column delimiters + */ + public static function split($text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, $padding = '#N/A') + { + $text = Functions::flattenSingleValue($text); + + $flags = self::matchFlags($matchMode); + + if ($rowDelimiter !== null) { + $delimiter = self::buildDelimiter($rowDelimiter); + $rows = ($delimiter === '()') + ? [$text] + : preg_split("/{$delimiter}/{$flags}", $text); + } else { + $rows = [$text]; + } + + /** @var array $rows */ + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + function ($row) { + return $row !== ''; + } + )); + } + + if ($columnDelimiter !== null) { + $delimiter = self::buildDelimiter($columnDelimiter); + array_walk( + $rows, + function (&$row) use ($delimiter, $flags, $ignoreEmpty): void { + $row = ($delimiter === '()') + ? [$row] + : preg_split("/{$delimiter}/{$flags}", $row); + /** @var array $row */ + if ($ignoreEmpty === true) { + $row = array_values(array_filter( + $row, + function ($value) { + return $value !== ''; + } + )); + } + } + ); + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + function ($row) { + return $row !== [] && $row !== ['']; + } + )); + } + } + + return self::applyPadding($rows, $padding); + } + + /** + * @param mixed $padding + */ + private static function applyPadding(array $rows, $padding): array + { + $columnCount = array_reduce( + $rows, + function (int $counter, array $row): int { + return max($counter, count($row)); + }, + 0 + ); + + return array_map( + function (array $row) use ($columnCount, $padding): array { + return (count($row) < $columnCount) + ? array_merge($row, array_fill(0, $columnCount - count($row), $padding)) + : $row; + }, + $rows + ); + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to split + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + $valueSet = Functions::flattenArray($delimiter); + + if (is_array($delimiter) && count($valueSet) > 1) { + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $valueSet + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote(Functions::flattenSingleValue($delimiter)) . ')'; + } + + private static function matchFlags(bool $matchMode): string + { + return ($matchMode === true) ? 'miu' : 'mu'; + } + + public static function fromArray(array $array, int $format = 0): string + { + $result = []; + foreach ($array as $row) { + $cells = []; + foreach ($row as $cellValue) { + $value = ($format === 1) ? self::formatValueMode1($cellValue) : self::formatValueMode0($cellValue); + $cells[] = $value; + } + $result[] = implode(($format === 1) ? ',' : ', ', $cells); + } + + $result = implode(($format === 1) ? ';' : ', ', $result); + + return ($format === 1) ? '{' . $result . '}' : $result; + } + + /** + * @param mixed $cellValue + */ + private static function formatValueMode0($cellValue): string + { + if (is_bool($cellValue)) { + return ($cellValue) ? Calculation::$localeBoolean['TRUE'] : Calculation::$localeBoolean['FALSE']; + } + + return (string) $cellValue; + } + + /** + * @param mixed $cellValue + */ + private static function formatValueMode1($cellValue): string + { + if (is_string($cellValue) && Functions::isError($cellValue) === false) { + return Calculation::FORMULA_STRING_QUOTE . $cellValue . Calculation::FORMULA_STRING_QUOTE; + } elseif (is_bool($cellValue)) { + return ($cellValue) ? Calculation::$localeBoolean['TRUE'] : Calculation::$localeBoolean['FALSE']; + } + + return (string) $cellValue; + } } diff --git a/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx index d013aee0..7b9fb0dd 100644 Binary files a/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx and b/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx differ diff --git a/src/PhpSpreadsheet/Calculation/locale/da/functions b/src/PhpSpreadsheet/Calculation/locale/da/functions index 6260760b..03b68c9b 100644 --- a/src/PhpSpreadsheet/Calculation/locale/da/functions +++ b/src/PhpSpreadsheet/Calculation/locale/da/functions @@ -245,6 +245,7 @@ ROWS = RÆKKER RTD = RTD TRANSPOSE = TRANSPONER VLOOKUP = LOPSLAG +*RC = RK ## ## Matematiske og trigonometriske funktioner (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/de/functions b/src/PhpSpreadsheet/Calculation/locale/de/functions index 331232f7..d49fc5f1 100644 --- a/src/PhpSpreadsheet/Calculation/locale/de/functions +++ b/src/PhpSpreadsheet/Calculation/locale/de/functions @@ -243,6 +243,7 @@ ROWS = ZEILEN RTD = RTD TRANSPOSE = MTRANS VLOOKUP = SVERWEIS +*RC = ZS ## ## Mathematische und trigonometrische Funktionen (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/es/functions b/src/PhpSpreadsheet/Calculation/locale/es/functions index 1f9f2891..88012aa1 100644 --- a/src/PhpSpreadsheet/Calculation/locale/es/functions +++ b/src/PhpSpreadsheet/Calculation/locale/es/functions @@ -245,6 +245,7 @@ ROWS = FILAS RTD = RDTR TRANSPOSE = TRANSPONER VLOOKUP = BUSCARV +*RC = FC ## ## Funciones matemáticas y trigonométricas (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/fi/functions b/src/PhpSpreadsheet/Calculation/locale/fi/functions index 33068d93..18f7c8c8 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fi/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fi/functions @@ -245,6 +245,7 @@ ROWS = RIVIT RTD = RTD TRANSPOSE = TRANSPONOI VLOOKUP = PHAKU +*RC = RS ## ## Matemaattiset ja trigonometriset funktiot (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/fr/functions b/src/PhpSpreadsheet/Calculation/locale/fr/functions index 78b603e9..621cb0db 100644 --- a/src/PhpSpreadsheet/Calculation/locale/fr/functions +++ b/src/PhpSpreadsheet/Calculation/locale/fr/functions @@ -240,6 +240,7 @@ ROWS = LIGNES RTD = RTD TRANSPOSE = TRANSPOSE VLOOKUP = RECHERCHEV +*RC = LC ## ## Fonctions mathématiques et trigonométriques (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/hu/functions b/src/PhpSpreadsheet/Calculation/locale/hu/functions index 46b30127..4a375ea2 100644 --- a/src/PhpSpreadsheet/Calculation/locale/hu/functions +++ b/src/PhpSpreadsheet/Calculation/locale/hu/functions @@ -245,6 +245,7 @@ ROWS = SOROK RTD = VIA TRANSPOSE = TRANSZPONÁLÁS VLOOKUP = FKERES +*RC = SO ## ## Matematikai és trigonometrikus függvények (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/nb/functions b/src/PhpSpreadsheet/Calculation/locale/nb/functions index b0a0f949..d352e1f4 100644 --- a/src/PhpSpreadsheet/Calculation/locale/nb/functions +++ b/src/PhpSpreadsheet/Calculation/locale/nb/functions @@ -245,6 +245,7 @@ ROWS = RADER RTD = RTD TRANSPOSE = TRANSPONER VLOOKUP = FINN.RAD +*RC = RK ## ## Matematikk- og trigonometrifunksjoner (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/nl/functions b/src/PhpSpreadsheet/Calculation/locale/nl/functions index 0e4f1597..ce0b30cc 100644 --- a/src/PhpSpreadsheet/Calculation/locale/nl/functions +++ b/src/PhpSpreadsheet/Calculation/locale/nl/functions @@ -244,6 +244,7 @@ ROWS = RIJEN RTD = RTG TRANSPOSE = TRANSPONEREN VLOOKUP = VERT.ZOEKEN +*RC = RK ## ## Wiskundige en trigonometrische functies (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/pt/br/functions b/src/PhpSpreadsheet/Calculation/locale/pt/br/functions index feba30d9..5781b0c7 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pt/br/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pt/br/functions @@ -242,6 +242,7 @@ ROWS = LINS RTD = RTD TRANSPOSE = TRANSPOR VLOOKUP = PROCV +*RC = LC ## ## Funções matemáticas e trigonométricas (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/pt/functions b/src/PhpSpreadsheet/Calculation/locale/pt/functions index 8a94d826..70a3bb0c 100644 --- a/src/PhpSpreadsheet/Calculation/locale/pt/functions +++ b/src/PhpSpreadsheet/Calculation/locale/pt/functions @@ -245,6 +245,7 @@ ROWS = LINS RTD = RTD TRANSPOSE = TRANSPOR VLOOKUP = PROCV +*RC = LC ## ## Funções matemáticas e trigonométricas (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Calculation/locale/sv/functions b/src/PhpSpreadsheet/Calculation/locale/sv/functions index 2531b4c1..491ecfb9 100644 --- a/src/PhpSpreadsheet/Calculation/locale/sv/functions +++ b/src/PhpSpreadsheet/Calculation/locale/sv/functions @@ -243,6 +243,7 @@ ROWS = RADER RTD = RTD TRANSPOSE = TRANSPONERA VLOOKUP = LETARAD +*RC = RK ## ## Matematiska och trigonometriska funktioner (Math & Trig Functions) diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index 535bdee0..a23a78b6 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -2,23 +2,44 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; class AddressHelper { public const R1C1_COORDINATE_REGEX = '/(R((?:\[-?\d*\])|(?:\d*))?)(C((?:\[-?\d*\])|(?:\d*))?)/i'; + /** @return string[] */ + public static function getRowAndColumnChars() + { + $rowChar = 'R'; + $colChar = 'C'; + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_EXCEL) { + $rowColChars = Calculation::localeFunc('*RC'); + if (mb_strlen($rowColChars) === 2) { + $rowChar = mb_substr($rowColChars, 0, 1); + $colChar = mb_substr($rowColChars, 1, 1); + } + } + + return [$rowChar, $colChar]; + } + /** * Converts an R1C1 format cell address to an A1 format cell address. */ public static function convertToA1( string $address, int $currentRowNumber = 1, - int $currentColumnNumber = 1 + int $currentColumnNumber = 1, + bool $useLocale = true ): string { - $validityCheck = preg_match('/^(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))$/i', $address, $cellReference); + [$rowChar, $colChar] = $useLocale ? self::getRowAndColumnChars() : ['R', 'C']; + $regex = '/^(' . $rowChar . '(\[?[-+]?\d*\]?))(' . $colChar . '(\[?[-+]?\d*\]?))$/i'; + $validityCheck = preg_match($regex, $address, $cellReference); - if ($validityCheck === 0) { + if (empty($validityCheck)) { throw new Exception('Invalid R1C1-format Cell Reference'); } @@ -92,7 +113,7 @@ class AddressHelper // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, // then modify the formula to use that new reference foreach ($cellReferences as $cellReference) { - $A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber); + $A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber, false); $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); } } diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 2005694d..d4d793d7 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -50,14 +50,14 @@ class Cell private $dataType; /** - * Collection of cells. + * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet). * * @var Cells */ private $parent; /** - * Index to cellXf. + * Index to the cellXf reference for the styling of this cell. * * @var int */ @@ -95,9 +95,8 @@ class Cell * Create a new Cell. * * @param mixed $value - * @param string $dataType */ - public function __construct($value, $dataType, Worksheet $worksheet) + public function __construct($value, ?string $dataType, Worksheet $worksheet) { // Initialise cell value $this->value = $value; @@ -111,7 +110,7 @@ class Cell $dataType = DataType::TYPE_STRING; } $this->dataType = $dataType; - } elseif (!self::getValueBinder()->bindValue($this, $value)) { + } elseif (self::getValueBinder()->bindValue($this, $value) === false) { throw new Exception('Value could not be bound to cell.'); } } @@ -167,10 +166,8 @@ class Cell /** * Get cell value with formatting. - * - * @return string */ - public function getFormattedValue() + public function getFormattedValue(): string { return (string) NumberFormat::toFormattedString( $this->getCalculatedValue(), @@ -188,7 +185,7 @@ class Cell * * @return $this */ - public function setValue($value) + public function setValue($value): self { if (!self::getValueBinder()->bindValue($this, $value)) { throw new Exception('Value could not be bound to cell.'); @@ -205,7 +202,7 @@ class Cell * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this * method, then it is your responsibility as an end-user developer to validate that the value and * the datatype match. - * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * If you do mismatch value and datatype, then the value you enter may be changed to match the datatype * that you specify. * * @return Cell @@ -271,7 +268,7 @@ class Cell * * @return mixed */ - public function getCalculatedValue($resetLog = true) + public function getCalculatedValue(bool $resetLog = true) { if ($this->dataType === DataType::TYPE_FORMULA) { try { @@ -316,10 +313,8 @@ class Cell * Set old calculated value (cached). * * @param mixed $originalValue Value - * - * @return Cell */ - public function setCalculatedValue($originalValue) + public function setCalculatedValue($originalValue): self { if ($originalValue !== null) { $this->calculatedValue = (is_numeric($originalValue)) ? (float) $originalValue : $originalValue; @@ -345,10 +340,8 @@ class Cell /** * Get cell data type. - * - * @return string */ - public function getDataType() + public function getDataType(): string { return $this->dataType; } @@ -357,10 +350,8 @@ class Cell * Set cell data type. * * @param string $dataType see DataType::TYPE_* - * - * @return Cell */ - public function setDataType($dataType) + public function setDataType($dataType): self { if ($dataType == DataType::TYPE_STRING2) { $dataType = DataType::TYPE_STRING; @@ -392,10 +383,8 @@ class Cell /** * Get Data validation rules. - * - * @return DataValidation */ - public function getDataValidation() + public function getDataValidation(): DataValidation { if (!isset($this->parent)) { throw new Exception('Cannot get data validation for cell that is not bound to a worksheet'); @@ -420,10 +409,8 @@ class Cell /** * Does this cell contain valid value? - * - * @return bool */ - public function hasValidValue() + public function hasValidValue(): bool { $validator = new DataValidator(); @@ -432,10 +419,8 @@ class Cell /** * Does this cell contain a Hyperlink? - * - * @return bool */ - public function hasHyperlink() + public function hasHyperlink(): bool { if (!isset($this->parent)) { throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet'); @@ -446,10 +431,8 @@ class Cell /** * Get Hyperlink. - * - * @return Hyperlink */ - public function getHyperlink() + public function getHyperlink(): Hyperlink { if (!isset($this->parent)) { throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet'); @@ -460,10 +443,8 @@ class Cell /** * Set Hyperlink. - * - * @return Cell */ - public function setHyperlink(?Hyperlink $hyperlink = null) + public function setHyperlink(?Hyperlink $hyperlink = null): self { if (!isset($this->parent)) { throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet'); @@ -486,10 +467,8 @@ class Cell /** * Get parent worksheet. - * - * @return Worksheet */ - public function getWorksheet() + public function getWorksheet(): Worksheet { try { $worksheet = $this->parent->getParent(); @@ -506,27 +485,22 @@ class Cell /** * Is this cell in a merge range. - * - * @return bool */ - public function isInMergeRange() + public function isInMergeRange(): bool { return (bool) $this->getMergeRange(); } /** * Is this cell the master (top left cell) in a merge range (that holds the actual data value). - * - * @return bool */ - public function isMergeRangeValueCell() + public function isMergeRangeValueCell(): bool { if ($mergeRange = $this->getMergeRange()) { $mergeRange = Coordinate::splitRange($mergeRange); [$startCell] = $mergeRange[0]; - if ($this->getCoordinate() === $startCell) { - return true; - } + + return $this->getCoordinate() === $startCell; } return false; @@ -576,10 +550,8 @@ class Cell /** * Re-bind parent. - * - * @return Cell */ - public function rebindParent(Worksheet $parent) + public function rebindParent(Worksheet $parent): self { $this->parent = $parent->getCellCollection(); @@ -590,10 +562,8 @@ class Cell * Is cell in a specific range? * * @param string $range Cell range (e.g. A1:A1) - * - * @return bool */ - public function isInRange($range) + public function isInRange(string $range): bool { [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range); @@ -614,7 +584,7 @@ class Cell * * @return int Result of comparison (always -1 or 1, never zero!) */ - public static function compareCells(self $a, self $b) + public static function compareCells(self $a, self $b): int { if ($a->getRow() < $b->getRow()) { return -1; @@ -629,10 +599,8 @@ class Cell /** * Get value binder to use. - * - * @return IValueBinder */ - public static function getValueBinder() + public static function getValueBinder(): IValueBinder { if (self::$valueBinder === null) { self::$valueBinder = new DefaultValueBinder(); @@ -655,33 +623,27 @@ class Cell public function __clone() { $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if ((is_object($value)) && ($key != 'parent')) { - $this->$key = clone $value; + foreach ($vars as $propertyName => $propertyValue) { + if ((is_object($propertyValue)) && ($propertyName !== 'parent')) { + $this->$propertyName = clone $propertyValue; } else { - $this->$key = $value; + $this->$propertyName = $propertyValue; } } } /** * Get index to cellXf. - * - * @return int */ - public function getXfIndex() + public function getXfIndex(): int { return $this->xfIndex; } /** * Set index to cellXf. - * - * @param int $indexValue - * - * @return Cell */ - public function setXfIndex($indexValue) + public function setXfIndex(int $indexValue): self { $this->xfIndex = $indexValue; @@ -695,7 +657,7 @@ class Cell * * @return $this */ - public function setFormulaAttributes($attributes) + public function setFormulaAttributes($attributes): self { $this->formulaAttributes = $attributes; diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 9aa879c3..2fca6212 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -183,12 +184,12 @@ abstract class Coordinate /** * Calculate range boundaries. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range coordinates [Start Cell, End Cell] * where Start Cell and End Cell are arrays (Column Number, Row Number) */ - public static function rangeBoundaries($range) + public static function rangeBoundaries(string $range): array { // Ensure $pRange is a valid range if (empty($range)) { @@ -205,6 +206,16 @@ abstract class Coordinate [$rangeA, $rangeB] = explode(':', $range); } + if (is_numeric($rangeA) && is_numeric($rangeB)) { + $rangeA = 'A' . $rangeA; + $rangeB = AddressRange::MAX_COLUMN . $rangeB; + } + + if (ctype_alpha($rangeA) && ctype_alpha($rangeB)) { + $rangeA = $rangeA . '1'; + $rangeB = $rangeB . AddressRange::MAX_ROW; + } + // Calculate range outer borders $rangeStart = self::coordinateFromString($rangeA); $rangeEnd = self::coordinateFromString($rangeB); @@ -219,7 +230,7 @@ abstract class Coordinate /** * Calculate range dimension. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range dimension (width, height) */ @@ -234,29 +245,19 @@ abstract class Coordinate /** * Calculate range boundaries. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range coordinates [Start Cell, End Cell] * where Start Cell and End Cell are arrays [Column ID, Row Number] */ public static function getRangeBoundaries($range) { - // Ensure $pRange is a valid range - if (empty($range)) { - $range = self::DEFAULT_RANGE; - } + [$rangeA, $rangeB] = self::rangeBoundaries($range); - // Uppercase coordinate - $range = strtoupper($range); - - // Extract range - if (strpos($range, ':') === false) { - $rangeA = $rangeB = $range; - } else { - [$rangeA, $rangeB] = explode(':', $range); - } - - return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)]; + return [ + [self::stringFromColumnIndex($rangeA[0]), $rangeA[1]], + [self::stringFromColumnIndex($rangeB[0]), $rangeB[1]], + ]; } /** @@ -349,6 +350,19 @@ abstract class Coordinate */ public static function extractAllCellReferencesInRange($cellRange): array { + if (substr_count($cellRange, '!') > 1) { + throw new Exception('3-D Range References are not supported'); + } + + [$worksheet, $cellRange] = Worksheet::extractSheetTitle($cellRange, true); + $quoted = ''; + if ($worksheet > '') { + $quoted = Worksheet::nameRequiresQuotes($worksheet) ? "'" : ''; + if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") { + $worksheet = substr($worksheet, 1, -1); + } + $worksheet = str_replace("'", "''", $worksheet); + } [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange); $cells = []; @@ -364,7 +378,12 @@ abstract class Coordinate $cellList = array_merge(...$cells); - return self::sortCellReferenceArray($cellList); + return array_map( + function ($cellAddress) use ($worksheet, $quoted) { + return ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress; + }, + self::sortCellReferenceArray($cellList) + ); } private static function processRangeSetOperators(array $operators, array $cells): array diff --git a/src/PhpSpreadsheet/Cell/DataType.php b/src/PhpSpreadsheet/Cell/DataType.php index 390dcde5..f19984db 100644 --- a/src/PhpSpreadsheet/Cell/DataType.php +++ b/src/PhpSpreadsheet/Cell/DataType.php @@ -21,7 +21,7 @@ class DataType /** * List of error codes. * - * @var array + * @var array */ private static $errorCodes = [ '#NULL!' => 0, @@ -31,12 +31,15 @@ class DataType '#NAME?' => 4, '#NUM!' => 5, '#N/A' => 6, + '#CALC!' => 7, ]; + public const MAX_STRING_LENGTH = 32767; + /** * Get list of error codes. * - * @return array + * @return array */ public static function getErrorCodes() { @@ -58,7 +61,7 @@ class DataType } // string must never be longer than 32,767 characters, truncate if necessary - $textValue = StringHelper::substring((string) $textValue, 0, 32767); + $textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH); // we require that newline is represented as "\n" in core, not as "\r\n" or "\r" $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 69607216..9ebb081f 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -10,6 +10,34 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class Axis extends Properties { + const AXIS_TYPE_CATEGORY = 'catAx'; + const AXIS_TYPE_DATE = 'dateAx'; + const AXIS_TYPE_VALUE = 'valAx'; + + const TIME_UNIT_DAYS = 'days'; + const TIME_UNIT_MONTHS = 'months'; + const TIME_UNIT_YEARS = 'years'; + + public function __construct() + { + parent::__construct(); + $this->fillColor = new ChartColor(); + } + + /** + * Chart Major Gridlines as. + * + * @var ?GridLines + */ + private $majorGridlines; + + /** + * Chart Minor Gridlines as. + * + * @var ?GridLines + */ + private $minorGridlines; + /** * Axis Number. * @@ -21,6 +49,9 @@ class Axis extends Properties 'numeric' => null, ]; + /** @var string */ + private $axisType = ''; + /** * Axis Options. * @@ -37,86 +68,24 @@ class Axis extends Properties 'axis_labels' => self::AXIS_LABELS_NEXT_TO, 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, + 'textRotation' => null, + 'hidden' => null, + 'majorTimeUnit' => self::TIME_UNIT_YEARS, + 'minorTimeUnit' => self::TIME_UNIT_MONTHS, + 'baseTimeUnit' => self::TIME_UNIT_DAYS, ]; /** * Fill Properties. * - * @var mixed[] + * @var ChartColor */ - private $fillProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; - - /** - * Line Properties. - * - * @var mixed[] - */ - private $lineProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; - - /** - * Line Style Properties. - * - * @var mixed[] - */ - private $lineStyleProperties = [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ]; - - /** - * Shadow Properties. - * - * @var mixed[] - */ - private $shadowProperties = Properties::PRESETS_OPTIONS[0]; - - /** - * Glow Properties. - * - * @var mixed[] - */ - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - /** - * Soft Edge Properties. - * - * @var mixed[] - */ - private $softEdges = [ - 'size' => null, - ]; + private $fillColor; private const NUMERIC_FORMAT = [ Properties::FORMAT_CODE_NUMBER, Properties::FORMAT_CODE_DATE, + Properties::FORMAT_CODE_DATE_ISO8601, ]; /** @@ -124,11 +93,11 @@ class Axis extends Properties * * @param mixed $format_code */ - public function setAxisNumberProperties($format_code, ?bool $numeric = null): void + public function setAxisNumberProperties($format_code, ?bool $numeric = null, int $sourceLinked = 0): void { $format = (string) $format_code; $this->axisNumber['format'] = $format; - $this->axisNumber['source_linked'] = 0; + $this->axisNumber['source_linked'] = $sourceLinked; if (is_bool($numeric)) { $this->axisNumber['numeric'] = $numeric; } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { @@ -158,36 +127,51 @@ class Axis extends Properties public function getAxisIsNumericFormat(): bool { - return (bool) $this->axisNumber['numeric']; + return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; + } + + public function setAxisOption(string $key, ?string $value): void + { + if ($value !== null && $value !== '') { + $this->axisOptions[$key] = $value; + } } /** * Set Axis Options Properties. - * - * @param string $axisLabels - * @param string $horizontalCrossesValue - * @param string $horizontalCrosses - * @param string $axisOrientation - * @param string $majorTmt - * @param string $minorTmt - * @param string $minimum - * @param string $maximum - * @param string $majorUnit - * @param string $minorUnit */ - public function setAxisOptionsProperties($axisLabels, $horizontalCrossesValue = null, $horizontalCrosses = null, $axisOrientation = null, $majorTmt = null, $minorTmt = null, $minimum = null, $maximum = null, $majorUnit = null, $minorUnit = null): void - { - $this->axisOptions['axis_labels'] = (string) $axisLabels; - ($horizontalCrossesValue !== null) ? $this->axisOptions['horizontal_crosses_value'] = (string) $horizontalCrossesValue : null; - ($horizontalCrosses !== null) ? $this->axisOptions['horizontal_crosses'] = (string) $horizontalCrosses : null; - ($axisOrientation !== null) ? $this->axisOptions['orientation'] = (string) $axisOrientation : null; - ($majorTmt !== null) ? $this->axisOptions['major_tick_mark'] = (string) $majorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minimum !== null) ? $this->axisOptions['minimum'] = (string) $minimum : null; - ($maximum !== null) ? $this->axisOptions['maximum'] = (string) $maximum : null; - ($majorUnit !== null) ? $this->axisOptions['major_unit'] = (string) $majorUnit : null; - ($minorUnit !== null) ? $this->axisOptions['minor_unit'] = (string) $minorUnit : null; + public function setAxisOptionsProperties( + string $axisLabels, + ?string $horizontalCrossesValue = null, + ?string $horizontalCrosses = null, + ?string $axisOrientation = null, + ?string $majorTmt = null, + ?string $minorTmt = null, + ?string $minimum = null, + ?string $maximum = null, + ?string $majorUnit = null, + ?string $minorUnit = null, + ?string $textRotation = null, + ?string $hidden = null, + ?string $baseTimeUnit = null, + ?string $majorTimeUnit = null, + ?string $minorTimeUnit = null + ): void { + $this->axisOptions['axis_labels'] = $axisLabels; + $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); + $this->setAxisOption('horizontal_crosses', $horizontalCrosses); + $this->setAxisOption('orientation', $axisOrientation); + $this->setAxisOption('major_tick_mark', $majorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minimum', $minimum); + $this->setAxisOption('maximum', $maximum); + $this->setAxisOption('major_unit', $majorUnit); + $this->setAxisOption('minor_unit', $minorUnit); + $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); + $this->setAxisOption('baseTimeUnit', $baseTimeUnit); + $this->setAxisOption('majorTimeUnit', $majorTimeUnit); + $this->setAxisOption('minorTimeUnit', $minorTimeUnit); } /** @@ -195,7 +179,7 @@ class Axis extends Properties * * @param string $property * - * @return string + * @return ?string */ public function getAxisOptionsProperty($property) { @@ -212,28 +196,32 @@ class Axis extends Properties $this->axisOptions['orientation'] = (string) $orientation; } - /** - * Set Fill Property. - * - * @param string $color - * @param int $alpha - * @param string $AlphaType - */ - public function setFillParameters($color, $alpha = 0, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function getAxisType(): string { - $this->fillProperties = $this->setColorProperties($color, $alpha, $AlphaType); + return $this->axisType; + } + + public function setAxisType(string $type): self + { + if ($type === self::AXIS_TYPE_CATEGORY || $type === self::AXIS_TYPE_VALUE || $type === self::AXIS_TYPE_DATE) { + $this->axisType = $type; + } else { + $this->axisType = ''; + } + + return $this; } /** - * Set Line Property. + * Set Fill Property. * - * @param string $color - * @param int $alpha - * @param string $alphaType + * @param ?string $color + * @param ?int $alpha + * @param ?string $AlphaType */ - public function setLineParameters($color, $alpha = 0, $alphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function setFillParameters($color, $alpha = null, $AlphaType = ChartColor::EXCEL_COLOR_TYPE_RGB): void { - $this->lineProperties = $this->setColorProperties($color, $alpha, $alphaType); + $this->fillColor->setColorProperties($color, $alpha, $AlphaType); } /** @@ -245,323 +233,67 @@ class Axis extends Properties */ public function getFillProperty($property) { - return $this->fillProperties[$property]; + return (string) $this->fillColor->getColorProperty($property); + } + + public function getFillColorObject(): ChartColor + { + return $this->fillColor; } /** - * Get Line Property. + * Get Line Color Property. * - * @param string $property + * @Deprecated 1.24.0 * - * @return string + * @See Properties::getLineColorProperty() + * Use the getLineColor property in the Properties class instead + * + * @param string $propertyName + * + * @return null|int|string */ - public function getLineProperty($property) + public function getLineProperty($propertyName) { - return $this->lineProperties[$property]; + return $this->getLineColorProperty($propertyName); } - /** - * Set Line Style Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - ($lineWidth !== null) ? $this->lineStyleProperties['width'] = $this->getExcelPointsWidth((float) $lineWidth) : null; - ($compoundType !== null) ? $this->lineStyleProperties['compound'] = (string) $compoundType : null; - ($dashType !== null) ? $this->lineStyleProperties['dash'] = (string) $dashType : null; - ($capType !== null) ? $this->lineStyleProperties['cap'] = (string) $capType : null; - ($joinType !== null) ? $this->lineStyleProperties['join'] = (string) $joinType : null; - ($headArrowType !== null) ? $this->lineStyleProperties['arrow']['head']['type'] = (string) $headArrowType : null; - ($headArrowSize !== null) ? $this->lineStyleProperties['arrow']['head']['size'] = (string) $headArrowSize : null; - ($endArrowType !== null) ? $this->lineStyleProperties['arrow']['end']['type'] = (string) $endArrowType : null; - ($endArrowSize !== null) ? $this->lineStyleProperties['arrow']['end']['size'] = (string) $endArrowSize : null; - } + /** @var string */ + private $crossBetween = ''; // 'between' or 'midCat' might be better - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) + public function setCrossBetween(string $crossBetween): self { - return $this->getArrayElementsValue($this->lineStyleProperties, $elements); - } - - /** - * Get Line Style Arrow Excel Width. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowWidth($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'w'); - } - - /** - * Get Line Style Arrow Excel Length. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowLength($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'len'); - } - - /** - * @param mixed $value - */ - public function setShadowProperty(string $propertyName, $value): self - { - if ($propertyName === 'color' && is_array($value)) { - $this->setShadowColor($value['value'], $value['alpha'], $value['type']); - } else { - $this->shadowProperties[$propertyName] = $value; - } + $this->crossBetween = $crossBetween; return $this; } - /** - * Set Shadow Properties. - * - * @param int $shadowPresets - * @param string $colorValue - * @param string $colorType - * @param null|int|string $colorAlpha - * @param null|float $blur - * @param null|int $angle - * @param null|float $distance - */ - public function setShadowProperties($shadowPresets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + public function getCrossBetween(): string { - $this->setShadowPresetsProperties((int) $shadowPresets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - (int) ($colorAlpha ?? $this->shadowProperties['color']['alpha']), - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); + return $this->crossBetween; } - /** - * Set Shadow Color. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) + public function getMajorGridlines(): ?GridLines { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + return $this->majorGridlines; + } + + public function getMinorGridlines(): ?GridLines + { + return $this->minorGridlines; + } + + public function setMajorGridlines(?GridLines $gridlines): self + { + $this->majorGridlines = $gridlines; return $this; } - private const SHADOW_ARRAY_KEYS = ['size', 'color']; - - /** - * Set Shadow Properties from Mapped Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + public function setMinorGridlines(?GridLines $gridlines): self { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { - $reference = &$this->shadowProperties[$property_key]; - $this->setShadowPropertiesMapValues($property_val, $reference); - } - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } + $this->minorGridlines = $gridlines; return $this; } - - /** - * Set Shadow Color. - * - * @param null|string $color - * @param null|int $alpha - * @param null|string $alphaType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $alphaType) - { - $this->shadowProperties['color'] = $this->setColorProperties($color, $alpha, $alphaType); - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param null|float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = $blur; - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param null|float|int $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if (is_numeric($angle)) { - $this->shadowProperties['direction'] = $angle; - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param null|float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = $distance; - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return null|array|int|string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param null|string $colorValue - * @param null|int $colorAlpha - * @param null|string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this->setGlowSize($size) - ->setGlowColor( - $colorValue ?? $this->glowProperties['color']['value'], - $colorAlpha ?? (int) $this->glowProperties['color']['alpha'], - $colorType ?? $this->glowProperties['color']['type'] - ); - } - - /** - * Get Glow Property. - * - * @param array|string $property - * - * @return null|string - */ - public function getGlowProperty($property) - { - return $this->getArrayElementsValue($this->glowProperties, $property); - } - - /** - * Set Glow Color. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - if ($size !== null) { - $this->glowProperties['size'] = $size; - } - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - $this->glowProperties['color'] = $this->setColorProperties($color, $alpha, $colorType); - - return $this; - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $this->softEdges['size'] = $size; - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; - } } diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index ec6342c5..f41d7883 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -17,42 +17,42 @@ class Chart /** * Worksheet. * - * @var Worksheet + * @var ?Worksheet */ private $worksheet; /** * Chart Title. * - * @var Title + * @var ?Title */ private $title; /** * Chart Legend. * - * @var Legend + * @var ?Legend */ private $legend; /** * X-Axis Label. * - * @var Title + * @var ?Title */ private $xAxisLabel; /** * Y-Axis Label. * - * @var Title + * @var ?Title */ private $yAxisLabel; /** * Chart Plot Area. * - * @var PlotArea + * @var ?PlotArea */ private $plotArea; @@ -84,20 +84,6 @@ class Chart */ private $xAxis; - /** - * Chart Major Gridlines as. - * - * @var GridLines - */ - private $majorGridlines; - - /** - * Chart Minor Gridlines as. - * - * @var GridLines - */ - private $minorGridlines; - /** * Top-Left Cell Position. * @@ -155,8 +141,18 @@ class Chart /** @var bool */ private $oneCellAnchor = false; + /** @var bool */ + private $autoTitleDeleted = false; + + /** @var bool */ + private $noFill = false; + + /** @var bool */ + private $roundedCorners = false; + /** * Create a new Chart. + * majorGridlines and minorGridlines are deprecated, moved to Axis. * * @param mixed $name * @param mixed $plotVisibleOnly @@ -172,10 +168,14 @@ class Chart $this->plotArea = $plotArea; $this->plotVisibleOnly = $plotVisibleOnly; $this->displayBlanksAs = $displayBlanksAs; - $this->xAxis = $xAxis; - $this->yAxis = $yAxis; - $this->majorGridlines = $majorGridlines; - $this->minorGridlines = $minorGridlines; + $this->xAxis = $xAxis ?? new Axis(); + $this->yAxis = $yAxis ?? new Axis(); + if ($majorGridlines !== null) { + $this->yAxis->setMajorGridlines($majorGridlines); + } + if ($minorGridlines !== null) { + $this->yAxis->setMinorGridlines($minorGridlines); + } } /** @@ -188,12 +188,17 @@ class Chart return $this->name; } + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + /** * Get Worksheet. - * - * @return Worksheet */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->worksheet; } @@ -210,12 +215,7 @@ class Chart return $this; } - /** - * Get Title. - * - * @return Title - */ - public function getTitle() + public function getTitle(): ?Title { return $this->title; } @@ -232,12 +232,7 @@ class Chart return $this; } - /** - * Get Legend. - * - * @return Legend - */ - public function getLegend() + public function getLegend(): ?Legend { return $this->legend; } @@ -254,12 +249,7 @@ class Chart return $this; } - /** - * Get X-Axis Label. - * - * @return Title - */ - public function getXAxisLabel() + public function getXAxisLabel(): ?Title { return $this->xAxisLabel; } @@ -276,12 +266,7 @@ class Chart return $this; } - /** - * Get Y-Axis Label. - * - * @return Title - */ - public function getYAxisLabel() + public function getYAxisLabel(): ?Title { return $this->yAxisLabel; } @@ -298,16 +283,21 @@ class Chart return $this; } - /** - * Get Plot Area. - * - * @return PlotArea - */ - public function getPlotArea() + public function getPlotArea(): ?PlotArea { return $this->plotArea; } + /** + * Set Plot Area. + */ + public function setPlotArea(PlotArea $plotArea): self + { + $this->plotArea = $plotArea; + + return $this; + } + /** * Get Plot Visible Only. * @@ -356,62 +346,58 @@ class Chart return $this; } - /** - * Get yAxis. - * - * @return Axis - */ - public function getChartAxisY() + public function getChartAxisY(): Axis { - if ($this->yAxis !== null) { - return $this->yAxis; - } - $this->yAxis = new Axis(); - return $this->yAxis; } /** - * Get xAxis. - * - * @return Axis + * Set yAxis. */ - public function getChartAxisX() + public function setChartAxisY(?Axis $axis): self { - if ($this->xAxis !== null) { - return $this->xAxis; - } - $this->xAxis = new Axis(); + $this->yAxis = $axis ?? new Axis(); + return $this; + } + + public function getChartAxisX(): Axis + { return $this->xAxis; } + /** + * Set xAxis. + */ + public function setChartAxisX(?Axis $axis): self + { + $this->xAxis = $axis ?? new Axis(); + + return $this; + } + /** * Get Major Gridlines. * - * @return GridLines + * @Deprecated 1.24.0 Use Axis->getMajorGridlines + * + * @codeCoverageIgnore */ - public function getMajorGridlines() + public function getMajorGridlines(): ?GridLines { - if ($this->majorGridlines !== null) { - return $this->majorGridlines; - } - - return new GridLines(); + return $this->yAxis->getMajorGridLines(); } /** * Get Minor Gridlines. * - * @return GridLines + * @Deprecated 1.24.0 Use Axis->getMinorGridlines + * + * @codeCoverageIgnore */ - public function getMinorGridlines() + public function getMinorGridlines(): ?GridLines { - if ($this->minorGridlines !== null) { - return $this->minorGridlines; - } - - return new GridLines(); + return $this->yAxis->getMinorGridLines(); } /** @@ -668,7 +654,7 @@ class Chart public function refresh(): void { - if ($this->worksheet !== null) { + if ($this->worksheet !== null && $this->plotArea !== null) { $this->plotArea->refresh($this->worksheet); } } @@ -696,7 +682,7 @@ class Chart $renderer = new $libraryName($this); - return $renderer->render($outputDestination); + return $renderer->render($outputDestination); // @phpstan-ignore-line } public function getRotX(): ?int @@ -758,4 +744,42 @@ class Chart return $this; } + + public function getAutoTitleDeleted(): bool + { + return $this->autoTitleDeleted; + } + + public function setAutoTitleDeleted(bool $autoTitleDeleted): self + { + $this->autoTitleDeleted = $autoTitleDeleted; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getRoundedCorners(): bool + { + return $this->roundedCorners; + } + + public function setRoundedCorners(?bool $roundedCorners): self + { + if ($roundedCorners !== null) { + $this->roundedCorners = $roundedCorners; + } + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/ChartColor.php b/src/PhpSpreadsheet/Chart/ChartColor.php new file mode 100644 index 00000000..87f31020 --- /dev/null +++ b/src/PhpSpreadsheet/Chart/ChartColor.php @@ -0,0 +1,177 @@ +setColorPropertiesArray($value); + } else { + $this->setColorProperties($value, $alpha, $type, $brightness); + } + } + + public function getValue(): string + { + return $this->value; + } + + public function setValue(string $value): self + { + $this->value = $value; + + return $this; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getAlpha(): ?int + { + return $this->alpha; + } + + public function setAlpha(?int $alpha): self + { + $this->alpha = $alpha; + + return $this; + } + + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + + /** + * @param null|float|int|string $alpha + * @param null|float|int|string $brightness + */ + public function setColorProperties(?string $color, $alpha = null, ?string $type = null, $brightness = null): self + { + if (empty($type) && !empty($color)) { + if (substr($color, 0, 1) === '*') { + $type = 'schemeClr'; + $color = substr($color, 1); + } elseif (substr($color, 0, 1) === '/') { + $type = 'prstClr'; + $color = substr($color, 1); + } elseif (preg_match('/^[0-9A-Fa-f]{6}$/', $color) === 1) { + $type = 'srgbClr'; + } + } + if ($color !== null) { + $this->setValue("$color"); + } + if ($type !== null) { + $this->setType($type); + } + if ($alpha === null) { + $this->setAlpha(null); + } elseif (is_numeric($alpha)) { + $this->setAlpha((int) $alpha); + } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } + + return $this; + } + + public function setColorPropertiesArray(array $color): self + { + return $this->setColorProperties( + $color['value'] ?? '', + $color['alpha'] ?? null, + $color['type'] ?? null, + $color['brightness'] ?? null + ); + } + + public function isUsable(): bool + { + return $this->type !== '' && $this->value !== ''; + } + + /** + * Get Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getColorProperty($propertyName) + { + $retVal = null; + if ($propertyName === 'value') { + $retVal = $this->value; + } elseif ($propertyName === 'type') { + $retVal = $this->type; + } elseif ($propertyName === 'alpha') { + $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; + } + + return $retVal; + } + + public static function alphaToXml(int $alpha): string + { + return (string) (100 - $alpha) . '000'; + } + + /** + * @param float|int|string $alpha + */ + public static function alphaFromXml($alpha): int + { + return 100 - ((int) $alpha / 1000); + } +} diff --git a/src/PhpSpreadsheet/Chart/DataSeries.php b/src/PhpSpreadsheet/Chart/DataSeries.php index dca1186e..5d33e96d 100644 --- a/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/src/PhpSpreadsheet/Chart/DataSeries.php @@ -94,7 +94,7 @@ class DataSeries private $plotCategory = []; /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ @@ -134,12 +134,12 @@ class DataSeries $this->plotOrder = $plotOrder; $keys = array_keys($plotValues); $this->plotValues = $plotValues; - if ((count($plotLabel) == 0) || ($plotLabel[$keys[0]] === null)) { + if (!isset($plotLabel[$keys[0]])) { $plotLabel[$keys[0]] = new DataSeriesValues(); } $this->plotLabel = $plotLabel; - if ((count($plotCategory) == 0) || ($plotCategory[$keys[0]] === null)) { + if (!isset($plotCategory[$keys[0]])) { $plotCategory[$keys[0]] = new DataSeriesValues(); } $this->plotCategory = $plotCategory; @@ -257,8 +257,6 @@ class DataSeries $keys = array_keys($this->plotLabel); if (in_array($index, $keys)) { return $this->plotLabel[$index]; - } elseif (isset($keys[$index])) { - return $this->plotLabel[$keys[$index]]; } return false; @@ -339,8 +337,6 @@ class DataSeries $keys = array_keys($this->plotValues); if (in_array($index, $keys)) { return $this->plotValues[$index]; - } elseif (isset($keys[$index])) { - return $this->plotValues[$keys[$index]]; } return false; diff --git a/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 6747934a..cd166b23 100644 --- a/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -7,7 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class DataSeriesValues +class DataSeriesValues extends Properties { const DATASERIES_TYPE_STRING = 'String'; const DATASERIES_TYPE_NUMBER = 'Number'; @@ -45,6 +45,12 @@ class DataSeriesValues */ private $pointMarker; + /** @var ChartColor */ + private $markerFillColor; + + /** @var ChartColor */ + private $markerBorderColor; + /** * Series Point Size. * @@ -69,26 +75,22 @@ class DataSeriesValues /** * Fill color (can be array with colors if dataseries have custom colors). * - * @var null|string|string[] + * @var null|ChartColor|ChartColor[] */ private $fillColor; - /** @var string */ - private $schemeClr = ''; - - /** - * Line Width. - * - * @var int - */ - private $lineWidth = 12700; - /** @var bool */ private $scatterLines = true; /** @var bool */ private $bubble3D = false; + /** @var ?Layout */ + private $labelLayout; + + /** @var TrendLine[] */ + private $trendLines = []; + /** * Create a new DataSeriesValues object. * @@ -98,18 +100,23 @@ class DataSeriesValues * @param int $pointCount * @param mixed $dataValues * @param null|mixed $marker - * @param null|string|string[] $fillColor + * @param null|ChartColor|ChartColor[]|string|string[] $fillColor * @param string $pointSize */ public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { + parent::__construct(); + $this->markerFillColor = new ChartColor(); + $this->markerBorderColor = new ChartColor(); $this->setDataType($dataType); $this->dataSource = $dataSource; $this->formatCode = $formatCode; $this->pointCount = $pointCount; $this->dataValues = $dataValues; $this->pointMarker = $marker; - $this->fillColor = $fillColor; + if ($fillColor !== null) { + $this->setFillColor($fillColor); + } if (is_numeric($pointSize)) { $this->pointSize = (int) $pointSize; } @@ -195,6 +202,16 @@ class DataSeriesValues return $this; } + public function getMarkerFillColor(): ChartColor + { + return $this->markerFillColor; + } + + public function getMarkerBorderColor(): ChartColor + { + return $this->markerBorderColor; + } + /** * Get Point Size. */ @@ -249,33 +266,96 @@ class DataSeriesValues return $this->pointCount; } + /** + * Get fill color object. + * + * @return null|ChartColor|ChartColor[] + */ + public function getFillColorObject() + { + return $this->fillColor; + } + + private function stringToChartColor(string $fillString): ChartColor + { + $value = $type = ''; + if (substr($fillString, 0, 1) === '*') { + $type = 'schemeClr'; + $value = substr($fillString, 1); + } elseif (substr($fillString, 0, 1) === '/') { + $type = 'prstClr'; + $value = substr($fillString, 1); + } elseif ($fillString !== '') { + $type = 'srgbClr'; + $value = $fillString; + $this->validateColor($value); + } + + return new ChartColor($value, null, $type); + } + + private function chartColorToString(ChartColor $chartColor): string + { + $type = (string) $chartColor->getColorProperty('type'); + $value = (string) $chartColor->getColorProperty('value'); + if ($type === '' || $value === '') { + return ''; + } + if ($type === 'schemeClr') { + return "*$value"; + } + if ($type === 'prstClr') { + return "/$value"; + } + + return $value; + } + /** * Get fill color. * - * @return null|string|string[] HEX color or array with HEX colors + * @return string|string[] HEX color or array with HEX colors */ public function getFillColor() { - return $this->fillColor; + if ($this->fillColor === null) { + return ''; + } + if (is_array($this->fillColor)) { + $array = []; + foreach ($this->fillColor as $chartColor) { + $array[] = $this->chartColorToString($chartColor); + } + + return $array; + } + + return $this->chartColorToString($this->fillColor); } /** * Set fill color for series. * - * @param null|string|string[] $color HEX color or array with HEX colors + * @param ChartColor|ChartColor[]|string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ public function setFillColor($color) { if (is_array($color)) { - foreach ($color as $colorValue) { - $this->validateColor($colorValue); + $this->fillColor = []; + foreach ($color as $fillString) { + if ($fillString instanceof ChartColor) { + $this->fillColor[] = $fillString; + } else { + $this->fillColor[] = $this->stringToChartColor($fillString); + } } + } elseif ($color instanceof ChartColor) { + $this->fillColor = $color; } else { - $this->validateColor("$color"); + $this->fillColor = $this->stringToChartColor($color); } - $this->fillColor = $color; return $this; } @@ -299,24 +379,23 @@ class DataSeriesValues /** * Get line width for series. * - * @return int + * @return null|float|int */ public function getLineWidth() { - return $this->lineWidth; + return $this->lineStyleProperties['width']; } /** * Set line width for the series. * - * @param int $width + * @param null|float|int $width * * @return $this */ public function setLineWidth($width) { - $minWidth = 12700; - $this->lineWidth = max($minWidth, $width); + $this->lineStyleProperties['width'] = $width; return $this; } @@ -459,15 +538,58 @@ class DataSeriesValues return $this; } - public function getSchemeClr(): string + /** + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. + * + * @var bool + */ + private $smoothLine; + + /** + * Get Smooth Line. + * + * @return bool + */ + public function getSmoothLine() { - return $this->schemeClr; + return $this->smoothLine; } - public function setSchemeClr(string $schemeClr): self + /** + * Set Smooth Line. + * + * @param bool $smoothLine + * + * @return $this + */ + public function setSmoothLine($smoothLine) { - $this->schemeClr = $schemeClr; + $this->smoothLine = $smoothLine; return $this; } + + public function getLabelLayout(): ?Layout + { + return $this->labelLayout; + } + + public function setLabelLayout(?Layout $labelLayout): self + { + $this->labelLayout = $labelLayout; + + return $this; + } + + public function setTrendLines(array $trendLines): self + { + $this->trendLines = $trendLines; + + return $this; + } + + public function getTrendLines(): array + { + return $this->trendLines; + } } diff --git a/src/PhpSpreadsheet/Chart/GridLines.php b/src/PhpSpreadsheet/Chart/GridLines.php index ad254c8c..8b86ccbd 100644 --- a/src/PhpSpreadsheet/Chart/GridLines.php +++ b/src/PhpSpreadsheet/Chart/GridLines.php @@ -10,450 +10,4 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class GridLines extends Properties { - /** - * Properties of Class: - * Object State (State for Minor Tick Mark) @var bool - * Line Properties @var array of mixed - * Shadow Properties @var array of mixed - * Glow Properties @var array of mixed - * Soft Properties @var array of mixed. - */ - private $objectState = false; - - private $lineProperties = [ - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => null, - 'alpha' => 0, - ], - 'style' => [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ], - ]; - - private $shadowProperties = Properties::PRESETS_OPTIONS[0]; - - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - private $softEdges = [ - 'size' => null, - ]; - - /** - * Get Object State. - * - * @return bool - */ - public function getObjectState() - { - return $this->objectState; - } - - /** - * Change Object State to True. - * - * @return $this - */ - private function activateObject() - { - $this->objectState = true; - - return $this; - } - - /** - * Set Line Color Properties. - * - * @param string $value - * @param int $alpha - * @param string $colorType - */ - public function setLineColorProperties($value, $alpha = 0, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void - { - $this->activateObject() - ->lineProperties['color'] = $this->setColorProperties( - $value, - $alpha, - $colorType - ); - } - - /** - * Set Line Color Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - $this->activateObject(); - ($lineWidth !== null) - ? $this->lineProperties['style']['width'] = $this->getExcelPointsWidth((float) $lineWidth) - : null; - ($compoundType !== null) - ? $this->lineProperties['style']['compound'] = (string) $compoundType - : null; - ($dashType !== null) - ? $this->lineProperties['style']['dash'] = (string) $dashType - : null; - ($capType !== null) - ? $this->lineProperties['style']['cap'] = (string) $capType - : null; - ($joinType !== null) - ? $this->lineProperties['style']['join'] = (string) $joinType - : null; - ($headArrowType !== null) - ? $this->lineProperties['style']['arrow']['head']['type'] = (string) $headArrowType - : null; - ($headArrowSize !== null) - ? $this->lineProperties['style']['arrow']['head']['size'] = (string) $headArrowSize - : null; - ($endArrowType !== null) - ? $this->lineProperties['style']['arrow']['end']['type'] = (string) $endArrowType - : null; - ($endArrowSize !== null) - ? $this->lineProperties['style']['arrow']['end']['size'] = (string) $endArrowSize - : null; - } - - /** - * Get Line Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getLineColorProperty($propertyName) - { - return $this->lineProperties['color'][$propertyName]; - } - - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) - { - return $this->getArrayElementsValue($this->lineProperties['style'], $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param string $colorValue - * @param int $colorAlpha - * @param string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this - ->activateObject() - ->setGlowSize($size) - ->setGlowColor($colorValue, $colorAlpha, $colorType); - } - - /** - * Get Glow Property. - * - * @param array|string $property - * - * @return null|string - */ - public function getGlowProperty($property) - { - return $this->getArrayElementsValue($this->glowProperties, $property); - } - - /** - * Get Glow Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getGlowColor($propertyName) - { - return $this->glowProperties['color'][$propertyName]; - } - - /** - * Get Glow Size. - * - * @return string - */ - public function getGlowSize() - { - return $this->glowProperties['size']; - } - - /** - * Set Glow Size. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - $this->glowProperties['size'] = $size; - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->glowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->glowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->glowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Get Line Style Arrow Parameters. - * - * @param string $arrowSelector - * @param string $propertySelector - * - * @return string - */ - public function getLineStyleArrowParameters($arrowSelector, $propertySelector) - { - return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); - } - - /** - * @param mixed $value - */ - public function setShadowProperty(string $propertyName, $value): self - { - $this->activateObject(); - $this->shadowProperties[$propertyName] = $value; - - return $this; - } - - /** - * Set Shadow Properties. - * - * @param int $presets - * @param string $colorValue - * @param string $colorType - * @param null|float|int|string $colorAlpha - * @param null|float $blur - * @param null|int $angle - * @param null|float $distance - */ - public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void - { - $this->activateObject() - ->setShadowPresetsProperties((int) $presets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : (int) $colorAlpha, - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Presets Properties. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); - - return $this; - } - - private const SHADOW_ARRAY_KEYS = ['size', 'color']; - - /** - * Set Shadow Properties Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) - { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { - $reference = &$this->shadowProperties[$property_key]; - $this->setShadowPropertiesMapValues($property_val, $reference); - } - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } - - return $this; - } - - /** - * Set Shadow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->shadowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->shadowProperties['color']['alpha'] = (int) $alpha; - } - if ($colorType !== null) { - $this->shadowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param ?float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = $blur; - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param null|float|int|string $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if (is_numeric($angle)) { - $this->shadowProperties['direction'] = $angle; - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param ?float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = $distance; - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $this->activateObject(); - $this->softEdges['size'] = $size; - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; - } } diff --git a/src/PhpSpreadsheet/Chart/Layout.php b/src/PhpSpreadsheet/Chart/Layout.php index cea96557..3dabcc63 100644 --- a/src/PhpSpreadsheet/Chart/Layout.php +++ b/src/PhpSpreadsheet/Chart/Layout.php @@ -53,11 +53,24 @@ class Layout */ private $height; + /** + * Position - t=top. + * + * @var string + */ + private $dLblPos = ''; + + /** @var string */ + private $numFmtCode = ''; + + /** @var bool */ + private $numFmtLinked = false; + /** * show legend key * Specifies that legend keys should be shown in data labels. * - * @var bool + * @var ?bool */ private $showLegendKey; @@ -65,7 +78,7 @@ class Layout * show value * Specifies that the value should be shown in a data label. * - * @var bool + * @var ?bool */ private $showVal; @@ -73,7 +86,7 @@ class Layout * show category name * Specifies that the category name should be shown in the data label. * - * @var bool + * @var ?bool */ private $showCatName; @@ -81,7 +94,7 @@ class Layout * show data series name * Specifies that the series name should be shown in the data label. * - * @var bool + * @var ?bool */ private $showSerName; @@ -89,14 +102,14 @@ class Layout * show percentage * Specifies that the percentage should be shown in the data label. * - * @var bool + * @var ?bool */ private $showPercent; /** * show bubble size. * - * @var bool + * @var ?bool */ private $showBubbleSize; @@ -104,10 +117,19 @@ class Layout * show leader lines * Specifies that leader lines should be shown for the data label. * - * @var bool + * @var ?bool */ private $showLeaderLines; + /** @var ?ChartColor */ + private $labelFillColor; + + /** @var ?ChartColor */ + private $labelBorderColor; + + /** @var ?ChartColor */ + private $labelFontColor; + /** * Create a new Layout. */ @@ -134,6 +156,37 @@ class Layout if (isset($layout['h'])) { $this->height = (float) $layout['h']; } + if (isset($layout['dLblPos'])) { + $this->dLblPos = (string) $layout['dLblPos']; + } + if (isset($layout['numFmtCode'])) { + $this->numFmtCode = (string) $layout['numFmtCode']; + } + $this->initBoolean($layout, 'showLegendKey'); + $this->initBoolean($layout, 'showVal'); + $this->initBoolean($layout, 'showCatName'); + $this->initBoolean($layout, 'showSerName'); + $this->initBoolean($layout, 'showPercent'); + $this->initBoolean($layout, 'showBubbleSize'); + $this->initBoolean($layout, 'showLeaderLines'); + $this->initBoolean($layout, 'numFmtLinked'); + $this->initColor($layout, 'labelFillColor'); + $this->initColor($layout, 'labelBorderColor'); + $this->initColor($layout, 'labelFontColor'); + } + + private function initBoolean(array $layout, string $name): void + { + if (isset($layout[$name])) { + $this->$name = (bool) $layout[$name]; + } + } + + private function initColor(array $layout, string $name): void + { + if (isset($layout[$name]) && $layout[$name] instanceof ChartColor) { + $this->$name = $layout[$name]; + } } /** @@ -304,12 +357,7 @@ class Layout return $this; } - /** - * Get show legend key. - * - * @return bool - */ - public function getShowLegendKey() + public function getShowLegendKey(): ?bool { return $this->showLegendKey; } @@ -317,24 +365,15 @@ class Layout /** * Set show legend key * Specifies that legend keys should be shown in data labels. - * - * @param bool $showLegendKey Show legend key - * - * @return $this */ - public function setShowLegendKey($showLegendKey) + public function setShowLegendKey(?bool $showLegendKey): self { $this->showLegendKey = $showLegendKey; return $this; } - /** - * Get show value. - * - * @return bool - */ - public function getShowVal() + public function getShowVal(): ?bool { return $this->showVal; } @@ -342,24 +381,15 @@ class Layout /** * Set show val * Specifies that the value should be shown in data labels. - * - * @param bool $showDataLabelValues Show val - * - * @return $this */ - public function setShowVal($showDataLabelValues) + public function setShowVal(?bool $showDataLabelValues): self { $this->showVal = $showDataLabelValues; return $this; } - /** - * Get show category name. - * - * @return bool - */ - public function getShowCatName() + public function getShowCatName(): ?bool { return $this->showCatName; } @@ -367,115 +397,147 @@ class Layout /** * Set show cat name * Specifies that the category name should be shown in data labels. - * - * @param bool $showCategoryName Show cat name - * - * @return $this */ - public function setShowCatName($showCategoryName) + public function setShowCatName(?bool $showCategoryName): self { $this->showCatName = $showCategoryName; return $this; } - /** - * Get show data series name. - * - * @return bool - */ - public function getShowSerName() + public function getShowSerName(): ?bool { return $this->showSerName; } /** - * Set show ser name + * Set show data series name. * Specifies that the series name should be shown in data labels. - * - * @param bool $showSeriesName Show series name - * - * @return $this */ - public function setShowSerName($showSeriesName) + public function setShowSerName(?bool $showSeriesName): self { $this->showSerName = $showSeriesName; return $this; } - /** - * Get show percentage. - * - * @return bool - */ - public function getShowPercent() + public function getShowPercent(): ?bool { return $this->showPercent; } /** - * Set show percentage + * Set show percentage. * Specifies that the percentage should be shown in data labels. - * - * @param bool $showPercentage Show percentage - * - * @return $this */ - public function setShowPercent($showPercentage) + public function setShowPercent(?bool $showPercentage): self { $this->showPercent = $showPercentage; return $this; } - /** - * Get show bubble size. - * - * @return bool - */ - public function getShowBubbleSize() + public function getShowBubbleSize(): ?bool { return $this->showBubbleSize; } /** - * Set show bubble size + * Set show bubble size. * Specifies that the bubble size should be shown in data labels. - * - * @param bool $showBubbleSize Show bubble size - * - * @return $this */ - public function setShowBubbleSize($showBubbleSize) + public function setShowBubbleSize(?bool $showBubbleSize): self { $this->showBubbleSize = $showBubbleSize; return $this; } - /** - * Get show leader lines. - * - * @return bool - */ - public function getShowLeaderLines() + public function getShowLeaderLines(): ?bool { return $this->showLeaderLines; } /** - * Set show leader lines + * Set show leader lines. * Specifies that leader lines should be shown in data labels. - * - * @param bool $showLeaderLines Show leader lines - * - * @return $this */ - public function setShowLeaderLines($showLeaderLines) + public function setShowLeaderLines(?bool $showLeaderLines): self { $this->showLeaderLines = $showLeaderLines; return $this; } + + public function getLabelFillColor(): ?ChartColor + { + return $this->labelFillColor; + } + + public function setLabelFillColor(?ChartColor $chartColor): self + { + $this->labelFillColor = $chartColor; + + return $this; + } + + public function getLabelBorderColor(): ?ChartColor + { + return $this->labelBorderColor; + } + + public function setLabelBorderColor(?ChartColor $chartColor): self + { + $this->labelBorderColor = $chartColor; + + return $this; + } + + public function getLabelFontColor(): ?ChartColor + { + return $this->labelFontColor; + } + + public function setLabelFontColor(?ChartColor $chartColor): self + { + $this->labelFontColor = $chartColor; + + return $this; + } + + public function getDLblPos(): string + { + return $this->dLblPos; + } + + public function setDLblPos(string $dLblPos): self + { + $this->dLblPos = $dLblPos; + + return $this; + } + + public function getNumFmtCode(): string + { + return $this->numFmtCode; + } + + public function setNumFmtCode(string $numFmtCode): self + { + $this->numFmtCode = $numFmtCode; + + return $this; + } + + public function getNumFmtLinked(): bool + { + return $this->numFmtLinked; + } + + public function setNumFmtLinked(bool $numFmtLinked): self + { + $this->numFmtLinked = $numFmtLinked; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Chart/Legend.php b/src/PhpSpreadsheet/Chart/Legend.php index 2f003cd8..fc16017c 100644 --- a/src/PhpSpreadsheet/Chart/Legend.php +++ b/src/PhpSpreadsheet/Chart/Legend.php @@ -18,7 +18,7 @@ class Legend const POSITION_TOP = 't'; const POSITION_TOPRIGHT = 'tr'; - private static $positionXLref = [ + const POSITION_XLREF = [ self::XL_LEGEND_POSITION_BOTTOM => self::POSITION_BOTTOM, self::XL_LEGEND_POSITION_CORNER => self::POSITION_TOPRIGHT, self::XL_LEGEND_POSITION_CUSTOM => '??', @@ -44,7 +44,7 @@ class Legend /** * Legend Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -80,7 +80,7 @@ class Legend */ public function setPosition($position) { - if (!in_array($position, self::$positionXLref)) { + if (!in_array($position, self::POSITION_XLREF)) { return false; } @@ -92,11 +92,11 @@ class Legend /** * Get legend position as an Excel internal numeric value. * - * @return int + * @return false|int */ public function getPositionXL() { - return array_search($this->position, self::$positionXLref); + return array_search($this->position, self::POSITION_XLREF); } /** @@ -108,11 +108,11 @@ class Legend */ public function setPositionXL($positionXL) { - if (!isset(self::$positionXLref[$positionXL])) { + if (!isset(self::POSITION_XLREF[$positionXL])) { return false; } - $this->position = self::$positionXLref[$positionXL]; + $this->position = self::POSITION_XLREF[$positionXL]; return true; } @@ -140,7 +140,7 @@ class Legend /** * Get Layout. * - * @return Layout + * @return ?Layout */ public function getLayout() { diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index ecb7b6c9..ccde4bb2 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -6,10 +6,34 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PlotArea { + /** + * No fill in plot area (show Excel gridlines through chart). + * + * @var bool + */ + private $noFill = false; + + /** + * PlotArea Gradient Stop list. + * Each entry is a 2-element array. + * First is position in %. + * Second is ChartColor. + * + * @var array[] + */ + private $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + * + * @var ?float + */ + private $gradientFillAngle; + /** * PlotArea Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -31,12 +55,7 @@ class PlotArea $this->plotSeries = $plotSeries; } - /** - * Get Layout. - * - * @return Layout - */ - public function getLayout() + public function getLayout(): ?Layout { return $this->layout; } @@ -106,4 +125,42 @@ class PlotArea $plotSeries->refresh($worksheet); } } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops() + { + return $this->gradientFillStops; + } } diff --git a/src/PhpSpreadsheet/Chart/Properties.php b/src/PhpSpreadsheet/Chart/Properties.php index 01a83915..f737ca08 100644 --- a/src/PhpSpreadsheet/Chart/Properties.php +++ b/src/PhpSpreadsheet/Chart/Properties.php @@ -10,10 +10,12 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ abstract class Properties { - const - EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; - const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; - const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_STANDARD = ChartColor::EXCEL_COLOR_TYPE_STANDARD; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_SCHEME = ChartColor::EXCEL_COLOR_TYPE_SCHEME; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_ARGB = ChartColor::EXCEL_COLOR_TYPE_ARGB; const AXIS_LABELS_LOW = 'low'; @@ -57,6 +59,8 @@ abstract class Properties const LINE_STYLE_COMPOUND_TRIPLE = 'tri'; const LINE_STYLE_DASH_SOLID = 'solid'; const LINE_STYLE_DASH_ROUND_DOT = 'sysDot'; + const LINE_STYLE_DASH_SQUARE_DOT = 'sysDash'; + /** @deprecated 1.24 use LINE_STYLE_DASH_SQUARE_DOT instead */ const LINE_STYLE_DASH_SQUERE_DOT = 'sysDash'; const LINE_STYPE_DASH_DASH = 'dash'; const LINE_STYLE_DASH_DASH_DOT = 'dashDot'; @@ -66,7 +70,7 @@ abstract class Properties const LINE_STYLE_CAP_SQUARE = 'sq'; const LINE_STYLE_CAP_ROUND = 'rnd'; const LINE_STYLE_CAP_FLAT = 'flat'; - const LINE_STYLE_JOIN_ROUND = 'bevel'; + const LINE_STYLE_JOIN_ROUND = 'round'; const LINE_STYLE_JOIN_MITER = 'miter'; const LINE_STYLE_JOIN_BEVEL = 'bevel'; const LINE_STYLE_ARROW_TYPE_NOARROW = null; @@ -115,14 +119,56 @@ abstract class Properties const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy - /** - * @param float $width - * - * @return float - */ - protected function getExcelPointsWidth($width) + /** @var bool */ + protected $objectState = false; // used only for minor gridlines + + /** @var ?float */ + protected $glowSize; + + /** @var ChartColor */ + protected $glowColor; + + /** @var array */ + protected $softEdges = [ + 'size' => null, + ]; + + /** @var array */ + protected $shadowProperties = self::PRESETS_OPTIONS[0]; + + /** @var ChartColor */ + protected $shadowColor; + + public function __construct() { - return $width * self::POINTS_WIDTH_MULTIPLIER; + $this->lineColor = new ChartColor(); + $this->glowColor = new ChartColor(); + $this->shadowColor = new ChartColor(); + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + + /** + * Get Object State. + * + * @return bool + */ + public function getObjectState() + { + return $this->objectState; + } + + /** + * Change Object State to True. + * + * @return $this + */ + public function activateObject() + { + $this->objectState = true; + + return $this; } public static function pointsToXml(float $width): string @@ -155,19 +201,6 @@ abstract class Properties return ((float) $value) / self::PERCENTAGE_MULTIPLIER; } - public static function alphaToXml(int $alpha): string - { - return (string) (100 - $alpha) . '000'; - } - - /** - * @param float|int|string $alpha - */ - public static function alphaFromXml($alpha): int - { - return 100 - ((int) $alpha / 1000); - } - /** * @param null|float|int|string $alpha */ @@ -176,37 +209,20 @@ abstract class Properties return [ 'type' => $colorType, 'value' => $color, - 'alpha' => (int) $alpha, + 'alpha' => ($alpha === null) ? null : (int) $alpha, ]; } - protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) - { - $sizes = [ - 1 => ['w' => 'sm', 'len' => 'sm'], - 2 => ['w' => 'sm', 'len' => 'med'], - 3 => ['w' => 'sm', 'len' => 'lg'], - 4 => ['w' => 'med', 'len' => 'sm'], - 5 => ['w' => 'med', 'len' => 'med'], - 6 => ['w' => 'med', 'len' => 'lg'], - 7 => ['w' => 'lg', 'len' => 'sm'], - 8 => ['w' => 'lg', 'len' => 'med'], - 9 => ['w' => 'lg', 'len' => 'lg'], - ]; - - return $sizes[$arraySelector][$arrayKaySelector]; - } - protected const PRESETS_OPTIONS = [ //NONE 0 => [ 'presets' => self::SHADOW_PRESETS_NOSHADOW, 'effect' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], + //'color' => [ + // 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, + // 'value' => 'black', + // 'alpha' => 40, + //], 'size' => [ 'sx' => null, 'sy' => null, @@ -405,11 +421,19 @@ abstract class Properties ], ]; - protected function getShadowPresetsMap($presetsOption) + protected function getShadowPresetsMap(int $presetsOption): array { return self::PRESETS_OPTIONS[$presetsOption] ?? self::PRESETS_OPTIONS[0]; } + /** + * Get value of array element. + * + * @param mixed $properties + * @param mixed $elements + * + * @return mixed + */ protected function getArrayElementsValue($properties, $elements) { $reference = &$properties; @@ -423,4 +447,539 @@ abstract class Properties return $reference; } + + /** + * Set Glow Properties. + * + * @param float $size + * @param ?string $colorValue + * @param ?int $colorAlpha + * @param ?string $colorType + */ + public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void + { + $this + ->activateObject() + ->setGlowSize($size); + $this->glowColor->setColorPropertiesArray( + [ + 'value' => $colorValue, + 'type' => $colorType, + 'alpha' => $colorAlpha, + ] + ); + } + + /** + * Get Glow Property. + * + * @param array|string $property + * + * @return null|array|float|int|string + */ + public function getGlowProperty($property) + { + $retVal = null; + if ($property === 'size') { + $retVal = $this->glowSize; + } elseif ($property === 'color') { + $retVal = [ + 'value' => $this->glowColor->getColorProperty('value'), + 'type' => $this->glowColor->getColorProperty('type'), + 'alpha' => $this->glowColor->getColorProperty('alpha'), + ]; + } elseif (is_array($property) && count($property) >= 2 && $property[0] === 'color') { + $retVal = $this->glowColor->getColorProperty($property[1]); + } + + return $retVal; + } + + /** + * Get Glow Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getGlowColor($propertyName) + { + return $this->glowColor->getColorProperty($propertyName); + } + + public function getGlowColorObject(): ChartColor + { + return $this->glowColor; + } + + /** + * Get Glow Size. + * + * @return ?float + */ + public function getGlowSize() + { + return $this->glowSize; + } + + /** + * Set Glow Size. + * + * @param ?float $size + * + * @return $this + */ + protected function setGlowSize($size) + { + $this->glowSize = $size; + + return $this; + } + + /** + * Set Soft Edges Size. + * + * @param float $size + */ + public function setSoftEdges($size): void + { + if ($size !== null) { + $this->activateObject(); + $this->softEdges['size'] = $size; + } + } + + /** + * Get Soft Edges Size. + * + * @return string + */ + public function getSoftEdgesSize() + { + return $this->softEdges['size']; + } + + /** + * @param mixed $value + */ + public function setShadowProperty(string $propertyName, $value): self + { + $this->activateObject(); + if ($propertyName === 'color' && is_array($value)) { + $this->shadowColor->setColorPropertiesArray($value); + } else { + $this->shadowProperties[$propertyName] = $value; + } + + return $this; + } + + /** + * Set Shadow Properties. + * + * @param int $presets + * @param string $colorValue + * @param string $colorType + * @param null|float|int|string $colorAlpha + * @param null|float $blur + * @param null|int $angle + * @param null|float $distance + */ + public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + { + $this->activateObject()->setShadowPresetsProperties((int) $presets); + if ($presets === 0) { + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + if ($colorValue !== null) { + $this->shadowColor->setValue($colorValue); + } + if ($colorType !== null) { + $this->shadowColor->setType($colorType); + } + if (is_numeric($colorAlpha)) { + $this->shadowColor->setAlpha((int) $colorAlpha); + } + $this + ->setShadowBlur($blur) + ->setShadowAngle($angle) + ->setShadowDistance($distance); + } + + /** + * Set Shadow Presets Properties. + * + * @param int $presets + * + * @return $this + */ + protected function setShadowPresetsProperties($presets) + { + $this->shadowProperties['presets'] = $presets; + $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + + return $this; + } + + protected const SHADOW_ARRAY_KEYS = ['size', 'color']; + + /** + * Set Shadow Properties Values. + * + * @param mixed $reference + * + * @return $this + */ + protected function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + { + $base_reference = $reference; + foreach ($propertiesMap as $property_key => $property_val) { + if (is_array($property_val)) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { + $reference = &$this->shadowProperties[$property_key]; + $this->setShadowPropertiesMapValues($property_val, $reference); + } + } else { + if ($base_reference === null) { + $this->shadowProperties[$property_key] = $property_val; + } else { + $reference[$property_key] = $property_val; + } + } + } + + return $this; + } + + /** + * Set Shadow Blur. + * + * @param ?float $blur + * + * @return $this + */ + protected function setShadowBlur($blur) + { + if ($blur !== null) { + $this->shadowProperties['blur'] = $blur; + } + + return $this; + } + + /** + * Set Shadow Angle. + * + * @param null|float|int|string $angle + * + * @return $this + */ + protected function setShadowAngle($angle) + { + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; + } + + return $this; + } + + /** + * Set Shadow Distance. + * + * @param ?float $distance + * + * @return $this + */ + protected function setShadowDistance($distance) + { + if ($distance !== null) { + $this->shadowProperties['distance'] = $distance; + } + + return $this; + } + + public function getShadowColorObject(): ChartColor + { + return $this->shadowColor; + } + + /** + * Get Shadow Property. + * + * @param string|string[] $elements + * + * @return array|string + */ + public function getShadowProperty($elements) + { + if ($elements === 'color') { + return [ + 'value' => $this->shadowColor->getValue(), + 'type' => $this->shadowColor->getType(), + 'alpha' => $this->shadowColor->getAlpha(), + ]; + } + + return $this->getArrayElementsValue($this->shadowProperties, $elements); + } + + public function getShadowArray(): array + { + $array = $this->shadowProperties; + if ($this->getShadowColorObject()->isUsable()) { + $array['color'] = $this->getShadowProperty('color'); + } + + return $array; + } + + /** @var ChartColor */ + protected $lineColor; + + /** @var array */ + protected $lineStyleProperties = [ + 'width' => null, //'9525', + 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => '', //self::LINE_STYLE_DASH_SOLID, + 'cap' => '', //self::LINE_STYLE_CAP_FLAT, + 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, + 'w' => '', + 'len' => '', + ], + 'end' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, + 'w' => '', + 'len' => '', + ], + ], + ]; + + public function copyLineStyles(self $otherProperties): void + { + $this->lineStyleProperties = $otherProperties->lineStyleProperties; + $this->lineColor = $otherProperties->lineColor; + $this->glowSize = $otherProperties->glowSize; + $this->glowColor = $otherProperties->glowColor; + $this->softEdges = $otherProperties->softEdges; + $this->shadowProperties = $otherProperties->shadowProperties; + } + + public function getLineColor(): ChartColor + { + return $this->lineColor; + } + + /** + * Set Line Color Properties. + * + * @param string $value + * @param ?int $alpha + * @param ?string $colorType + */ + public function setLineColorProperties($value, $alpha = null, $colorType = null): void + { + $this->activateObject(); + $this->lineColor->setColorPropertiesArray( + $this->setColorProperties( + $value, + $alpha, + $colorType + ) + ); + } + + /** + * Get Line Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getLineColorProperty($propertyName) + { + return $this->lineColor->getColorProperty($propertyName); + } + + /** + * Set Line Style Properties. + * + * @param null|float|int|string $lineWidth + * @param string $compoundType + * @param string $dashType + * @param string $capType + * @param string $joinType + * @param string $headArrowType + * @param string $headArrowSize + * @param string $endArrowType + * @param string $endArrowSize + * @param string $headArrowWidth + * @param string $headArrowLength + * @param string $endArrowWidth + * @param string $endArrowLength + */ + public function setLineStyleProperties($lineWidth = null, $compoundType = '', $dashType = '', $capType = '', $joinType = '', $headArrowType = '', $headArrowSize = '', $endArrowType = '', $endArrowSize = '', $headArrowWidth = '', $headArrowLength = '', $endArrowWidth = '', $endArrowLength = ''): void + { + $this->activateObject(); + if (is_numeric($lineWidth)) { + $this->lineStyleProperties['width'] = $lineWidth; + } + if ($compoundType !== '') { + $this->lineStyleProperties['compound'] = $compoundType; + } + if ($dashType !== '') { + $this->lineStyleProperties['dash'] = $dashType; + } + if ($capType !== '') { + $this->lineStyleProperties['cap'] = $capType; + } + if ($joinType !== '') { + $this->lineStyleProperties['join'] = $joinType; + } + if ($headArrowType !== '') { + $this->lineStyleProperties['arrow']['head']['type'] = $headArrowType; + } + if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { + $this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize; + $this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; + $this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; + } + if ($endArrowType !== '') { + $this->lineStyleProperties['arrow']['end']['type'] = $endArrowType; + } + if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { + $this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize; + $this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; + $this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; + } + if ($headArrowWidth !== '') { + $this->lineStyleProperties['arrow']['head']['w'] = $headArrowWidth; + } + if ($headArrowLength !== '') { + $this->lineStyleProperties['arrow']['head']['len'] = $headArrowLength; + } + if ($endArrowWidth !== '') { + $this->lineStyleProperties['arrow']['end']['w'] = $endArrowWidth; + } + if ($endArrowLength !== '') { + $this->lineStyleProperties['arrow']['end']['len'] = $endArrowLength; + } + } + + public function getLineStyleArray(): array + { + return $this->lineStyleProperties; + } + + public function setLineStyleArray(array $lineStyleProperties = []): self + { + $this->activateObject(); + $this->lineStyleProperties['width'] = $lineStyleProperties['width'] ?? null; + $this->lineStyleProperties['compound'] = $lineStyleProperties['compound'] ?? ''; + $this->lineStyleProperties['dash'] = $lineStyleProperties['dash'] ?? ''; + $this->lineStyleProperties['cap'] = $lineStyleProperties['cap'] ?? ''; + $this->lineStyleProperties['join'] = $lineStyleProperties['join'] ?? ''; + $this->lineStyleProperties['arrow']['head']['type'] = $lineStyleProperties['arrow']['head']['type'] ?? ''; + $this->lineStyleProperties['arrow']['head']['size'] = $lineStyleProperties['arrow']['head']['size'] ?? ''; + $this->lineStyleProperties['arrow']['head']['w'] = $lineStyleProperties['arrow']['head']['w'] ?? ''; + $this->lineStyleProperties['arrow']['head']['len'] = $lineStyleProperties['arrow']['head']['len'] ?? ''; + $this->lineStyleProperties['arrow']['end']['type'] = $lineStyleProperties['arrow']['end']['type'] ?? ''; + $this->lineStyleProperties['arrow']['end']['size'] = $lineStyleProperties['arrow']['end']['size'] ?? ''; + $this->lineStyleProperties['arrow']['end']['w'] = $lineStyleProperties['arrow']['end']['w'] ?? ''; + $this->lineStyleProperties['arrow']['end']['len'] = $lineStyleProperties['arrow']['end']['len'] ?? ''; + + return $this; + } + + /** + * @param mixed $value + */ + public function setLineStyleProperty(string $propertyName, $value): self + { + $this->activateObject(); + $this->lineStyleProperties[$propertyName] = $value; + + return $this; + } + + /** + * Get Line Style Property. + * + * @param array|string $elements + * + * @return string + */ + public function getLineStyleProperty($elements) + { + return $this->getArrayElementsValue($this->lineStyleProperties, $elements); + } + + protected const ARROW_SIZES = [ + 1 => ['w' => 'sm', 'len' => 'sm'], + 2 => ['w' => 'sm', 'len' => 'med'], + 3 => ['w' => 'sm', 'len' => 'lg'], + 4 => ['w' => 'med', 'len' => 'sm'], + 5 => ['w' => 'med', 'len' => 'med'], + 6 => ['w' => 'med', 'len' => 'lg'], + 7 => ['w' => 'lg', 'len' => 'sm'], + 8 => ['w' => 'lg', 'len' => 'med'], + 9 => ['w' => 'lg', 'len' => 'lg'], + ]; + + /** + * Get Line Style Arrow Size. + * + * @param int $arraySelector + * @param string $arrayKaySelector + * + * @return string + */ + protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) + { + return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? ''; + } + + /** + * Get Line Style Arrow Parameters. + * + * @param string $arrowSelector + * @param string $propertySelector + * + * @return string + */ + public function getLineStyleArrowParameters($arrowSelector, $propertySelector) + { + return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrowSelector]['size'], $propertySelector); + } + + /** + * Get Line Style Arrow Width. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowWidth($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'w']); + } + + /** + * Get Line Style Arrow Excel Length. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowLength($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'len']); + } } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php index b276707d..0b0164b4 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php @@ -2,68 +2,20 @@ namespace PhpOffice\PhpSpreadsheet\Chart\Renderer; -use AccBarPlot; -use AccLinePlot; -use BarPlot; -use ContourPlot; -use Graph; -use GroupBarPlot; -use LinePlot; -use PhpOffice\PhpSpreadsheet\Chart\Chart; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -use PieGraph; -use PiePlot; -use PiePlot3D; -use PiePlotC; -use RadarGraph; -use RadarPlot; -use ScatterPlot; -use Spline; -use StockPlot; - /** - * Jpgraph is not maintained in Composer, and the version there - * is extremely out of date. For that reason, all unit test - * requiring Jpgraph are skipped. So, do not measure - * code coverage for this class till that is fixed. + * Jpgraph is not oficially maintained in Composer, so the version there + * could be out of date. For that reason, all unit test requiring Jpgraph + * are skipped. So, do not measure code coverage for this class till that + * is fixed. + * + * This implementation uses abandoned package + * https://packagist.org/packages/jpgraph/jpgraph * * @codeCoverageIgnore */ -class JpGraph implements IRenderer +class JpGraph extends JpGraphRendererBase { - private static $width = 640; - - private static $height = 480; - - private static $colourSet = [ - 'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1', - 'darkmagenta', 'coral', 'dodgerblue3', 'eggplant', - 'mediumblue', 'magenta', 'sandybrown', 'cyan', - 'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen', - 'goldenrod2', - ]; - - private static $markSet; - - private $chart; - - private $graph; - - private static $plotColour = 0; - - private static $plotMark = 0; - - /** - * Create a new jpgraph. - */ - public function __construct(Chart $chart) - { - self::init(); - $this->graph = null; - $this->chart = $chart; - } - - private static function init(): void + protected static function init(): void { static $loaded = false; if ($loaded) { @@ -81,802 +33,6 @@ class JpGraph implements IRenderer \JpGraph\JpGraph::module('scatter'); \JpGraph\JpGraph::module('stock'); - self::$markSet = [ - 'diamond' => MARK_DIAMOND, - 'square' => MARK_SQUARE, - 'triangle' => MARK_UTRIANGLE, - 'x' => MARK_X, - 'star' => MARK_STAR, - 'dot' => MARK_FILLEDCIRCLE, - 'dash' => MARK_DTRIANGLE, - 'circle' => MARK_CIRCLE, - 'plus' => MARK_CROSS, - ]; - $loaded = true; } - - private function formatPointMarker($seriesPlot, $markerID) - { - $plotMarkKeys = array_keys(self::$markSet); - if ($markerID === null) { - // Use default plot marker (next marker in the series) - self::$plotMark %= count(self::$markSet); - $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); - } elseif ($markerID !== 'none') { - // Use specified plot marker (if it exists) - if (isset(self::$markSet[$markerID])) { - $seriesPlot->mark->SetType(self::$markSet[$markerID]); - } else { - // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) - self::$plotMark %= count(self::$markSet); - $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); - } - } else { - // Hide plot marker - $seriesPlot->mark->Hide(); - } - $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); - $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - - return $seriesPlot; - } - - private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') - { - $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); - if ($datasetLabelFormatCode !== null) { - // Retrieve any label formatting code - $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); - } - - $testCurrentIndex = 0; - foreach ($datasetLabels as $i => $datasetLabel) { - if (is_array($datasetLabel)) { - if ($rotation == 'bar') { - $datasetLabels[$i] = implode(' ', $datasetLabel); - } else { - $datasetLabel = array_reverse($datasetLabel); - $datasetLabels[$i] = implode("\n", $datasetLabel); - } - } else { - // Format labels according to any formatting code - if ($datasetLabelFormatCode !== null) { - $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); - } - } - ++$testCurrentIndex; - } - - return $datasetLabels; - } - - private function percentageSumCalculation($groupID, $seriesCount) - { - $sumValues = []; - // Adjust our values to a percentage value across all series in the group - for ($i = 0; $i < $seriesCount; ++$i) { - if ($i == 0) { - $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - } else { - $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - foreach ($nextValues as $k => $value) { - if (isset($sumValues[$k])) { - $sumValues[$k] += $value; - } else { - $sumValues[$k] = $value; - } - } - } - } - - return $sumValues; - } - - private function percentageAdjustValues($dataValues, $sumValues) - { - foreach ($dataValues as $k => $dataValue) { - $dataValues[$k] = $dataValue / $sumValues[$k] * 100; - } - - return $dataValues; - } - - private function getCaption($captionElement) - { - // Read any caption - $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; - // Test if we have a title caption to display - if ($caption !== null) { - // If we do, it could be a plain string or an array - if (is_array($caption)) { - // Implode an array to a plain string - $caption = implode('', $caption); - } - } - - return $caption; - } - - private function renderTitle(): void - { - $title = $this->getCaption($this->chart->getTitle()); - if ($title !== null) { - $this->graph->title->Set($title); - } - } - - private function renderLegend(): void - { - $legend = $this->chart->getLegend(); - if ($legend !== null) { - $legendPosition = $legend->getPosition(); - switch ($legendPosition) { - case 'r': - $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right - $this->graph->legend->SetColumns(1); - - break; - case 'l': - $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left - $this->graph->legend->SetColumns(1); - - break; - case 't': - $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top - - break; - case 'b': - $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom - - break; - default: - $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right - $this->graph->legend->SetColumns(1); - - break; - } - } else { - $this->graph->legend->Hide(); - } - } - - private function renderCartesianPlotArea($type = 'textlin'): void - { - $this->graph = new Graph(self::$width, self::$height); - $this->graph->SetScale($type); - - $this->renderTitle(); - - // Rotate for bar rather than column chart - $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); - $reverse = $rotation == 'bar'; - - $xAxisLabel = $this->chart->getXAxisLabel(); - if ($xAxisLabel !== null) { - $title = $this->getCaption($xAxisLabel); - if ($title !== null) { - $this->graph->xaxis->SetTitle($title, 'center'); - $this->graph->xaxis->title->SetMargin(35); - if ($reverse) { - $this->graph->xaxis->title->SetAngle(90); - $this->graph->xaxis->title->SetMargin(90); - } - } - } - - $yAxisLabel = $this->chart->getYAxisLabel(); - if ($yAxisLabel !== null) { - $title = $this->getCaption($yAxisLabel); - if ($title !== null) { - $this->graph->yaxis->SetTitle($title, 'center'); - if ($reverse) { - $this->graph->yaxis->title->SetAngle(0); - $this->graph->yaxis->title->SetMargin(-55); - } - } - } - } - - private function renderPiePlotArea(): void - { - $this->graph = new PieGraph(self::$width, self::$height); - - $this->renderTitle(); - } - - private function renderRadarPlotArea(): void - { - $this->graph = new RadarGraph(self::$width, self::$height); - $this->graph->SetScale('lin'); - - $this->renderTitle(); - } - - private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void - { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - if ($grouping == 'percentStacked') { - $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); - } else { - $sumValues = []; - } - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - - if ($grouping == 'percentStacked') { - $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); - } - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - $seriesPlot = new LinePlot($dataValues); - if ($combination) { - $seriesPlot->SetBarCenter(); - } - - if ($filled) { - $seriesPlot->SetFilled(true); - $seriesPlot->SetColor('black'); - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); - } else { - // Set the appropriate plot marker - $this->formatPointMarker($seriesPlot, $marker); - } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetLegend($dataLabel); - - $seriesPlots[] = $seriesPlot; - } - - if ($grouping == 'standard') { - $groupPlot = $seriesPlots; - } else { - $groupPlot = new AccLinePlot($seriesPlots); - } - $this->graph->Add($groupPlot); - } - - private function renderPlotBar($groupID, $dimensions = '2d'): void - { - $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); - // Rotate for bar rather than column chart - if (($groupID == 0) && ($rotation == 'bar')) { - $this->graph->Set90AndMargin(); - } - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); - // Rotate for bar rather than column chart - if ($rotation == 'bar') { - $datasetLabels = array_reverse($datasetLabels); - $this->graph->yaxis->SetPos('max'); - $this->graph->yaxis->SetLabelAlign('center', 'top'); - $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); - } - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - if ($grouping == 'percentStacked') { - $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); - } else { - $sumValues = []; - } - - // Loop through each data series in turn - for ($j = 0; $j < $seriesCount; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); - if ($grouping == 'percentStacked') { - $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); - } - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - // Reverse the $dataValues order for bar rather than column chart - if ($rotation == 'bar') { - $dataValues = array_reverse($dataValues); - } - $seriesPlot = new BarPlot($dataValues); - $seriesPlot->SetColor('black'); - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); - if ($dimensions == '3d') { - $seriesPlot->SetShadow(); - } - if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { - $dataLabel = ''; - } else { - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); - } - $seriesPlot->SetLegend($dataLabel); - - $seriesPlots[] = $seriesPlot; - } - // Reverse the plot order for bar rather than column chart - if (($rotation == 'bar') && ($grouping != 'percentStacked')) { - $seriesPlots = array_reverse($seriesPlots); - } - - if ($grouping == 'clustered') { - $groupPlot = new GroupBarPlot($seriesPlots); - } elseif ($grouping == 'standard') { - $groupPlot = new GroupBarPlot($seriesPlots); - } else { - $groupPlot = new AccBarPlot($seriesPlots); - if ($dimensions == '3d') { - $groupPlot->SetShadow(); - } - } - - $this->graph->Add($groupPlot); - } - - private function renderPlotScatter($groupID, $bubble): void - { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - - foreach ($dataValuesY as $k => $dataValueY) { - $dataValuesY[$k] = $k; - } - - $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); - if ($scatterStyle == 'lineMarker') { - $seriesPlot->SetLinkPoints(); - $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); - } elseif ($scatterStyle == 'smoothMarker') { - $spline = new Spline($dataValuesY, $dataValuesX); - [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); - $lplot = new LinePlot($splineDataX, $splineDataY); - $lplot->SetColor(self::$colourSet[self::$plotColour]); - - $this->graph->Add($lplot); - } - - if ($bubble) { - $this->formatPointMarker($seriesPlot, 'dot'); - $seriesPlot->mark->SetColor('black'); - $seriesPlot->mark->SetSize($bubbleSize); - } else { - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - $this->formatPointMarker($seriesPlot, $marker); - } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetLegend($dataLabel); - - $this->graph->Add($seriesPlot); - } - } - - private function renderPlotRadar($groupID): void - { - $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - - $dataValues = []; - foreach ($dataValuesY as $k => $dataValueY) { - $dataValues[$k] = implode(' ', array_reverse($dataValueY)); - } - $tmp = array_shift($dataValues); - $dataValues[] = $tmp; - $tmp = array_shift($dataValuesX); - $dataValuesX[] = $tmp; - - $this->graph->SetTitles(array_reverse($dataValues)); - - $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); - - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - if ($radarStyle == 'filled') { - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); - } - $this->formatPointMarker($seriesPlot, $marker); - $seriesPlot->SetLegend($dataLabel); - - $this->graph->Add($seriesPlot); - } - } - - private function renderPlotContour($groupID): void - { - $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - $dataValues = []; - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - - $dataValues[$i] = $dataValuesX; - } - $seriesPlot = new ContourPlot($dataValues); - - $this->graph->Add($seriesPlot); - } - - private function renderPlotStock($groupID): void - { - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); - - $dataValues = []; - // Loop through each data series in turn and build the plot arrays - foreach ($plotOrder as $i => $v) { - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); - foreach ($dataValuesX as $j => $dataValueX) { - $dataValues[$plotOrder[$i]][$j] = $dataValueX; - } - } - if (empty($dataValues)) { - return; - } - - $dataValuesPlot = []; - // Flatten the plot arrays to a single dimensional array to work with jpgraph - $jMax = count($dataValues[0]); - for ($j = 0; $j < $jMax; ++$j) { - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesPlot[] = $dataValues[$i][$j]; - } - } - - // Set the x-axis labels - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesPlot = new StockPlot($dataValuesPlot); - $seriesPlot->SetWidth(20); - - $this->graph->Add($seriesPlot); - } - - private function renderAreaChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, true, false, $dimensions); - } - } - - private function renderLineChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, false, false, $dimensions); - } - } - - private function renderBarChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotBar($i, $dimensions); - } - } - - private function renderScatterChart($groupCount): void - { - $this->renderCartesianPlotArea('linlin'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotScatter($i, false); - } - } - - private function renderBubbleChart($groupCount): void - { - $this->renderCartesianPlotArea('linlin'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotScatter($i, true); - } - } - - private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void - { - $this->renderPiePlotArea(); - - $iLimit = ($multiplePlots) ? $groupCount : 1; - for ($groupID = 0; $groupID < $iLimit; ++$groupID) { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - $datasetLabels = []; - if ($groupID == 0) { - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - } - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - // For pie charts, we only display the first series: doughnut charts generally display all series - $jLimit = ($multiplePlots) ? $seriesCount : 1; - // Loop through each data series in turn - for ($j = 0; $j < $jLimit; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - if ($dimensions == '3d') { - $seriesPlot = new PiePlot3D($dataValues); - } else { - if ($doughnut) { - $seriesPlot = new PiePlotC($dataValues); - } else { - $seriesPlot = new PiePlot($dataValues); - } - } - - if ($multiplePlots) { - $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); - } - - if ($doughnut) { - $seriesPlot->SetMidColor('white'); - } - - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - if (count($datasetLabels) > 0) { - $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); - } - if ($dimensions != '3d') { - $seriesPlot->SetGuideLines(false); - } - if ($j == 0) { - if ($exploded) { - $seriesPlot->ExplodeAll(); - } - $seriesPlot->SetLegends($datasetLabels); - } - - $this->graph->Add($seriesPlot); - } - } - } - - private function renderRadarChart($groupCount): void - { - $this->renderRadarPlotArea(); - - for ($groupID = 0; $groupID < $groupCount; ++$groupID) { - $this->renderPlotRadar($groupID); - } - } - - private function renderStockChart($groupCount): void - { - $this->renderCartesianPlotArea('intint'); - - for ($groupID = 0; $groupID < $groupCount; ++$groupID) { - $this->renderPlotStock($groupID); - } - } - - private function renderContourChart($groupCount, $dimensions): void - { - $this->renderCartesianPlotArea('intint'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotContour($i); - } - } - - private function renderCombinationChart($groupCount, $dimensions, $outputDestination) - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $dimensions = null; - $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); - switch ($chartType) { - case 'area3DChart': - $dimensions = '3d'; - // no break - case 'areaChart': - $this->renderPlotLine($i, true, true, $dimensions); - - break; - case 'bar3DChart': - $dimensions = '3d'; - // no break - case 'barChart': - $this->renderPlotBar($i, $dimensions); - - break; - case 'line3DChart': - $dimensions = '3d'; - // no break - case 'lineChart': - $this->renderPlotLine($i, false, true, $dimensions); - - break; - case 'scatterChart': - $this->renderPlotScatter($i, false); - - break; - case 'bubbleChart': - $this->renderPlotScatter($i, true); - - break; - default: - $this->graph = null; - - return false; - } - } - - $this->renderLegend(); - - $this->graph->Stroke($outputDestination); - - return true; - } - - public function render($outputDestination) - { - self::$plotColour = 0; - - $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); - - $dimensions = null; - if ($groupCount == 1) { - $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); - } else { - $chartTypes = []; - for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); - } - $chartTypes = array_unique($chartTypes); - if (count($chartTypes) == 1) { - $chartType = array_pop($chartTypes); - } elseif (count($chartTypes) == 0) { - echo 'Chart is not yet implemented
'; - - return false; - } else { - return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); - } - } - - switch ($chartType) { - case 'area3DChart': - $dimensions = '3d'; - // no break - case 'areaChart': - $this->renderAreaChart($groupCount, $dimensions); - - break; - case 'bar3DChart': - $dimensions = '3d'; - // no break - case 'barChart': - $this->renderBarChart($groupCount, $dimensions); - - break; - case 'line3DChart': - $dimensions = '3d'; - // no break - case 'lineChart': - $this->renderLineChart($groupCount, $dimensions); - - break; - case 'pie3DChart': - $dimensions = '3d'; - // no break - case 'pieChart': - $this->renderPieChart($groupCount, $dimensions, false, false); - - break; - case 'doughnut3DChart': - $dimensions = '3d'; - // no break - case 'doughnutChart': - $this->renderPieChart($groupCount, $dimensions, true, true); - - break; - case 'scatterChart': - $this->renderScatterChart($groupCount); - - break; - case 'bubbleChart': - $this->renderBubbleChart($groupCount); - - break; - case 'radarChart': - $this->renderRadarChart($groupCount); - - break; - case 'surface3DChart': - $dimensions = '3d'; - // no break - case 'surfaceChart': - $this->renderContourChart($groupCount, $dimensions); - - break; - case 'stockChart': - $this->renderStockChart($groupCount); - - break; - default: - echo $chartType . ' is not yet implemented
'; - - return false; - } - $this->renderLegend(); - - $this->graph->Stroke($outputDestination); - - return true; - } } diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php new file mode 100644 index 00000000..cb9b544b --- /dev/null +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -0,0 +1,852 @@ +graph = null; + $this->chart = $chart; + + self::$markSet = [ + 'diamond' => MARK_DIAMOND, + 'square' => MARK_SQUARE, + 'triangle' => MARK_UTRIANGLE, + 'x' => MARK_X, + 'star' => MARK_STAR, + 'dot' => MARK_FILLEDCIRCLE, + 'dash' => MARK_DTRIANGLE, + 'circle' => MARK_CIRCLE, + 'plus' => MARK_CROSS, + ]; + } + + /** + * This method should be overriden in descendants to do real JpGraph library initialization. + */ + abstract protected static function init(): void; + + private function formatPointMarker($seriesPlot, $markerID) + { + $plotMarkKeys = array_keys(self::$markSet); + if ($markerID === null) { + // Use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } elseif ($markerID !== 'none') { + // Use specified plot marker (if it exists) + if (isset(self::$markSet[$markerID])) { + $seriesPlot->mark->SetType(self::$markSet[$markerID]); + } else { + // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } + } else { + // Hide plot marker + $seriesPlot->mark->Hide(); + } + $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); + $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + + return $seriesPlot; + } + + private function formatDataSetLabels($groupID, $datasetLabels, $rotation = '') + { + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? ''; + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); + + $testCurrentIndex = 0; + foreach ($datasetLabels as $i => $datasetLabel) { + if (is_array($datasetLabel)) { + if ($rotation == 'bar') { + $datasetLabels[$i] = implode(' ', $datasetLabel); + } else { + $datasetLabel = array_reverse($datasetLabel); + $datasetLabels[$i] = implode("\n", $datasetLabel); + } + } else { + // Format labels according to any formatting code + if ($datasetLabelFormatCode !== null) { + $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); + } + } + ++$testCurrentIndex; + } + + return $datasetLabels; + } + + private function percentageSumCalculation($groupID, $seriesCount) + { + $sumValues = []; + // Adjust our values to a percentage value across all series in the group + for ($i = 0; $i < $seriesCount; ++$i) { + if ($i == 0) { + $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + } else { + $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + foreach ($nextValues as $k => $value) { + if (isset($sumValues[$k])) { + $sumValues[$k] += $value; + } else { + $sumValues[$k] = $value; + } + } + } + } + + return $sumValues; + } + + private function percentageAdjustValues($dataValues, $sumValues) + { + foreach ($dataValues as $k => $dataValue) { + $dataValues[$k] = $dataValue / $sumValues[$k] * 100; + } + + return $dataValues; + } + + private function getCaption($captionElement) + { + // Read any caption + $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; + // Test if we have a title caption to display + if ($caption !== null) { + // If we do, it could be a plain string or an array + if (is_array($caption)) { + // Implode an array to a plain string + $caption = implode('', $caption); + } + } + + return $caption; + } + + private function renderTitle(): void + { + $title = $this->getCaption($this->chart->getTitle()); + if ($title !== null) { + $this->graph->title->Set($title); + } + } + + private function renderLegend(): void + { + $legend = $this->chart->getLegend(); + if ($legend !== null) { + $legendPosition = $legend->getPosition(); + switch ($legendPosition) { + case 'r': + $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right + $this->graph->legend->SetColumns(1); + + break; + case 'l': + $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left + $this->graph->legend->SetColumns(1); + + break; + case 't': + $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top + + break; + case 'b': + $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom + + break; + default: + $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right + $this->graph->legend->SetColumns(1); + + break; + } + } else { + $this->graph->legend->Hide(); + } + } + + private function renderCartesianPlotArea($type = 'textlin'): void + { + $this->graph = new Graph(self::$width, self::$height); + $this->graph->SetScale($type); + + $this->renderTitle(); + + // Rotate for bar rather than column chart + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); + $reverse = $rotation == 'bar'; + + $xAxisLabel = $this->chart->getXAxisLabel(); + if ($xAxisLabel !== null) { + $title = $this->getCaption($xAxisLabel); + if ($title !== null) { + $this->graph->xaxis->SetTitle($title, 'center'); + $this->graph->xaxis->title->SetMargin(35); + if ($reverse) { + $this->graph->xaxis->title->SetAngle(90); + $this->graph->xaxis->title->SetMargin(90); + } + } + } + + $yAxisLabel = $this->chart->getYAxisLabel(); + if ($yAxisLabel !== null) { + $title = $this->getCaption($yAxisLabel); + if ($title !== null) { + $this->graph->yaxis->SetTitle($title, 'center'); + if ($reverse) { + $this->graph->yaxis->title->SetAngle(0); + $this->graph->yaxis->title->SetMargin(-55); + } + } + } + } + + private function renderPiePlotArea(): void + { + $this->graph = new PieGraph(self::$width, self::$height); + + $this->renderTitle(); + } + + private function renderRadarPlotArea(): void + { + $this->graph = new RadarGraph(self::$width, self::$height); + $this->graph->SetScale('lin'); + + $this->renderTitle(); + } + + private function renderPlotLine($groupID, $filled = false, $combination = false): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$i]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointMarker(); + + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + $seriesPlot = new LinePlot($dataValues); + if ($combination) { + $seriesPlot->SetBarCenter(); + } + + if ($filled) { + $seriesPlot->SetFilled(true); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + } else { + // Set the appropriate plot marker + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($index)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + + if ($grouping == 'standard') { + $groupPlot = $seriesPlots; + } else { + $groupPlot = new AccLinePlot($seriesPlots); + } + $this->graph->Add($groupPlot); + } + + private function renderPlotBar($groupID, $dimensions = '2d'): void + { + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); + // Rotate for bar rather than column chart + if (($groupID == 0) && ($rotation == 'bar')) { + $this->graph->Set90AndMargin(); + } + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation); + // Rotate for bar rather than column chart + if ($rotation == 'bar') { + $datasetLabels = array_reverse($datasetLabels); + $this->graph->yaxis->SetPos('max'); + $this->graph->yaxis->SetLabelAlign('center', 'top'); + $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); + } + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($j = 0; $j < $seriesCount; ++$j) { + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$j]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + // Reverse the $dataValues order for bar rather than column chart + if ($rotation == 'bar') { + $dataValues = array_reverse($dataValues); + } + $seriesPlot = new BarPlot($dataValues); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + if ($dimensions == '3d') { + $seriesPlot->SetShadow(); + } + if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { + $dataLabel = ''; + } else { + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); + } + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + // Reverse the plot order for bar rather than column chart + if (($rotation == 'bar') && ($grouping != 'percentStacked')) { + $seriesPlots = array_reverse($seriesPlots); + } + + if ($grouping == 'clustered') { + $groupPlot = new GroupBarPlot($seriesPlots); + } elseif ($grouping == 'standard') { + $groupPlot = new GroupBarPlot($seriesPlots); + } else { + $groupPlot = new AccBarPlot($seriesPlots); + if ($dimensions == '3d') { + $groupPlot->SetShadow(); + } + } + + $this->graph->Add($groupPlot); + } + + private function renderPlotScatter($groupID, $bubble): void + { + $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + foreach ($dataValuesY as $k => $dataValueY) { + $dataValuesY[$k] = $k; + } + + $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); + if ($scatterStyle == 'lineMarker') { + $seriesPlot->SetLinkPoints(); + $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); + } elseif ($scatterStyle == 'smoothMarker') { + $spline = new Spline($dataValuesY, $dataValuesX); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); + $lplot = new LinePlot($splineDataX, $splineDataY); + $lplot->SetColor(self::$colourSet[self::$plotColour]); + + $this->graph->Add($lplot); + } + + if ($bubble) { + $this->formatPointMarker($seriesPlot, 'dot'); + $seriesPlot->mark->SetColor('black'); + $seriesPlot->mark->SetSize($bubbleSize); + } else { + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotRadar($groupID): void + { + $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + $dataValues = []; + foreach ($dataValuesY as $k => $dataValueY) { + $dataValues[$k] = implode(' ', array_reverse($dataValueY)); + } + $tmp = array_shift($dataValues); + $dataValues[] = $tmp; + $tmp = array_shift($dataValuesX); + $dataValuesX[] = $tmp; + + $this->graph->SetTitles(array_reverse($dataValues)); + + $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); + + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if ($radarStyle == 'filled') { + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); + } + $this->formatPointMarker($seriesPlot, $marker); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotContour($groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + $dataValues = []; + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + $dataValues[$i] = $dataValuesX; + } + $seriesPlot = new ContourPlot($dataValues); + + $this->graph->Add($seriesPlot); + } + + private function renderPlotStock($groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); + + $dataValues = []; + // Loop through each data series in turn and build the plot arrays + foreach ($plotOrder as $i => $v) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v); + if ($dataValuesX === false) { + continue; + } + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); + foreach ($dataValuesX as $j => $dataValueX) { + $dataValues[$plotOrder[$i]][$j] = $dataValueX; + } + } + if (empty($dataValues)) { + return; + } + + $dataValuesPlot = []; + // Flatten the plot arrays to a single dimensional array to work with jpgraph + $jMax = count($dataValues[0]); + for ($j = 0; $j < $jMax; ++$j) { + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesPlot[] = $dataValues[$i][$j] ?? null; + } + } + + // Set the x-axis labels + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesPlot = new StockPlot($dataValuesPlot); + $seriesPlot->SetWidth(20); + + $this->graph->Add($seriesPlot); + } + + private function renderAreaChart($groupCount): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, true, false); + } + } + + private function renderLineChart($groupCount): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, false, false); + } + } + + private function renderBarChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotBar($i, $dimensions); + } + } + + private function renderScatterChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, false); + } + } + + private function renderBubbleChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, true); + } + } + + private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void + { + $this->renderPiePlotArea(); + + $iLimit = ($multiplePlots) ? $groupCount : 1; + for ($groupID = 0; $groupID < $iLimit; ++$groupID) { + $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + $datasetLabels = []; + if ($groupID == 0) { + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + } + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + // For pie charts, we only display the first series: doughnut charts generally display all series + $jLimit = ($multiplePlots) ? $seriesCount : 1; + // Loop through each data series in turn + for ($j = 0; $j < $jLimit; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + if ($dimensions == '3d') { + $seriesPlot = new PiePlot3D($dataValues); + } else { + if ($doughnut) { + $seriesPlot = new PiePlotC($dataValues); + } else { + $seriesPlot = new PiePlot($dataValues); + } + } + + if ($multiplePlots) { + $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); + } + + if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) { + $seriesPlot->SetMidColor('white'); + } + + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if (count($datasetLabels) > 0) { + $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); + } + if ($dimensions != '3d') { + $seriesPlot->SetGuideLines(false); + } + if ($j == 0) { + if ($exploded) { + $seriesPlot->ExplodeAll(); + } + $seriesPlot->SetLegends($datasetLabels); + } + + $this->graph->Add($seriesPlot); + } + } + } + + private function renderRadarChart($groupCount): void + { + $this->renderRadarPlotArea(); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotRadar($groupID); + } + } + + private function renderStockChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotStock($groupID); + } + } + + private function renderContourChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotContour($i); + } + } + + private function renderCombinationChart($groupCount, $outputDestination) + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $dimensions = null; + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + switch ($chartType) { + case 'area3DChart': + case 'areaChart': + $this->renderPlotLine($i, true, true); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderPlotBar($i, $dimensions); + + break; + case 'line3DChart': + case 'lineChart': + $this->renderPlotLine($i, false, true); + + break; + case 'scatterChart': + $this->renderPlotScatter($i, false); + + break; + case 'bubbleChart': + $this->renderPlotScatter($i, true); + + break; + default: + $this->graph = null; + + return false; + } + } + + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } + + public function render($outputDestination) + { + self::$plotColour = 0; + + $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); + + $dimensions = null; + if ($groupCount == 1) { + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = array_pop($chartTypes); + } elseif (count($chartTypes) == 0) { + echo 'Chart is not yet implemented
'; + + return false; + } else { + return $this->renderCombinationChart($groupCount, $outputDestination); + } + } + + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderAreaChart($groupCount); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderBarChart($groupCount, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderLineChart($groupCount); + + break; + case 'pie3DChart': + $dimensions = '3d'; + // no break + case 'pieChart': + $this->renderPieChart($groupCount, $dimensions, false, false); + + break; + case 'doughnut3DChart': + $dimensions = '3d'; + // no break + case 'doughnutChart': + $this->renderPieChart($groupCount, $dimensions, true, true); + + break; + case 'scatterChart': + $this->renderScatterChart($groupCount); + + break; + case 'bubbleChart': + $this->renderBubbleChart($groupCount); + + break; + case 'radarChart': + $this->renderRadarChart($groupCount); + + break; + case 'surface3DChart': + case 'surfaceChart': + $this->renderContourChart($groupCount); + + break; + case 'stockChart': + $this->renderStockChart($groupCount); + + break; + default: + echo $chartType . ' is not yet implemented
'; + + return false; + } + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } +} diff --git a/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php new file mode 100644 index 00000000..e1f0f90a --- /dev/null +++ b/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php @@ -0,0 +1,36 @@ +layout; } diff --git a/src/PhpSpreadsheet/Chart/TrendLine.php b/src/PhpSpreadsheet/Chart/TrendLine.php new file mode 100644 index 00000000..75a5896c --- /dev/null +++ b/src/PhpSpreadsheet/Chart/TrendLine.php @@ -0,0 +1,226 @@ +setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); + } + + public function getTrendLineType(): string + { + return $this->trendLineType; + } + + public function setTrendLineType(string $trendLineType): self + { + $this->trendLineType = $trendLineType; + + return $this; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setOrder(int $order): self + { + $this->order = $order; + + return $this; + } + + public function getPeriod(): int + { + return $this->period; + } + + public function setPeriod(int $period): self + { + $this->period = $period; + + return $this; + } + + public function getDispRSqr(): bool + { + return $this->dispRSqr; + } + + public function setDispRSqr(bool $dispRSqr): self + { + $this->dispRSqr = $dispRSqr; + + return $this; + } + + public function getDispEq(): bool + { + return $this->dispEq; + } + + public function setDispEq(bool $dispEq): self + { + $this->dispEq = $dispEq; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getBackward(): float + { + return $this->backward; + } + + public function setBackward(float $backward): self + { + $this->backward = $backward; + + return $this; + } + + public function getForward(): float + { + return $this->forward; + } + + public function setForward(float $forward): self + { + $this->forward = $forward; + + return $this; + } + + public function getIntercept(): float + { + return $this->intercept; + } + + public function setIntercept(float $intercept): self + { + $this->intercept = $intercept; + + return $this; + } + + public function setTrendLineProperties( + ?string $trendLineType = null, + ?int $order = 0, + ?int $period = 0, + ?bool $dispRSqr = false, + ?bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ): self { + if (!empty($trendLineType)) { + $this->setTrendLineType($trendLineType); + } + if ($order !== null) { + $this->setOrder($order); + } + if ($period !== null) { + $this->setPeriod($period); + } + if ($dispRSqr !== null) { + $this->setDispRSqr($dispRSqr); + } + if ($dispEq !== null) { + $this->setDispEq($dispEq); + } + if ($backward !== null) { + $this->setBackward($backward); + } + if ($forward !== null) { + $this->setForward($forward); + } + if ($intercept !== null) { + $this->setIntercept($intercept); + } + if ($name !== null) { + $this->setName($name); + } + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 20fccf48..6183e733 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -91,10 +91,8 @@ class Cells * Whether the collection holds a cell for the given coordinate. * * @param string $cellCoordinate Coordinate of the cell to check - * - * @return bool */ - public function has($cellCoordinate) + public function has($cellCoordinate): bool { return ($cellCoordinate === $this->currentCoordinate) || isset($this->index[$cellCoordinate]); } @@ -103,10 +101,8 @@ class Cells * Add or update a cell in the collection. * * @param Cell $cell Cell to update - * - * @return Cell */ - public function update(Cell $cell) + public function update(Cell $cell): Cell { return $this->add($cell->getCoordinate(), $cell); } @@ -165,10 +161,8 @@ class Cells /** * Return the column coordinate of the currently active cell object. - * - * @return string */ - public function getCurrentColumn() + public function getCurrentColumn(): string { sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); @@ -177,10 +171,8 @@ class Cells /** * Return the row coordinate of the currently active cell object. - * - * @return int */ - public function getCurrentRow() + public function getCurrentRow(): int { sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); @@ -276,7 +268,9 @@ class Cells */ private function getUniqueID() { - return Settings::getCache() instanceof Memory + $cacheType = Settings::getCache(); + + return ($cacheType instanceof Memory\SimpleCache1 || $cacheType instanceof Memory\SimpleCache3) ? random_bytes(7) . ':' : uniqid('phpspreadsheet.', true) . '.'; } @@ -471,7 +465,7 @@ class Cells */ private function getAllCacheKeys() { - foreach ($this->getCoordinates() as $coordinate) { + foreach ($this->index as $coordinate => $value) { yield $this->cachePrefix . $coordinate; } } diff --git a/src/PhpSpreadsheet/Collection/CellsFactory.php b/src/PhpSpreadsheet/Collection/CellsFactory.php index 26f18dfc..b3833bd8 100644 --- a/src/PhpSpreadsheet/Collection/CellsFactory.php +++ b/src/PhpSpreadsheet/Collection/CellsFactory.php @@ -12,9 +12,8 @@ abstract class CellsFactory * * @param Worksheet $worksheet Enable cell caching for this worksheet * - * @return Cells * */ - public static function getInstance(Worksheet $worksheet) + public static function getInstance(Worksheet $worksheet): Cells { return new Cells($worksheet, Settings::getCache()); } diff --git a/src/PhpSpreadsheet/Collection/Memory.php b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php similarity index 93% rename from src/PhpSpreadsheet/Collection/Memory.php rename to src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php index 2690ab7d..a0eb6ec2 100644 --- a/src/PhpSpreadsheet/Collection/Memory.php +++ b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php @@ -1,6 +1,6 @@ cache = []; + + return true; + } + + /** + * @param string $key + */ + public function delete($key): bool + { + unset($this->cache[$key]); + + return true; + } + + /** + * @param iterable $keys + */ + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + /** + * @param string $key + * @param mixed $default + */ + public function get($key, $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + /** + * @param iterable $keys + * @param mixed $default + */ + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + /** + * @param string $key + */ + public function has($key): bool + { + return array_key_exists($key, $this->cache); + } + + /** + * @param string $key + * @param mixed $value + * @param null|DateInterval|int $ttl + */ + public function set($key, $value, $ttl = null): bool + { + $this->cache[$key] = $value; + + return true; + } + + /** + * @param iterable $values + * @param null|DateInterval|int $ttl + */ + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/src/PhpSpreadsheet/DefinedName.php b/src/PhpSpreadsheet/DefinedName.php index 3b874b43..464fa8e3 100644 --- a/src/PhpSpreadsheet/DefinedName.php +++ b/src/PhpSpreadsheet/DefinedName.php @@ -150,7 +150,7 @@ abstract class DefinedName // New title $newTitle = $this->name; - ReferenceHelper::getInstance()->updateNamedFormulas($this->worksheet->getParent(), $oldTitle, $newTitle); + ReferenceHelper::getInstance()->updateNamedFormulae($this->worksheet->getParent(), $oldTitle, $newTitle); } return $this; diff --git a/src/PhpSpreadsheet/Helper/Dimension.php b/src/PhpSpreadsheet/Helper/Dimension.php index 4e3e6680..ff07ce5b 100644 --- a/src/PhpSpreadsheet/Helper/Dimension.php +++ b/src/PhpSpreadsheet/Helper/Dimension.php @@ -55,10 +55,22 @@ class Dimension */ protected $unit; + /** + * Phpstan bug has been fixed; this function allows us to + * pass Phpstan whether fixed or not. + * + * @param mixed $value + */ + private static function stanBugFixed($value): array + { + return is_array($value) ? $value : [null, null]; + } + public function __construct(string $dimension) { - [$size, $unit] = sscanf($dimension, '%[1234567890.]%s'); + [$size, $unit] = self::stanBugFixed(sscanf($dimension, '%[1234567890.]%s')); $unit = strtolower(trim($unit ?? '')); + $size = (float) $size; // If a UoM is specified, then convert the size to pixels for internal storage if (isset(self::ABSOLUTE_UNITS[$unit])) { diff --git a/src/PhpSpreadsheet/Helper/Html.php b/src/PhpSpreadsheet/Helper/Html.php index 632efebc..b8ed386a 100644 --- a/src/PhpSpreadsheet/Helper/Html.php +++ b/src/PhpSpreadsheet/Helper/Html.php @@ -12,7 +12,7 @@ use PhpOffice\PhpSpreadsheet\Style\Font; class Html { - protected static $colourMap = [ + private const COLOUR_MAP = [ 'aliceblue' => 'f0f8ff', 'antiquewhite' => 'faebd7', 'antiquewhite1' => 'ffefdb', @@ -532,25 +532,34 @@ class Html 'yellowgreen' => '9acd32', ]; - protected $face; + /** @var ?string */ + private $face; - protected $size; + /** @var ?string */ + private $size; - protected $color; + /** @var ?string */ + private $color; - protected $bold = false; + /** @var bool */ + private $bold = false; - protected $italic = false; + /** @var bool */ + private $italic = false; - protected $underline = false; + /** @var bool */ + private $underline = false; - protected $superscript = false; + /** @var bool */ + private $superscript = false; - protected $subscript = false; + /** @var bool */ + private $subscript = false; - protected $strikethrough = false; + /** @var bool */ + private $strikethrough = false; - protected $startTagCallbacks = [ + private const START_TAG_CALLBACKS = [ 'font' => 'startFontTag', 'b' => 'startBoldTag', 'strong' => 'startBoldTag', @@ -563,7 +572,7 @@ class Html 'sub' => 'startSubscriptTag', ]; - protected $endTagCallbacks = [ + private const END_TAG_CALLBACKS = [ 'font' => 'endFontTag', 'b' => 'endBoldTag', 'strong' => 'endBoldTag', @@ -584,16 +593,18 @@ class Html 'h6' => 'breakTag', ]; - protected $stack = []; + /** @var array */ + private $stack = []; - protected $stringData = ''; + /** @var string */ + private $stringData = ''; /** * @var RichText */ - protected $richTextObject; + private $richTextObject; - protected function initialise(): void + private function initialise(): void { $this->face = $this->size = $this->color = null; $this->bold = $this->italic = $this->underline = $this->superscript = $this->subscript = $this->strikethrough = false; @@ -633,7 +644,7 @@ class Html return $this->richTextObject; } - protected function cleanWhitespace(): void + private function cleanWhitespace(): void { foreach ($this->richTextObject->getRichTextElements() as $key => $element) { $text = $element->getText(); @@ -647,7 +658,7 @@ class Html } } - protected function buildTextRun(): void + private function buildTextRun(): void { $text = $this->stringData; if (trim($text) === '') { @@ -655,37 +666,40 @@ class Html } $richtextRun = $this->richTextObject->createTextRun($this->stringData); - if ($this->face) { - $richtextRun->getFont()->setName($this->face); - } - if ($this->size) { - $richtextRun->getFont()->setSize($this->size); - } - if ($this->color) { - $richtextRun->getFont()->setColor(new Color('ff' . $this->color)); - } - if ($this->bold) { - $richtextRun->getFont()->setBold(true); - } - if ($this->italic) { - $richtextRun->getFont()->setItalic(true); - } - if ($this->underline) { - $richtextRun->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - } - if ($this->superscript) { - $richtextRun->getFont()->setSuperscript(true); - } - if ($this->subscript) { - $richtextRun->getFont()->setSubscript(true); - } - if ($this->strikethrough) { - $richtextRun->getFont()->setStrikethrough(true); + $font = $richtextRun->getFont(); + if ($font !== null) { + if ($this->face) { + $font->setName($this->face); + } + if ($this->size) { + $font->setSize($this->size); + } + if ($this->color) { + $font->setColor(new Color('ff' . $this->color)); + } + if ($this->bold) { + $font->setBold(true); + } + if ($this->italic) { + $font->setItalic(true); + } + if ($this->underline) { + $font->setUnderline(Font::UNDERLINE_SINGLE); + } + if ($this->superscript) { + $font->setSuperscript(true); + } + if ($this->subscript) { + $font->setSubscript(true); + } + if ($this->strikethrough) { + $font->setStrikethrough(true); + } } $this->stringData = ''; } - protected function rgbToColour(string $rgbValue): string + private function rgbToColour(string $rgbValue): string { preg_match_all('/\d+/', $rgbValue, $values); foreach ($values[0] as &$value) { @@ -697,100 +711,103 @@ class Html public static function colourNameLookup(string $colorName): string { - return self::$colourMap[$colorName] ?? ''; + return self::COLOUR_MAP[$colorName] ?? ''; } - protected function startFontTag($tag): void + private function startFontTag(DOMElement $tag): void { - foreach ($tag->attributes as $attribute) { - $attributeName = strtolower($attribute->name); - $attributeValue = $attribute->value; + $attrs = $tag->attributes; + if ($attrs !== null) { + foreach ($attrs as $attribute) { + $attributeName = strtolower($attribute->name); + $attributeValue = $attribute->value; - if ($attributeName == 'color') { - if (preg_match('/rgb\s*\(/', $attributeValue)) { - $this->$attributeName = $this->rgbToColour($attributeValue); - } elseif (strpos(trim($attributeValue), '#') === 0) { - $this->$attributeName = ltrim($attributeValue, '#'); + if ($attributeName == 'color') { + if (preg_match('/rgb\s*\(/', $attributeValue)) { + $this->$attributeName = $this->rgbToColour($attributeValue); + } elseif (strpos(trim($attributeValue), '#') === 0) { + $this->$attributeName = ltrim($attributeValue, '#'); + } else { + $this->$attributeName = static::colourNameLookup($attributeValue); + } } else { - $this->$attributeName = static::colourNameLookup($attributeValue); + $this->$attributeName = $attributeValue; } - } else { - $this->$attributeName = $attributeValue; } } } - protected function endFontTag(): void + private function endFontTag(): void { $this->face = $this->size = $this->color = null; } - protected function startBoldTag(): void + private function startBoldTag(): void { $this->bold = true; } - protected function endBoldTag(): void + private function endBoldTag(): void { $this->bold = false; } - protected function startItalicTag(): void + private function startItalicTag(): void { $this->italic = true; } - protected function endItalicTag(): void + private function endItalicTag(): void { $this->italic = false; } - protected function startUnderlineTag(): void + private function startUnderlineTag(): void { $this->underline = true; } - protected function endUnderlineTag(): void + private function endUnderlineTag(): void { $this->underline = false; } - protected function startSubscriptTag(): void + private function startSubscriptTag(): void { $this->subscript = true; } - protected function endSubscriptTag(): void + private function endSubscriptTag(): void { $this->subscript = false; } - protected function startSuperscriptTag(): void + private function startSuperscriptTag(): void { $this->superscript = true; } - protected function endSuperscriptTag(): void + private function endSuperscriptTag(): void { $this->superscript = false; } - protected function startStrikethruTag(): void + private function startStrikethruTag(): void { $this->strikethrough = true; } - protected function endStrikethruTag(): void + private function endStrikethruTag(): void { $this->strikethrough = false; } - protected function breakTag(): void + private function breakTag(): void { $this->stringData .= "\n"; } - protected function parseTextNode(DOMText $textNode): void + private function parseTextNode(DOMText $textNode): void { $domText = (string) preg_replace( '/\s+/u', @@ -804,7 +821,7 @@ class Html /** * @param string $callbackTag */ - protected function handleCallback(DOMElement $element, $callbackTag, array $callbacks): void + private function handleCallback(DOMElement $element, $callbackTag, array $callbacks): void { if (isset($callbacks[$callbackTag])) { $elementHandler = $callbacks[$callbackTag]; @@ -815,20 +832,20 @@ class Html } } - protected function parseElementNode(DOMElement $element): void + private function parseElementNode(DOMElement $element): void { $callbackTag = strtolower($element->nodeName); $this->stack[] = $callbackTag; - $this->handleCallback($element, $callbackTag, $this->startTagCallbacks); + $this->handleCallback($element, $callbackTag, self::START_TAG_CALLBACKS); $this->parseElements($element); array_pop($this->stack); - $this->handleCallback($element, $callbackTag, $this->endTagCallbacks); + $this->handleCallback($element, $callbackTag, self::END_TAG_CALLBACKS); } - protected function parseElements(DOMNode $element): void + private function parseElements(DOMNode $element): void { foreach ($element->childNodes as $child) { if ($child instanceof DOMText) { diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index 8ce37003..a0063bd3 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Helper; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\IWriter; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -76,6 +77,11 @@ class Sample { // Populate samples $baseDir = realpath(__DIR__ . '/../../../samples'); + if ($baseDir === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException('realpath returned false'); + // @codeCoverageIgnoreEnd + } $directory = new RecursiveDirectoryIterator($baseDir); $iterator = new RecursiveIteratorIterator($directory); $regex = new RegexIterator($iterator, '/^.+\.php$/', RecursiveRegexIterator::GET_MATCH); @@ -83,8 +89,13 @@ class Sample $files = []; foreach ($regex as $file) { $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); + if (is_array($file)) { + // @codeCoverageIgnoreStart + throw new RuntimeException('str_replace returned array'); + // @codeCoverageIgnoreEnd + } $info = pathinfo($file); - $category = str_replace('_', ' ', $info['dirname']); + $category = str_replace('_', ' ', $info['dirname'] ?? ''); $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); if (!in_array($category, ['.', 'boostrap', 'templates'])) { if (!isset($files[$category])) { @@ -171,17 +182,49 @@ class Sample public function getTemporaryFilename($extension = 'xlsx') { $temporaryFilename = tempnam($this->getTemporaryFolder(), 'phpspreadsheet-'); + if ($temporaryFilename === false) { + // @codeCoverageIgnoreStart + throw new RuntimeException('tempnam returned false'); + // @codeCoverageIgnoreEnd + } unlink($temporaryFilename); return $temporaryFilename . '.' . $extension; } - public function log($message): void + public function log(string $message): void { $eol = $this->isCli() ? PHP_EOL : '
'; echo date('H:i:s ') . $message . $eol; } + public function titles(string $category, string $functionName, ?string $description = null): void + { + $this->log(sprintf('%s Functions:', $category)); + $description === null + ? $this->log(sprintf('Function: %s()', rtrim($functionName, '()'))) + : $this->log(sprintf('Function: %s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + } + + public function displayGrid(array $matrix): void + { + $renderer = new TextGrid($matrix, $this->isCli()); + echo $renderer->render(); + } + + public function logCalculationResult( + Worksheet $worksheet, + string $functionName, + string $formulaCell, + ?string $descriptionCell = null + ): void { + if ($descriptionCell !== null) { + $this->log($worksheet->getCell($descriptionCell)->getValue()); + } + $this->log($worksheet->getCell($formulaCell)->getValue()); + $this->log(sprintf('%s() Result is ', $functionName) . $worksheet->getCell($formulaCell)->getCalculatedValue()); + } + /** * Log ending notes. */ diff --git a/src/PhpSpreadsheet/Helper/TextGrid.php b/src/PhpSpreadsheet/Helper/TextGrid.php new file mode 100644 index 00000000..acb9ae60 --- /dev/null +++ b/src/PhpSpreadsheet/Helper/TextGrid.php @@ -0,0 +1,139 @@ +rows = array_keys($matrix); + $this->columns = array_keys($matrix[$this->rows[0]]); + + $matrix = array_values($matrix); + array_walk( + $matrix, + function (&$row): void { + $row = array_values($row); + } + ); + + $this->matrix = $matrix; + $this->isCli = $isCli; + } + + public function render(): string + { + $this->gridDisplay = $this->isCli ? '' : ''; + + $maxRow = max($this->rows); + $maxRowLength = strlen((string) $maxRow) + 1; + $columnWidths = $this->getColumnWidths($this->matrix); + + $this->renderColumnHeader($maxRowLength, $columnWidths); + $this->renderRows($maxRowLength, $columnWidths); + $this->renderFooter($maxRowLength, $columnWidths); + + $this->gridDisplay .= $this->isCli ? '' : ''; + + return $this->gridDisplay; + } + + private function renderRows(int $maxRowLength, array $columnWidths): void + { + foreach ($this->matrix as $row => $rowData) { + $this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' '; + $this->renderCells($rowData, $columnWidths); + $this->gridDisplay .= '|' . PHP_EOL; + } + } + + private function renderCells(array $rowData, array $columnWidths): void + { + foreach ($rowData as $column => $cell) { + $cell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell); + $this->gridDisplay .= '| '; + $this->gridDisplay .= str_pad($cell, $columnWidths[$column] + 1, ' '); + } + } + + private function renderColumnHeader(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1); + } + $this->gridDisplay .= '+' . PHP_EOL; + + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' '); + } + $this->gridDisplay .= '|' . PHP_EOL; + + $this->renderFooter($maxRowLength, $columnWidths); + } + + private function renderFooter(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-'; + $this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-'); + } + $this->gridDisplay .= '+' . PHP_EOL; + } + + private function getColumnWidths(array $matrix): array + { + $columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix); + $columnWidths = []; + for ($column = 0; $column < $columnCount; ++$column) { + $columnWidths[] = $this->getColumnWidth(array_column($this->matrix, $column)); + } + + return $columnWidths; + } + + private function getColumnWidth(array $columnData): int + { + $columnWidth = 0; + $columnData = array_values($columnData); + + foreach ($columnData as $columnValue) { + if (is_string($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue)); + } elseif (is_bool($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue ? 'TRUE' : 'FALSE')); + } + + $columnWidth = max($columnWidth, strlen((string) $columnWidth)); + } + + return $columnWidth; + } +} diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index ca087e61..1dcb0a12 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -363,7 +363,7 @@ class Gnumeric extends BaseReader if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos((string) $mergeCells, ':') !== false) { - $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 4edf3cf8..76f128e0 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -201,7 +201,7 @@ class Html extends BaseReader /** * Loads Spreadsheet from file. */ - protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + public function loadSpreadsheetFromFile(string $filename): Spreadsheet { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -632,16 +632,6 @@ class Html extends BaseReader } } - /** - * Make sure mb_convert_encoding returns string. - * - * @param mixed $result - */ - private static function ensureString($result): string - { - return is_string($result) ? $result : ''; - } - /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. * @@ -660,8 +650,14 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scanFile($filename), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scanFile($filename); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -672,6 +668,11 @@ class Html extends BaseReader return $this->loadDocument($dom, $spreadsheet); } + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + /** * Spreadsheet from content. * @@ -683,8 +684,14 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scan($content); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 7e776ab7..e3de4731 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -20,6 +20,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; use XMLReader; use ZipArchive; @@ -759,7 +760,7 @@ class Ods extends BaseReader } $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } diff --git a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index 8155b838..40008d01 100644 --- a/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -52,7 +52,7 @@ class XmlScanner public static function threadSafeLibxmlDisableEntityLoaderAvailability() { - if (PHP_MAJOR_VERSION == 7) { + if (PHP_MAJOR_VERSION === 7) { switch (PHP_MINOR_VERSION) { case 2: return PHP_RELEASE_VERSION >= 1; diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 9f8a3ace..a8de5228 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -4585,7 +4585,7 @@ class Xls extends BaseReader (strpos($cellRangeAddress, ':') !== false) && ($this->includeCellRangeFiltered($cellRangeAddress)) ) { - $this->phpSheet->mergeCells($cellRangeAddress); + $this->phpSheet->mergeCells($cellRangeAddress, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } @@ -6999,7 +6999,7 @@ class Xls extends BaseReader } break; - // Unknown cases // don't know how to deal with + // Unknown cases // don't know how to deal with default: throw new Exception('Unrecognized token ' . sprintf('%02X', $id) . ' in formula'); diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php index 743d9387..15d0b733 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF5.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BIFF5 { - protected static $map = [ + private const BIFF5_COLOR_MAP = [ 0x08 => '000000', 0x09 => 'FFFFFF', 0x0A => 'FF0000', @@ -72,10 +72,6 @@ class BIFF5 */ public static function lookup($color) { - if (isset(self::$map[$color])) { - return ['rgb' => self::$map[$color]]; - } - - return ['rgb' => '000000']; + return ['rgb' => self::BIFF5_COLOR_MAP[$color] ?? '000000']; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php index 5c109fb0..019ec79e 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BIFF8.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BIFF8 { - protected static $map = [ + private const BIFF8_COLOR_MAP = [ 0x08 => '000000', 0x09 => 'FFFFFF', 0x0A => 'FF0000', @@ -72,10 +72,6 @@ class BIFF8 */ public static function lookup($color) { - if (isset(self::$map[$color])) { - return ['rgb' => self::$map[$color]]; - } - - return ['rgb' => '000000']; + return ['rgb' => self::BIFF8_COLOR_MAP[$color] ?? '000000']; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php b/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php index 90d50e33..b6a96af8 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color/BuiltIn.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls\Color; class BuiltIn { - protected static $map = [ + private const BUILTIN_COLOR_MAP = [ 0x00 => '000000', 0x01 => 'FFFFFF', 0x02 => 'FF0000', @@ -26,10 +26,6 @@ class BuiltIn */ public static function lookup($color) { - if (isset(self::$map[$color])) { - return ['rgb' => self::$map[$color]]; - } - - return ['rgb' => '000000']; + return ['rgb' => self::BUILTIN_COLOR_MAP[$color] ?? '000000']; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php b/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php index 7daf7230..0b79366b 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php +++ b/src/PhpSpreadsheet/Reader/Xls/ErrorCode.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; class ErrorCode { - protected static $map = [ + private const ERROR_CODE_MAP = [ 0x00 => '#NULL!', 0x07 => '#DIV/0!', 0x0F => '#VALUE!', @@ -23,10 +23,6 @@ class ErrorCode */ public static function lookup($code) { - if (isset(self::$map[$code])) { - return self::$map[$code]; - } - - return false; + return self::ERROR_CODE_MAP[$code] ?? false; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/RC4.php b/src/PhpSpreadsheet/Reader/Xls/RC4.php index 691aca7c..b7c7c900 100644 --- a/src/PhpSpreadsheet/Reader/Xls/RC4.php +++ b/src/PhpSpreadsheet/Reader/Xls/RC4.php @@ -4,11 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; class RC4 { - // Context - protected $s = []; + /** @var int[] */ + protected $s = []; // Context + /** @var int */ protected $i = 0; + /** @var int */ protected $j = 0; /** diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 4cb487a6..8ae68503 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -32,6 +32,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Font; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Color; +use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; @@ -294,7 +295,7 @@ class Xlsx extends BaseReader return $worksheetInfo; } - private static function castToBoolean($c) + private static function castToBoolean(SimpleXMLElement $c): bool { $value = isset($c->v) ? (string) $c->v : null; if ($value == '0') { @@ -306,18 +307,25 @@ class Xlsx extends BaseReader return (bool) $c->v; } - private static function castToError($c) + private static function castToError(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } - private static function castToString($c) + private static function castToString(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } - private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType): void + /** + * @param mixed $value + * @param mixed $calculatedValue + */ + private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void { + if ($c === null) { + return; + } $attr = $c->f->attributes(); $cellDataType = 'f'; $value = "={$c->f}"; @@ -390,7 +398,7 @@ class Xlsx extends BaseReader $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); } - return $contents; + return ($contents === false) ? '' : $contents; } /** @@ -415,7 +423,7 @@ class Xlsx extends BaseReader [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; - $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); + $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS); $theme = null; $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { @@ -462,6 +470,7 @@ class Xlsx extends BaseReader $rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS); $propertyReader = new PropertyReader($this->securityScanner, $excel->getProperties()); + $chartDetails = []; foreach ($rels->Relationship as $relx) { $rel = self::getAttributes($relx); $relTarget = (string) $rel['Target']; @@ -480,7 +489,7 @@ class Xlsx extends BaseReader $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); break; - //Ribbon + //Ribbon case Namespaces::EXTENSIBILITY: $customUI = $relTarget; if ($customUI) { @@ -531,7 +540,7 @@ class Xlsx extends BaseReader } break; - // a vbaProject ? (: some macros) + // a vbaProject ? (: some macros) case Namespaces::VBA: $macros = $ele['Target']; @@ -908,7 +917,7 @@ class Xlsx extends BaseReader foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { $mergeRef = (string) $mergeCell['ref']; if (strpos($mergeRef, ':') !== false) { - $docSheet->mergeCells((string) $mergeCell['ref']); + $docSheet->mergeCells((string) $mergeCell['ref'], Worksheet::MERGE_CELL_CONTENT_HIDE); } } } @@ -923,13 +932,16 @@ class Xlsx extends BaseReader $xmlSheet->addChild('dataValidations'); } - foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) { + foreach ($xmlSheet->extLst->ext->children(Namespaces::DATA_VALIDATIONS1)->dataValidations->dataValidation as $item) { + $item = self::testSimpleXml($item); $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation'); foreach ($item->attributes() ?? [] as $attr) { $node->addAttribute($attr->getName(), $attr); } - $node->addAttribute('sqref', $item->children('xm', true)->sqref); - $node->addChild('formula1', $item->formula1->children('xm', true)->f); + $node->addAttribute('sqref', $item->children(Namespaces::DATA_VALIDATIONS2)->sqref); + if (isset($item->formula1)) { + $node->addChild('formula1', $item->formula1->children(Namespaces::DATA_VALIDATIONS2)->f); + } } } @@ -1145,7 +1157,7 @@ class Xlsx extends BaseReader } // Header/footer images - if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) { + if ($xmlSheet && $xmlSheet->legacyDrawingHF) { if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS); $vmlRelationship = ''; @@ -1272,6 +1284,7 @@ class Xlsx extends BaseReader if ($xmlDrawingChildren->oneCellAnchor) { foreach ($xmlDrawingChildren->oneCellAnchor as $oneCellAnchor) { + $oneCellAnchor = self::testSimpleXml($oneCellAnchor); if ($oneCellAnchor->pic->blipFill) { /** @var SimpleXMLElement $blip */ $blip = $oneCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; @@ -1279,8 +1292,6 @@ class Xlsx extends BaseReader $xfrm = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; /** @var SimpleXMLElement $outerShdw */ $outerShdw = $oneCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; - /** @var SimpleXMLElement $hlinkClick */ - $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $objDrawing->setName((string) self::getArrayItem(self::getAttributes($oneCellAnchor->pic->nvPicPr->cNvPr), 'name')); @@ -1357,11 +1368,11 @@ class Xlsx extends BaseReader } if ($xmlDrawingChildren->twoCellAnchor) { foreach ($xmlDrawingChildren->twoCellAnchor as $twoCellAnchor) { + $twoCellAnchor = self::testSimpleXml($twoCellAnchor); if ($twoCellAnchor->pic->blipFill) { $blip = $twoCellAnchor->pic->blipFill->children(Namespaces::DRAWINGML)->blip; $xfrm = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->xfrm; $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; - $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); /** @scrutinizer ignore-call */ $editAs = $twoCellAnchor->attributes(); @@ -1552,7 +1563,7 @@ class Xlsx extends BaseReader break; case '_xlnm.Print_Area': - $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; $newRangeSets = []; foreach ($rangeSets as $rangeSet) { [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); @@ -1607,7 +1618,7 @@ class Xlsx extends BaseReader if (strpos((string) $definedName, '!') !== false) { $range[0] = str_replace("''", "'", $range[0]); $range[0] = str_replace("'", '', $range[0]); - if ($worksheet = $excel->getSheetByName($range[0])) { + if ($worksheet = $excel->getSheetByName($range[0])) { // @phpstan-ignore-line $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); } else { $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope)); @@ -1628,7 +1639,7 @@ class Xlsx extends BaseReader // Need to split on a comma or a space if not in quotes, and extract the first part. $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $definedRange); // Extract sheet name - [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); + [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); // @phpstan-ignore-line $extractedSheetName = trim($extractedSheetName, "'"); // Locate sheet @@ -1675,7 +1686,7 @@ class Xlsx extends BaseReader if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { - $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); + $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); // @phpstan-ignore-line $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); // For oneCellAnchor or absoluteAnchor positioned charts, // toCoordinate is not in the data. Does it need to be calculated? @@ -1697,7 +1708,7 @@ class Xlsx extends BaseReader break; - // unparsed + // unparsed case 'application/vnd.ms-excel.controlproperties+xml': $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; @@ -1724,28 +1735,28 @@ class Xlsx extends BaseReader $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { if (is_object($is->r)) { - /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); } else { $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + $objFont = $objText->getFont() ?? new StyleFont(); if (isset($run->rPr->rFont)) { $attr = $run->rPr->rFont->attributes(); if (isset($attr['val'])) { - $objText->getFont()->setName((string) $attr['val']); + $objFont->setName((string) $attr['val']); } } if (isset($run->rPr->sz)) { $attr = $run->rPr->sz->attributes(); if (isset($attr['val'])) { - $objText->getFont()->setSize((float) $attr['val']); + $objFont->setSize((float) $attr['val']); } } if (isset($run->rPr->color)) { - $objText->getFont()->setColor(new Color($this->styleReader->readColor($run->rPr->color))); + $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color))); } if (isset($run->rPr->b)) { $attr = $run->rPr->b->attributes(); @@ -1753,7 +1764,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setBold(true); + $objFont->setBold(true); } } if (isset($run->rPr->i)) { @@ -1762,7 +1773,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setItalic(true); + $objFont->setItalic(true); } } if (isset($run->rPr->vertAlign)) { @@ -1770,19 +1781,19 @@ class Xlsx extends BaseReader if (isset($attr['val'])) { $vertAlign = strtolower((string) $attr['val']); if ($vertAlign == 'superscript') { - $objText->getFont()->setSuperscript(true); + $objFont->setSuperscript(true); } if ($vertAlign == 'subscript') { - $objText->getFont()->setSubscript(true); + $objFont->setSubscript(true); } } } if (isset($run->rPr->u)) { $attr = $run->rPr->u->attributes(); if (!isset($attr['val'])) { - $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); + $objFont->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); } else { - $objText->getFont()->setUnderline((string) $attr['val']); + $objFont->setUnderline((string) $attr['val']); } } if (isset($run->rPr->strike)) { @@ -1791,7 +1802,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setStrikethrough(true); + $objFont->setStrikethrough(true); } } } @@ -1844,22 +1855,30 @@ class Xlsx extends BaseReader } } + /** + * @param null|array|bool|SimpleXMLElement $array + * @param int|string $key + * + * @return mixed + */ private static function getArrayItem($array, $key = 0) { - return $array[$key] ?? null; + return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); } + /** + * @param null|SimpleXMLElement|string $base + * @param null|SimpleXMLElement|string $add + */ private static function dirAdd($base, $add): string { - $add = "$add"; - if (substr($add, 0, 4) === '/xl/') { - $add = substr($add, 4); - } + $base = (string) $base; + $add = (string) $add; return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } - private static function toCSSArray($style): array + private static function toCSSArray(string $style): array { $style = self::stripWhiteSpaceFromStyleString($style); @@ -1873,15 +1892,15 @@ class Xlsx extends BaseReader } if (strpos($item[1], 'pt') !== false) { $item[1] = str_replace('pt', '', $item[1]); - $item[1] = Font::fontSizeToPixels($item[1]); + $item[1] = (string) Font::fontSizeToPixels((int) $item[1]); } if (strpos($item[1], 'in') !== false) { $item[1] = str_replace('in', '', $item[1]); - $item[1] = Font::inchSizeToPixels($item[1]); + $item[1] = (string) Font::inchSizeToPixels((int) $item[1]); } if (strpos($item[1], 'cm') !== false) { $item[1] = str_replace('cm', '', $item[1]); - $item[1] = Font::centimeterSizeToPixels($item[1]); + $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]); } $style[$item[0]] = $item[1]; @@ -1890,11 +1909,14 @@ class Xlsx extends BaseReader return $style; } - public static function stripWhiteSpaceFromStyleString($string): string + public static function stripWhiteSpaceFromStyleString(string $string): string { return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); } + /** + * @param mixed $value + */ private static function boolean($value): bool { if (is_object($value)) { @@ -1963,7 +1985,7 @@ class Xlsx extends BaseReader return $returnValue; } - private function readFormControlProperties(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -1990,7 +2012,7 @@ class Xlsx extends BaseReader unset($unparsedCtrlProps); } - private function readPrinterSettings(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -2147,6 +2169,6 @@ class Xlsx extends BaseReader } } - return (count($array) === 64) ? $array : []; + return $array; } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index 77c36f7b..a6ab4d89 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -38,7 +38,7 @@ class AutoFilter } } - private function readAutoFilter($autoFilterRange, $xmlSheet): void + private function readAutoFilter(string $autoFilterRange, SimpleXMLElement $xmlSheet): void { $autoFilter = $this->parent->getAutoFilter(); $autoFilter->setRange($autoFilterRange); @@ -49,15 +49,15 @@ class AutoFilter if ($filterColumn->filters) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); $filters = $filterColumn->filters; - if ((isset($filters['blank'])) && ($filters['blank'] == 1)) { + if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Standard filters are always an OR join, so no join rule needs to be set // Entries can be either filter elements foreach ($filters->filter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Or Date Group elements @@ -79,7 +79,7 @@ class AutoFilter foreach ($filters->dateGroupItem as $dateGroupItem) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', [ 'year' => (string) $dateGroupItem['year'], 'month' => (string) $dateGroupItem['month'], @@ -93,9 +93,9 @@ class AutoFilter } } - private function readCustomAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->customFilters) { + if (isset($filterColumn, $filterColumn->customFilters)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); $customFilters = $filterColumn->customFilters; // Custom filters can an AND or an OR join; @@ -112,15 +112,15 @@ class AutoFilter } } - private function readDynamicAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->dynamicFilter) { + if (isset($filterColumn, $filterColumn->dynamicFilter)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); // We should only ever have one dynamic filter foreach ($filterColumn->dynamicFilter as $filterRule) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', (string) $filterRule['val'], (string) $filterRule['type'] )->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); @@ -134,9 +134,9 @@ class AutoFilter } } - private function readTopTenAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->top10) { + if (isset($filterColumn, $filterColumn->top10)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); // We should only ever have one top10 filter foreach ($filterColumn->top10 as $filterRule) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php b/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php index 1679f01f..2f146458 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; class BaseParserClass { - protected static function boolean($value) + /** + * @param mixed $value + */ + protected static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 55a150b7..c22334ca 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -4,16 +4,18 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; +use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; @@ -72,13 +74,34 @@ class Chart $rotX = $rotY = $rAngAx = $perspective = null; $xAxis = new Axis(); $yAxis = new Axis(); - $majorGridlines = $minorGridlines = null; + $autoTitleDeleted = null; + $chartNoFill = false; + $gradientArray = []; + $gradientLin = null; + $roundedCorners = false; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'spPr': + $possibleNoFill = $chartElementsC->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $chartNoFill = true; + } + + break; + case 'roundedCorners': + /** @var bool */ + $roundedCorners = self::getAttribute($chartElementsC->roundedCorners, 'val', 'boolean'); + + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { - $chartDetailsC = $chartDetails->children($this->cNamespace); + $chartDetails = Xlsx::testSimpleXml($chartDetails); switch ($chartDetailsKey) { + case 'autoTitleDeleted': + /** @var bool */ + $autoTitleDeleted = self::getAttribute($chartElementsC->chart->autoTitleDeleted, 'val', 'boolean'); + + break; case 'view3D': $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer'); @@ -89,40 +112,92 @@ class Chart case 'plotArea': $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; + $catAxRead = false; + $plotNoFill = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + $gradient = Xlsx::testSimpleXml($gradient); + /** @var float */ + $pos = self::getAttribute($gradient, 'pos', 'float'); + $gradientArray[] = [ + $pos / ChartProperties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = ChartProperties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + } + + break; case 'layout': $plotAreaLayout = $this->chartLayoutDetails($chartDetail); break; - case 'catAx': + case Axis::AXIS_TYPE_CATEGORY: + case Axis::AXIS_TYPE_DATE: + $catAxRead = true; if (isset($chartDetail->title)) { $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } + $xAxis->setAxisType($chartDetailKey); $this->readEffects($chartDetail, $xAxis); - - break; - case 'dateAx': - if (isset($chartDetail->title)) { - $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); + if (isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } } + if (isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); + } + $xAxis->setMajorGridlines($majorGridlines); + } + if (isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); + } + $xAxis->setMinorGridlines($minorGridlines); + } + $this->setAxisProperties($chartDetail, $xAxis); break; - case 'valAx': + case Axis::AXIS_TYPE_VALUE: $whichAxis = null; $axPos = null; if (isset($chartDetail->axPos)) { $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); - + } + if ($catAxRead) { + $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); + } elseif (!empty($axPos)) { switch ($axPos) { case 't': case 'b': $whichAxis = $xAxis; + $xAxis->setAxisType($chartDetailKey); break; case 'r': case 'l': $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); break; } @@ -144,18 +219,31 @@ class Chart } } $this->readEffects($chartDetail, $whichAxis); - if (isset($chartDetail->majorGridlines)) { + if ($whichAxis !== null && isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } + if ($whichAxis !== null && isset($chartDetail->majorGridlines)) { $majorGridlines = new GridLines(); if (isset($chartDetail->majorGridlines->spPr)) { $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); } + $whichAxis->setMajorGridlines($majorGridlines); } - if (isset($chartDetail->minorGridlines)) { + if ($whichAxis !== null && isset($chartDetail->minorGridlines)) { $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); if (isset($chartDetail->minorGridlines->spPr)) { $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); } + $whichAxis->setMinorGridlines($minorGridlines); } + $this->setAxisProperties($chartDetail, $whichAxis); break; case 'barChart': @@ -182,7 +270,7 @@ class Chart case 'doughnutChart': case 'pieChart': case 'pie3DChart': - $explosion = isset($chartDetail->ser->explosion); + $explosion = self::getAttribute($chartDetail->ser->explosion, 'val', 'string'); $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle("$explosion"); $plotSeries[] = $plotSer; @@ -226,7 +314,7 @@ class Chart break; case 'stockChart': $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); - $plotAttributes = $this->readChartAttributes($plotAreaLayout); + $plotAttributes = $this->readChartAttributes($chartDetail); break; } @@ -236,6 +324,12 @@ class Chart } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } break; case 'plotVisOnly': @@ -255,6 +349,7 @@ class Chart $legendLayout = null; $legendOverlay = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { case 'legendPos': $legendPos = self::getAttribute($chartDetail, 'val', 'string'); @@ -277,7 +372,14 @@ class Chart } } } - $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis, $majorGridlines, $minorGridlines); + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } + $chart->setRoundedCorners($roundedCorners); + if (is_bool($autoTitleDeleted)) { + $chart->setAutoTitleDeleted($autoTitleDeleted); + } if (is_int($rotX)) { $chart->setRotX($rotX); } @@ -299,14 +401,24 @@ class Chart $caption = []; $titleLayout = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { case 'tx': - $titleDetails = $chartDetail->rich->children($this->aNamespace); - foreach ($titleDetails as $titleKey => $titleDetail) { - switch ($titleKey) { - case 'p': - $titleDetailPart = $titleDetail->children($this->aNamespace); - $caption[] = $this->parseRichText($titleDetailPart); + if (isset($chartDetail->rich)) { + $titleDetails = $chartDetail->rich->children($this->aNamespace); + foreach ($titleDetails as $titleKey => $titleDetail) { + $titleDetail = Xlsx::testSimpleXml($titleDetail); + switch ($titleKey) { + case 'p': + $titleDetailPart = $titleDetail->children($this->aNamespace); + $caption[] = $this->parseRichText($titleDetailPart); + } + } + } elseif (isset($chartDetail->strRef->strCache)) { + foreach ($chartDetail->strRef->strCache->pt as $pt) { + if (isset($pt->v)) { + $caption[] = (string) $pt->v; + } } } @@ -332,6 +444,7 @@ class Chart } $layout = []; foreach ($details as $detailKey => $detail) { + $detail = Xlsx::testSimpleXml($detail); $layout[$detailKey] = self::getAttribute($detail, 'val', 'string'); } @@ -354,13 +467,18 @@ class Chart case 'ser': $marker = null; $seriesIndex = ''; - $srgbClr = null; - $lineWidth = null; + $fillColor = null; $pointSize = null; $noFill = false; - $schemeClr = ''; $bubble3D = false; + $dptColors = []; + $markerFillColor = null; + $markerBorderColor = null; + $lineStyle = null; + $labelLayout = null; + $trendLines = []; foreach ($seriesDetails as $seriesKey => $seriesDetail) { + $seriesDetail = Xlsx::testSimpleXml($seriesDetail); switch ($seriesKey) { case 'idx': $seriesIndex = self::getAttribute($seriesDetail, 'val', 'integer'); @@ -377,24 +495,82 @@ class Chart break; case 'spPr': $children = $seriesDetail->children($this->aNamespace); - $ln = $children->ln; - $lineWidth = self::getAttribute($ln, 'w', 'string'); - if (is_countable($ln->noFill) && count($ln->noFill) === 1) { - $noFill = true; + if (isset($children->ln)) { + $ln = $children->ln; + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + $lineStyle = new GridLines(); + $this->readLineStyle($seriesDetails, $lineStyle); + } + if (isset($children->effectLst)) { + if ($lineStyle === null) { + $lineStyle = new GridLines(); + } + $this->readEffects($seriesDetails, $lineStyle); } if (isset($children->solidFill)) { - $this->readColor($children->solidFill, $srgbClr, $schemeClr); + $fillColor = new ChartColor($this->readColor($children->solidFill)); } + break; + case 'dPt': + $dptIdx = (int) self::getAttribute($seriesDetail->idx, 'val', 'string'); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $arrayColors = $this->readColor($children->solidFill); + $dptColors[$dptIdx] = new ChartColor($arrayColors); + } + } + + break; + case 'trendline': + $trendLine = new TrendLine(); + $this->readLineStyle($seriesDetail, $trendLine); + /** @var ?string */ + $trendLineType = self::getAttribute($seriesDetail->trendlineType, 'val', 'string'); + /** @var ?bool */ + $dispRSqr = self::getAttribute($seriesDetail->dispRSqr, 'val', 'boolean'); + /** @var ?bool */ + $dispEq = self::getAttribute($seriesDetail->dispEq, 'val', 'boolean'); + /** @var ?int */ + $order = self::getAttribute($seriesDetail->order, 'val', 'integer'); + /** @var ?int */ + $period = self::getAttribute($seriesDetail->period, 'val', 'integer'); + /** @var ?float */ + $forward = self::getAttribute($seriesDetail->forward, 'val', 'float'); + /** @var ?float */ + $backward = self::getAttribute($seriesDetail->backward, 'val', 'float'); + /** @var ?float */ + $intercept = self::getAttribute($seriesDetail->intercept, 'val', 'float'); + /** @var ?string */ + $name = (string) $seriesDetail->name; + $trendLine->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); + $trendLines[] = $trendLine; + break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; - if (count($seriesDetail->spPr) === 1) { - $ln = $seriesDetail->spPr->children($this->aNamespace); - if (isset($ln->solidFill)) { - $this->readColor($ln->solidFill, $srgbClr, $schemeClr); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $markerFillColor = $this->readColor($children->solidFill); + } + if (isset($children->ln->solidFill)) { + $markerBorderColor = $this->readColor($children->ln->solidFill); } } @@ -408,25 +584,40 @@ class Chart break; case 'val': - $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'bubbleSize': - $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", "$srgbClr", "$pointSize"); + $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'bubble3D': $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); break; + case 'dLbls': + $labelLayout = new Layout($this->readChartAttributes($seriesDetails)); + + break; + } + } + if ($labelLayout) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLabelLayout($labelLayout); } } if ($noFill) { @@ -440,26 +631,15 @@ class Chart $seriesValues[$seriesIndex]->setScatterLines(false); } } - if (is_numeric($lineWidth)) { + if ($lineStyle !== null) { if (isset($seriesLabel[$seriesIndex])) { - $seriesLabel[$seriesIndex]->setLineWidth((int) $lineWidth); + $seriesLabel[$seriesIndex]->copyLineStyles($lineStyle); } if (isset($seriesCategory[$seriesIndex])) { - $seriesCategory[$seriesIndex]->setLineWidth((int) $lineWidth); + $seriesCategory[$seriesIndex]->copyLineStyles($lineStyle); } if (isset($seriesValues[$seriesIndex])) { - $seriesValues[$seriesIndex]->setLineWidth((int) $lineWidth); - } - } - if ($schemeClr) { - if (isset($seriesLabel[$seriesIndex])) { - $seriesLabel[$seriesIndex]->setSchemeClr($schemeClr); - } - if (isset($seriesCategory[$seriesIndex])) { - $seriesCategory[$seriesIndex]->setSchemeClr($schemeClr); - } - if (isset($seriesValues[$seriesIndex])) { - $seriesValues[$seriesIndex]->setSchemeClr($schemeClr); + $seriesValues[$seriesIndex]->copyLineStyles($lineStyle); } } if ($bubble3D) { @@ -473,6 +653,61 @@ class Chart $seriesValues[$seriesIndex]->setBubble3D($bubble3D); } } + if (!empty($dptColors)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setFillColor($dptColors); + } + } + if ($markerFillColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + } + if ($markerBorderColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + } + if ($smoothLine) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSmoothLine(true); + } + } + if (!empty($trendLines)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setTrendLines($trendLines); + } + } } } /** @phpstan-ignore-next-line */ @@ -485,11 +720,11 @@ class Chart /** * @return mixed */ - private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?string $srgbClr = null, ?string $pointSize = null) + private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?ChartColor $fillColor = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's'); @@ -501,7 +736,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace)); $seriesValues @@ -512,7 +747,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's'); @@ -524,7 +759,7 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $srgbClr, "$pointSize"); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's'); @@ -556,6 +791,7 @@ class Chart $pointCount = 0; foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) { + $seriesValue = Xlsx::testSimpleXml($seriesValue); switch ($seriesValueIdx) { case 'ptCount': $pointCount = self::getAttribute($seriesValue, 'val', 'integer'); @@ -628,7 +864,6 @@ class Chart private function parseRichText(SimpleXMLElement $titleDetailPart): RichText { $value = new RichText(); - $objText = null; $defaultFontSize = null; $defaultBold = null; $defaultItalic = null; @@ -639,8 +874,7 @@ class Chart $defaultLatin = null; $defaultEastAsian = null; $defaultComplexScript = null; - $defaultSrgbColor = ''; - $defaultSchemeColor = ''; + $defaultFontColor = null; if (isset($titleDetailPart->pPr->defRPr)) { /** @var ?int */ $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer'); @@ -670,7 +904,7 @@ class Chart $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string'); } if (isset($titleDetailPart->pPr->defRPr->solidFill)) { - $this->readColor($titleDetailPart->pPr->defRPr->solidFill, $defaultSrgbColor, $defaultSchemeClr); + $defaultFontColor = $this->readColor($titleDetailPart->pPr->defRPr->solidFill); } } foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { @@ -696,9 +930,8 @@ class Chart $latinName = null; $eastAsian = null; $complexScript = null; - $fontSrgbClr = ''; - $fontSchemeClr = ''; - $uSchemeClr = null; + $fontColor = null; + $underlineColor = null; if (isset($titleDetailElement->rPr)) { // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { @@ -722,10 +955,8 @@ class Chart $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); // not used now, not sure it ever was, grandfathering - /** @var ?string */ - $fontSrgbClr = self::getAttribute($titleDetailElement->rPr, 'color', 'string'); if (isset($titleDetailElement->rPr->solidFill)) { - $this->readColor($titleDetailElement->rPr->solidFill, $fontSrgbClr, $fontSchemeClr); + $fontColor = $this->readColor($titleDetailElement->rPr->solidFill); } /** @var ?bool */ @@ -739,9 +970,8 @@ class Chart /** @var ?string */ $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); - if (isset($titleDetailElement->rPr->uFill->solidFill->schemeClr)) { - /** @var ?string */ - $uSchemeClr = self::getAttribute($titleDetailElement->rPr->uFill->solidFill->schemeClr, 'val', 'string'); + if (isset($titleDetailElement->rPr->uFill->solidFill)) { + $underlineColor = $this->readColor($titleDetailElement->rPr->uFill->solidFill); } /** @var ?string */ @@ -776,19 +1006,15 @@ class Chart if (is_int($fontSize)) { $objText->getFont()->setSize(floor($fontSize / 100)); $fontFound = true; + } else { + $objText->getFont()->setSize(null, true); } - $fontSrgbClr = $fontSrgbClr ?? $defaultSrgbColor; - if (!empty($fontSrgbClr)) { - $objText->getFont()->setColor(new Color($fontSrgbClr)); + $fontColor = $fontColor ?? $defaultFontColor; + if (!empty($fontColor)) { + $objText->getFont()->setChartColor($fontColor); $fontFound = true; } - // need to think about what to do here - //$fontSchemeClr = $fontSchemeClr ?? $defaultSchemeColor; - //if (!empty($fontSchemeClr)) { - // $objText->getFont()->setColor(new Color($fontSrgbClr)); - // $fontFound = true; - //} $bold = $bold ?? $defaultBold; if ($bold !== null) { @@ -825,8 +1051,8 @@ class Chart $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); } $fontFound = true; - if ($uSchemeClr) { - $objText->getFont()->setUSchemeClr($uSchemeClr); + if ($underlineColor) { + $objText->getFont()->setUnderlineColor($underlineColor); } } @@ -849,12 +1075,19 @@ class Chart } /** - * @param null|Layout|SimpleXMLElement $chartDetail + * @param ?SimpleXMLElement $chartDetail */ private function readChartAttributes($chartDetail): array { $plotAttributes = []; if (isset($chartDetail->dLbls)) { + if (isset($chartDetail->dLbls->dLblPos)) { + $plotAttributes['dLblPos'] = self::getAttribute($chartDetail->dLbls->dLblPos, 'val', 'string'); + } + if (isset($chartDetail->dLbls->numFmt)) { + $plotAttributes['numFmtCode'] = self::getAttribute($chartDetail->dLbls->numFmt, 'formatCode', 'string'); + $plotAttributes['numFmtLinked'] = self::getAttribute($chartDetail->dLbls->numFmt, 'sourceLinked', 'boolean'); + } if (isset($chartDetail->dLbls->showLegendKey)) { $plotAttributes['showLegendKey'] = self::getAttribute($chartDetail->dLbls->showLegendKey, 'val', 'string'); } @@ -876,6 +1109,21 @@ class Chart if (isset($chartDetail->dLbls->showLeaderLines)) { $plotAttributes['showLeaderLines'] = self::getAttribute($chartDetail->dLbls->showLeaderLines, 'val', 'string'); } + if (isset($chartDetail->dLbls->spPr)) { + $sppr = $chartDetail->dLbls->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $plotAttributes['labelFillColor'] = new ChartColor($this->readColor($sppr->solidFill)); + } + if (isset($sppr->ln->solidFill)) { + $plotAttributes['labelBorderColor'] = new ChartColor($this->readColor($sppr->ln->solidFill)); + } + } + if (isset($chartDetail->dLbls->txPr)) { + $txpr = $chartDetail->dLbls->txPr->children($this->aNamespace); + if (isset($txpr->p->pPr->defRPr->solidFill)) { + $plotAttributes['labelFontColor'] = new ChartColor($this->readColor($txpr->p->pPr->defRPr->solidFill)); + } + } } return $plotAttributes; @@ -920,10 +1168,7 @@ class Chart } } - /** - * @param null|Axis|GridLines $chartObject may be extended to include other types - */ - private function readEffects(SimpleXMLElement $chartDetail, $chartObject): void + private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void { if (!isset($chartObject, $chartDetail->spPr)) { return; @@ -931,7 +1176,7 @@ class Chart $sppr = $chartDetail->spPr->children($this->aNamespace); if (isset($sppr->effectLst->glow)) { - $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / Properties::POINTS_WIDTH_MULTIPLIER; + $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / ChartProperties::POINTS_WIDTH_MULTIPLIER; if ($axisGlowSize != 0.0) { $colorArray = $this->readColor($sppr->effectLst->glow); $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']); @@ -942,7 +1187,7 @@ class Chart /** @var string */ $softEdgeSize = self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string'); if (is_numeric($softEdgeSize)) { - $chartObject->setSoftEdges((float) Properties::xmlToPoints($softEdgeSize)); + $chartObject->setSoftEdges((float) ChartProperties::xmlToPoints($softEdgeSize)); } } @@ -957,20 +1202,20 @@ class Chart if ($type !== '') { /** @var string */ $blur = self::getAttribute($sppr->effectLst->$type, 'blurRad', 'string'); - $blur = is_numeric($blur) ? Properties::xmlToPoints($blur) : null; + $blur = is_numeric($blur) ? ChartProperties::xmlToPoints($blur) : null; /** @var string */ $dist = self::getAttribute($sppr->effectLst->$type, 'dist', 'string'); - $dist = is_numeric($dist) ? Properties::xmlToPoints($dist) : null; + $dist = is_numeric($dist) ? ChartProperties::xmlToPoints($dist) : null; /** @var string */ $direction = self::getAttribute($sppr->effectLst->$type, 'dir', 'string'); - $direction = is_numeric($direction) ? Properties::xmlToAngle($direction) : null; + $direction = is_numeric($direction) ? ChartProperties::xmlToAngle($direction) : null; $algn = self::getAttribute($sppr->effectLst->$type, 'algn', 'string'); $rot = self::getAttribute($sppr->effectLst->$type, 'rotWithShape', 'string'); $size = []; foreach (['sx', 'sy'] as $sizeType) { $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); if (is_numeric($sizeValue)) { - $size[$sizeType] = Properties::xmlToTenthOfPercent((string) $sizeValue); + $size[$sizeType] = ChartProperties::xmlToTenthOfPercent((string) $sizeValue); } else { $size[$sizeType] = null; } @@ -978,7 +1223,7 @@ class Chart foreach (['kx', 'ky'] as $sizeType) { $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); if (is_numeric($sizeValue)) { - $size[$sizeType] = Properties::xmlToAngle((string) $sizeValue); + $size[$sizeType] = ChartProperties::xmlToAngle((string) $sizeValue); } else { $size[$sizeType] = null; } @@ -1001,42 +1246,171 @@ class Chart 'innerShdw', ]; - private function readColor(SimpleXMLElement $colorXml, ?string &$srgbClr = null, ?string &$schemeClr = null): array + private function readColor(SimpleXMLElement $colorXml): array { $result = [ 'type' => null, 'value' => null, 'alpha' => null, + 'brightness' => null, ]; - if (isset($colorXml->srgbClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_ARGB; - $result['value'] = $srgbClr = self::getAttribute($colorXml->srgbClr, 'val', 'string'); - if (isset($colorXml->srgbClr->alpha)) { - /** @var string */ - $alpha = self::getAttribute($colorXml->srgbClr->alpha, 'val', 'string'); - $alpha = Properties::alphaFromXml($alpha); - $result['alpha'] = $alpha; - } - } elseif (isset($colorXml->schemeClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_SCHEME; - $result['value'] = $schemeClr = self::getAttribute($colorXml->schemeClr, 'val', 'string'); - if (isset($colorXml->schemeClr->alpha)) { - /** @var string */ - $alpha = self::getAttribute($colorXml->schemeClr->alpha, 'val', 'string'); - $alpha = Properties::alphaFromXml($alpha); - $result['alpha'] = $alpha; - } - } elseif (isset($colorXml->prstClr)) { - $result['type'] = Properties::EXCEL_COLOR_TYPE_STANDARD; - $result['value'] = self::getAttribute($colorXml->prstClr, 'val', 'string'); - if (isset($colorXml->prstClr->alpha)) { - /** @var string */ - $alpha = self::getAttribute($colorXml->prstClr->alpha, 'val', 'string'); - $alpha = Properties::alphaFromXml($alpha); - $result['alpha'] = $alpha; + foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { + if (isset($colorXml->$type)) { + $result['type'] = $type; + $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string'); + if (isset($colorXml->$type->alpha)) { + /** @var string */ + $alpha = self::getAttribute($colorXml->$type->alpha, 'val', 'string'); + if (is_numeric($alpha)) { + $result['alpha'] = ChartColor::alphaFromXml($alpha); + } + } + if (isset($colorXml->$type->lumMod)) { + /** @var string */ + $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } + + break; } } return $result; } + + private function readLineStyle(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void + { + if (!isset($chartObject, $chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + + if (!isset($sppr->ln)) { + return; + } + $lineWidth = null; + /** @var string */ + $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string'); + if (is_numeric($lineWidthTemp)) { + $lineWidth = ChartProperties::xmlToPoints($lineWidthTemp); + } + /** @var string */ + $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string'); + /** @var string */ + $dashType = self::getAttribute($sppr->ln->prstDash, 'val', 'string'); + /** @var string */ + $capType = self::getAttribute($sppr->ln, 'cap', 'string'); + if (isset($sppr->ln->miter)) { + $joinType = ChartProperties::LINE_STYLE_JOIN_MITER; + } elseif (isset($sppr->ln->bevel)) { + $joinType = ChartProperties::LINE_STYLE_JOIN_BEVEL; + } else { + $joinType = ''; + } + $headArrowSize = ''; + $endArrowSize = ''; + /** @var string */ + $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string'); + /** @var string */ + $headArrowWidth = self::getAttribute($sppr->ln->headEnd, 'w', 'string'); + /** @var string */ + $headArrowLength = self::getAttribute($sppr->ln->headEnd, 'len', 'string'); + /** @var string */ + $endArrowType = self::getAttribute($sppr->ln->tailEnd, 'type', 'string'); + /** @var string */ + $endArrowWidth = self::getAttribute($sppr->ln->tailEnd, 'w', 'string'); + /** @var string */ + $endArrowLength = self::getAttribute($sppr->ln->tailEnd, 'len', 'string'); + $chartObject->setLineStyleProperties( + $lineWidth, + $compoundType, + $dashType, + $capType, + $joinType, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize, + $headArrowWidth, + $headArrowLength, + $endArrowWidth, + $endArrowLength + ); + $colorArray = $this->readColor($sppr->ln->solidFill); + $chartObject->getLineColor()->setColorPropertiesArray($colorArray); + } + + private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void + { + if (!isset($whichAxis)) { + return; + } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string')); + } + if (isset($chartDetail->numFmt)) { + $whichAxis->setAxisNumberProperties( + (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), + null, + (int) self::getAttribute($chartDetail->numFmt, 'sourceLinked', 'int') + ); + } + if (isset($chartDetail->crossBetween)) { + $whichAxis->setCrossBetween((string) self::getAttribute($chartDetail->crossBetween, 'val', 'string')); + } + if (isset($chartDetail->majorTickMark)) { + $whichAxis->setAxisOption('major_tick_mark', (string) self::getAttribute($chartDetail->majorTickMark, 'val', 'string')); + } + if (isset($chartDetail->minorTickMark)) { + $whichAxis->setAxisOption('minor_tick_mark', (string) self::getAttribute($chartDetail->minorTickMark, 'val', 'string')); + } + if (isset($chartDetail->tickLblPos)) { + $whichAxis->setAxisOption('axis_labels', (string) self::getAttribute($chartDetail->tickLblPos, 'val', 'string')); + } + if (isset($chartDetail->crosses)) { + $whichAxis->setAxisOption('horizontal_crosses', (string) self::getAttribute($chartDetail->crosses, 'val', 'string')); + } + if (isset($chartDetail->crossesAt)) { + $whichAxis->setAxisOption('horizontal_crosses_value', (string) self::getAttribute($chartDetail->crossesAt, 'val', 'string')); + } + if (isset($chartDetail->scaling->orientation)) { + $whichAxis->setAxisOption('orientation', (string) self::getAttribute($chartDetail->scaling->orientation, 'val', 'string')); + } + if (isset($chartDetail->scaling->max)) { + $whichAxis->setAxisOption('maximum', (string) self::getAttribute($chartDetail->scaling->max, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->majorUnit)) { + $whichAxis->setAxisOption('major_unit', (string) self::getAttribute($chartDetail->majorUnit, 'val', 'string')); + } + if (isset($chartDetail->minorUnit)) { + $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); + } + if (isset($chartDetail->baseTimeUnit)) { + $whichAxis->setAxisOption('baseTimeUnit', (string) self::getAttribute($chartDetail->baseTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->majorTimeUnit)) { + $whichAxis->setAxisOption('majorTimeUnit', (string) self::getAttribute($chartDetail->majorTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->minorTimeUnit)) { + $whichAxis->setAxisOption('minorTimeUnit', (string) self::getAttribute($chartDetail->minorTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->txPr)) { + $children = $chartDetail->txPr->children($this->aNamespace); + if (isset($children->bodyPr)) { + /** @var string */ + $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); + if (is_numeric($textRotation)) { + $whichAxis->setAxisOption('textRotation', (string) ChartProperties::xmlToAngle($textRotation)); + } + } + } + } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php index 2a1e2afd..34705733 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -10,8 +10,10 @@ use SimpleXMLElement; class ColumnAndRowAttributes extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -120,7 +122,7 @@ class ColumnAndRowAttributes extends BaseParserClass } } - private function isFilteredColumn(IReadFilter $readFilter, $columnCoordinate, array $rowsAttributes) + private function isFilteredColumn(IReadFilter $readFilter, string $columnCoordinate, array $rowsAttributes): bool { foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -131,7 +133,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readColumnAttributes(SimpleXMLElement $worksheetCols, $readDataOnly) + private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array { $columnAttributes = []; @@ -151,7 +153,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function readColumnRangeAttributes(SimpleXMLElement $column, $readDataOnly) + private function readColumnRangeAttributes(SimpleXMLElement $column, bool $readDataOnly): array { $columnAttributes = []; @@ -172,7 +174,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function isFilteredRow(IReadFilter $readFilter, $rowCoordinate, array $columnsAttributes) + private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool { foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -183,7 +185,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readRowAttributes(SimpleXMLElement $worksheetRow, $readDataOnly) + private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly): array { $rowAttributes = []; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index c631a0fe..7d947bac 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -10,11 +10,14 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueO use PhpOffice\PhpSpreadsheet\Style\Style as Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; +use stdClass; class ConditionalStyles { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; /** @@ -22,6 +25,7 @@ class ConditionalStyles */ private $ns; + /** @var array */ private $dxfs; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs = []) @@ -146,7 +150,7 @@ class ConditionalStyles return $cfStyle; } - private function readConditionalStyles($xmlSheet): array + private function readConditionalStyles(SimpleXMLElement $xmlSheet): array { $conditionals = []; foreach ($xmlSheet->conditionalFormatting as $conditional) { @@ -162,7 +166,7 @@ class ConditionalStyles return $conditionals; } - private function setConditionalStyles(Worksheet $worksheet, array $conditionals, $xmlExtLst): void + private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void { foreach ($conditionals as $cellRangeReference => $cfRules) { ksort($cfRules); @@ -176,7 +180,7 @@ class ConditionalStyles } } - private function readStyleRules($cfRules, $extLst) + private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array { $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); $conditionalStyles = []; @@ -213,7 +217,7 @@ class ConditionalStyles if (isset($cfRule->dataBar)) { $objConditional->setDataBar( - $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) + $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) // @phpstan-ignore-line ); } else { $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); @@ -225,7 +229,10 @@ class ConditionalStyles return $conditionalStyles; } - private function readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions): ConditionalDataBar + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarOfConditionalRule($cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar { $dataBar = new ConditionalDataBar(); //dataBar attribute @@ -257,7 +264,10 @@ class ConditionalStyles return $dataBar; } - private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, $conditionalFormattingRuleExtensions): void + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, array $conditionalFormattingRuleExtensions): void { if (isset($cfRule->extLst)) { $ns = $cfRule->extLst->getNamespaces(true); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index b699cb57..dac76230 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class DataValidations { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -22,7 +24,7 @@ class DataValidations { foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { // Uppercase coordinate - $range = strtoupper($dataValidation['sqref']); + $range = strtoupper((string) $dataValidation['sqref']); $rangeSet = explode(' ', $range); foreach ($rangeSet as $range) { $stRange = $this->worksheet->shrinkRangeToFit($range); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php index 84884996..7d48c796 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class Hyperlinks { + /** @var Worksheet */ private $worksheet; + /** @var array */ private $hyperlinks = []; public function __construct(Worksheet $workSheet) diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index 57a88bb0..7f484c2f 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -58,6 +58,10 @@ class Namespaces const VBA = 'http://schemas.microsoft.com/office/2006/relationships/vbaProject'; + const DATA_VALIDATIONS1 = 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'; + + const DATA_VALIDATIONS2 = 'http://schemas.microsoft.com/office/excel/2006/main'; + const DC_ELEMENTS = 'http://purl.org/dc/elements/1.1/'; const DC_TERMS = 'http://purl.org/dc/terms'; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php index 56f18f98..08decd6e 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class PageSetup extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -18,16 +20,17 @@ class PageSetup extends BaseParserClass $this->worksheetXml = $worksheetXml; } - public function load(array $unparsedLoadedData) + public function load(array $unparsedLoadedData): array { - if (!$this->worksheetXml) { + $worksheetXml = $this->worksheetXml; + if ($worksheetXml === null) { return $unparsedLoadedData; } - $this->margins($this->worksheetXml, $this->worksheet); - $unparsedLoadedData = $this->pageSetup($this->worksheetXml, $this->worksheet, $unparsedLoadedData); - $this->headerFooter($this->worksheetXml, $this->worksheet); - $this->pageBreaks($this->worksheetXml, $this->worksheet); + $this->margins($worksheetXml, $this->worksheet); + $unparsedLoadedData = $this->pageSetup($worksheetXml, $this->worksheet, $unparsedLoadedData); + $this->headerFooter($worksheetXml, $this->worksheet); + $this->pageBreaks($worksheetXml, $this->worksheet); return $unparsedLoadedData; } @@ -45,7 +48,7 @@ class PageSetup extends BaseParserClass } } - private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData) + private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array { if ($xmlSheet->pageSetup) { $docPageSetup = $worksheet->getPageSetup(); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php index a302cc56..9c02da9f 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -7,8 +7,10 @@ use SimpleXMLElement; class SheetViewOptions extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -24,10 +26,11 @@ class SheetViewOptions extends BaseParserClass } if (isset($this->worksheetXml->sheetPr)) { - $this->tabColor($this->worksheetXml->sheetPr, $styleReader); - $this->codeName($this->worksheetXml->sheetPr); - $this->outlines($this->worksheetXml->sheetPr); - $this->pageSetup($this->worksheetXml->sheetPr); + $sheetPr = $this->worksheetXml->sheetPr; + $this->tabColor($sheetPr, $styleReader); + $this->codeName($sheetPr); + $this->outlines($sheetPr); + $this->pageSetup($sheetPr); } if (isset($this->worksheetXml->sheetFormatPr)) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index f84aaa68..8d380907 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -365,11 +365,12 @@ class Styles extends BaseParserClass return (string) $attr['rgb']; } if (isset($attr['indexed'])) { - if (empty($this->workbookPalette)) { - return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; + $indexedColor = (int) $attr['indexed']; + if ($indexedColor >= count($this->workbookPalette)) { + return Color::indexedColor($indexedColor - 7, $background)->getARGB() ?? ''; } - return Color::indexedColor((int) ($attr['indexed']), $background, $this->workbookPalette)->getARGB() ?? ''; + return Color::indexedColor($indexedColor, $background, $this->workbookPalette)->getARGB() ?? ''; } if (isset($attr['theme'])) { if ($this->theme !== null) { diff --git a/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php b/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php index 9d61e3d3..4743afbf 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php @@ -29,7 +29,7 @@ class WorkbookView $this->spreadsheet->setActiveSheetIndex(0); $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView; - if (($readDataOnly !== true || !empty($this->loadSheetsOnly)) && !empty($workbookView)) { + if ($readDataOnly !== true && !empty($workbookView)) { $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView)); // active sheet index $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 0b5e0966..d8f0d9dc 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; /** @@ -364,7 +365,7 @@ class Xml extends BaseReader $rowTo = $rowTo + $cell_ss['MergeDown']; } $cellRange .= ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } $hasCalculatedValue = false; diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 046c5894..3f53ed1d 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -253,10 +253,11 @@ class ReferenceHelper ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) : uksort($aDataValidationCollection, [self::class, 'cellSort']); - foreach ($aDataValidationCollection as $cellAddress => $value) { + foreach ($aDataValidationCollection as $cellAddress => $dataValidation) { $newReference = $this->updateCellReference($cellAddress); if ($cellAddress !== $newReference) { - $worksheet->setDataValidation($newReference, $value); + $dataValidation->setSqref($newReference); + $worksheet->setDataValidation($newReference, $dataValidation); $worksheet->setDataValidation($cellAddress, null); } } @@ -316,6 +317,7 @@ class ReferenceHelper $objColumnDimension->setColumnIndex($newReference); } } + $worksheet->refreshColumnDimensions(); } } @@ -339,6 +341,7 @@ class ReferenceHelper $objRowDimension->setRowIndex($newRoweference); } } + $worksheet->refreshRowDimensions(); $copyDimension = $worksheet->getRowDimension($beforeRow - 1); @@ -367,7 +370,6 @@ class ReferenceHelper Worksheet $worksheet ): void { $remove = ($numberOfColumns < 0 || $numberOfRows < 0); - $allCoordinates = $worksheet->getCoordinates(); if ( $this->cellReferenceHelper === null || @@ -394,12 +396,13 @@ class ReferenceHelper } // Find missing coordinates. This is important when inserting column before the last column + $cellCollection = $worksheet->getCellCollection(); $missingCoordinates = array_filter( array_map(function ($row) use ($highestColumn) { return $highestColumn . $row; }, range(1, $highestRow)), - function ($coordinate) use ($allCoordinates) { - return !in_array($coordinate, $allCoordinates); + function ($coordinate) use ($cellCollection) { + return $cellCollection->has($coordinate) === false; } ); @@ -408,16 +411,15 @@ class ReferenceHelper foreach ($missingCoordinates as $coordinate) { $worksheet->createNewCell($coordinate); } - - // Refresh all coordinates - $allCoordinates = $worksheet->getCoordinates(); } - // Loop through cells, bottom-up, and change cell coordinate + $allCoordinates = $worksheet->getCoordinates(); if ($remove) { // It's faster to reverse and pop than to use unshift, especially with large cell collections $allCoordinates = array_reverse($allCoordinates); } + + // Loop through cells, bottom-up, and change cell coordinate while ($coordinate = array_pop($allCoordinates)) { $cell = $worksheet->getCell($coordinate); $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); @@ -526,15 +528,17 @@ class ReferenceHelper if ($objDrawing->getCoordinates() != $newReference) { $objDrawing->setCoordinates($newReference); } + if ($objDrawing->getCoordinates2() !== '') { + $newReference = $this->updateCellReference($objDrawing->getCoordinates2()); + if ($objDrawing->getCoordinates2() != $newReference) { + $objDrawing->setCoordinates2($newReference); + } + } } // Update workbook: define names if (count($worksheet->getParent()->getDefinedNames()) > 0) { - foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $definedName->setValue($this->updateCellReference($definedName->getValue())); - } - } + $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); } // Garbage collect @@ -858,13 +862,13 @@ class ReferenceHelper } /** - * Update named formulas (i.e. containing worksheet references / named ranges). + * Update named formulae (i.e. containing worksheet references / named ranges). * * @param Spreadsheet $spreadsheet Object to update * @param string $oldName Old name (name to replace) * @param string $newName New name */ - public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void + public function updateNamedFormulae(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void { if ($oldName == '') { return; @@ -873,7 +877,7 @@ class ReferenceHelper foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getCoordinates(false) as $coordinate) { $cell = $sheet->getCell($coordinate); - if (($cell !== null) && ($cell->getDataType() === DataType::TYPE_FORMULA)) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { $formula = $cell->getValue(); if (strpos($formula, $oldName) !== false) { $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); @@ -885,6 +889,40 @@ class ReferenceHelper } } + private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { + if ($definedName->isFormula() === false) { + $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } else { + $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } + } + } + + private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + $cellAddress = $definedName->getValue(); + $asFormula = ($cellAddress[0] === '='); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($asFormula === true) { + $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } else { + $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='))); + } + } + } + + private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } + } + /** * Update cell range. * @@ -924,34 +962,43 @@ class ReferenceHelper private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void { - for ($i = 1; $i <= $highestRow - 1; ++$i) { - for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns); + $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + + for ($row = 1; $row <= $highestRow - 1; ++$row) { + for ($column = $startColumnId; $column !== $endColumnId; ++$column) { + $coordinate = $column . $row; + $this->clearStripCell($worksheet, $coordinate); } } } private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void { - $lastColumnIndex = Coordinate::columnIndexFromString($highestColumn) - 1; + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + ++$highestColumn; - for ($i = $beforeColumn - 1; $i <= $lastColumnIndex; ++$i) { - for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } + for ($column = $startColumnId; $column !== $highestColumn; ++$column) { + for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) { + $coordinate = $column . $row; + $this->clearStripCell($worksheet, $coordinate); } } } + private function clearStripCell(Worksheet $worksheet, string $coordinate): void + { + $worksheet->removeConditionalStyles($coordinate); + $worksheet->setHyperlink($coordinate); + $worksheet->setDataValidation($coordinate); + $worksheet->removeComment($coordinate); + + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); + } + } + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void { $autoFilter = $worksheet->getAutoFilter(); diff --git a/src/PhpSpreadsheet/Settings.php b/src/PhpSpreadsheet/Settings.php index 5fbbadb6..3282a596 100644 --- a/src/PhpSpreadsheet/Settings.php +++ b/src/PhpSpreadsheet/Settings.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Memory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\CacheInterface; +use ReflectionClass; class Settings { @@ -161,12 +162,19 @@ class Settings public static function getCache(): CacheInterface { if (!self::$cache) { - self::$cache = new Memory(); + self::$cache = self::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(); } return self::$cache; } + public static function useSimpleCacheVersion3(): bool + { + return + PHP_MAJOR_VERSION === 8 && + (new ReflectionClass(CacheInterface::class))->getMethod('get')->getReturnType() !== null; + } + /** * Set the HTTP client implementation to be used for network request. */ diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 1adf213e..e90c679b 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -13,7 +13,7 @@ class Font const AUTOSIZE_METHOD_APPROX = 'approx'; const AUTOSIZE_METHOD_EXACT = 'exact'; - private static $autoSizeMethods = [ + private const AUTOSIZE_METHODS = [ self::AUTOSIZE_METHOD_APPROX, self::AUTOSIZE_METHOD_EXACT, ]; @@ -101,6 +101,105 @@ class Font const VERDANA_ITALIC = 'verdanai.ttf'; const VERDANA_BOLD_ITALIC = 'verdanaz.ttf'; + const FONT_FILE_NAMES = [ + 'Arial' => [ + 'x' => self::ARIAL, + 'xb' => self::ARIAL_BOLD, + 'xi' => self::ARIAL_ITALIC, + 'xbi' => self::ARIAL_BOLD_ITALIC, + ], + 'Calibri' => [ + 'x' => self::CALIBRI, + 'xb' => self::CALIBRI_BOLD, + 'xi' => self::CALIBRI_ITALIC, + 'xbi' => self::CALIBRI_BOLD_ITALIC, + ], + 'Comic Sans MS' => [ + 'x' => self::COMIC_SANS_MS, + 'xb' => self::COMIC_SANS_MS_BOLD, + 'xi' => self::COMIC_SANS_MS, + 'xbi' => self::COMIC_SANS_MS_BOLD, + ], + 'Courier New' => [ + 'x' => self::COURIER_NEW, + 'xb' => self::COURIER_NEW_BOLD, + 'xi' => self::COURIER_NEW_ITALIC, + 'xbi' => self::COURIER_NEW_BOLD_ITALIC, + ], + 'Georgia' => [ + 'x' => self::GEORGIA, + 'xb' => self::GEORGIA_BOLD, + 'xi' => self::GEORGIA_ITALIC, + 'xbi' => self::GEORGIA_BOLD_ITALIC, + ], + 'Impact' => [ + 'x' => self::IMPACT, + 'xb' => self::IMPACT, + 'xi' => self::IMPACT, + 'xbi' => self::IMPACT, + ], + 'Liberation Sans' => [ + 'x' => self::LIBERATION_SANS, + 'xb' => self::LIBERATION_SANS_BOLD, + 'xi' => self::LIBERATION_SANS_ITALIC, + 'xbi' => self::LIBERATION_SANS_BOLD_ITALIC, + ], + 'Lucida Console' => [ + 'x' => self::LUCIDA_CONSOLE, + 'xb' => self::LUCIDA_CONSOLE, + 'xi' => self::LUCIDA_CONSOLE, + 'xbi' => self::LUCIDA_CONSOLE, + ], + 'Lucida Sans Unicode' => [ + 'x' => self::LUCIDA_SANS_UNICODE, + 'xb' => self::LUCIDA_SANS_UNICODE, + 'xi' => self::LUCIDA_SANS_UNICODE, + 'xbi' => self::LUCIDA_SANS_UNICODE, + ], + 'Microsoft Sans Serif' => [ + 'x' => self::MICROSOFT_SANS_SERIF, + 'xb' => self::MICROSOFT_SANS_SERIF, + 'xi' => self::MICROSOFT_SANS_SERIF, + 'xbi' => self::MICROSOFT_SANS_SERIF, + ], + 'Palatino Linotype' => [ + 'x' => self::PALATINO_LINOTYPE, + 'xb' => self::PALATINO_LINOTYPE_BOLD, + 'xi' => self::PALATINO_LINOTYPE_ITALIC, + 'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC, + ], + 'Symbol' => [ + 'x' => self::SYMBOL, + 'xb' => self::SYMBOL, + 'xi' => self::SYMBOL, + 'xbi' => self::SYMBOL, + ], + 'Tahoma' => [ + 'x' => self::TAHOMA, + 'xb' => self::TAHOMA_BOLD, + 'xi' => self::TAHOMA, + 'xbi' => self::TAHOMA_BOLD, + ], + 'Times New Roman' => [ + 'x' => self::TIMES_NEW_ROMAN, + 'xb' => self::TIMES_NEW_ROMAN_BOLD, + 'xi' => self::TIMES_NEW_ROMAN_ITALIC, + 'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC, + ], + 'Trebuchet MS' => [ + 'x' => self::TREBUCHET_MS, + 'xb' => self::TREBUCHET_MS_BOLD, + 'xi' => self::TREBUCHET_MS_ITALIC, + 'xbi' => self::TREBUCHET_MS_BOLD_ITALIC, + ], + 'Verdana' => [ + 'x' => self::VERDANA, + 'xb' => self::VERDANA_BOLD, + 'xi' => self::VERDANA_ITALIC, + 'xbi' => self::VERDANA_BOLD_ITALIC, + ], + ]; + /** * AutoSize method. * @@ -113,54 +212,65 @@ class Font * * @var string */ - private static $trueTypeFontPath; + private static $trueTypeFontPath = ''; /** * How wide is a default column for a given default font and size? * Empirical data found by inspecting real Excel files and reading off the pixel width * in Microsoft Office Excel 2007. + * Added height in points. + */ + public const DEFAULT_COLUMN_WIDTHS = [ + 'Arial' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + 11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5], + 9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25], + 10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75], + ], + ]; + + /** + * List of column widths. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use DEFAULT_COLUMN_WIDTHS constant instead. * * @var array */ - public static $defaultColumnWidths = [ - 'Arial' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 64, 'width' => 9.14062500], - 10 => ['px' => 64, 'width' => 9.14062500], - ], - 'Calibri' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 56, 'width' => 9.33203125], - 10 => ['px' => 64, 'width' => 9.14062500], - 11 => ['px' => 64, 'width' => 9.14062500], - ], - 'Verdana' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 64, 'width' => 9.14062500], - 9 => ['px' => 72, 'width' => 9.00000000], - 10 => ['px' => 72, 'width' => 9.00000000], - ], - ]; + public static $defaultColumnWidths = self::DEFAULT_COLUMN_WIDTHS; /** * Set autoSize method. @@ -171,7 +281,7 @@ class Font */ public static function setAutoSizeMethod($method) { - if (!in_array($method, self::$autoSizeMethods)) { + if (!in_array($method, self::AUTOSIZE_METHODS)) { return false; } self::$autoSizeMethod = $method; @@ -219,7 +329,7 @@ class Font * Calculate an (approximate) OpenXML column width, based on font size and text contained. * * @param FontStyle $font Font object - * @param RichText|string $cellText Text to calculate width + * @param null|RichText|string $cellText Text to calculate width * @param int $rotation Rotation angle * @param null|FontStyle $defaultFont Font object * @param bool $filterAdjustment Add space for Autofilter or Table dropdown @@ -238,7 +348,8 @@ class Font } // Special case if there are one or more newline characters ("\n") - if (strpos($cellText ?? '', "\n") !== false) { + $cellText = $cellText ?? ''; + if (strpos(/** @scrutinizer ignore-type */ $cellText, "\n") !== false) { $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { @@ -281,7 +392,7 @@ class Font } // Convert from pixel width to column width - $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont); + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle()); // Return return (int) round($columnWidth, 6); @@ -299,7 +410,12 @@ class Font // font size should really be supplied in pixels in GD2, // but since GD2 seems to assume 72dpi, pixels and points are the same $fontFile = self::getTrueTypeFontFileFromFont($font); - $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text); + $textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text); + if ($textBox === false) { + // @codeCoverageIgnoreStart + throw new PhpSpreadsheetException('imagettfbbox failed'); + // @codeCoverageIgnoreEnd + } // Get corners positions $lowerLeftCornerX = $textBox[0]; @@ -409,129 +525,48 @@ class Font * * @return string Path to TrueType font file */ - public static function getTrueTypeFontFileFromFont(FontStyle $font) + public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true) { - if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) { + if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) { throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); } $name = $font->getName(); + if (!isset(self::FONT_FILE_NAMES[$name])) { + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + } $bold = $font->getBold(); $italic = $font->getItalic(); - - // Check if we can map font to true type font file - switch ($name) { - case 'Arial': - $fontFile = ( - $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) - : ($italic ? self::ARIAL_ITALIC : self::ARIAL) - ); - - break; - case 'Calibri': - $fontFile = ( - $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD) - : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI) - ); - - break; - case 'Courier New': - $fontFile = ( - $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD) - : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW) - ); - - break; - case 'Comic Sans MS': - $fontFile = ( - $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS - ); - - break; - case 'Georgia': - $fontFile = ( - $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) - : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) - ); - - break; - case 'Impact': - $fontFile = self::IMPACT; - - break; - case 'Liberation Sans': - $fontFile = ( - $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) - : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) - ); - - break; - case 'Lucida Console': - $fontFile = self::LUCIDA_CONSOLE; - - break; - case 'Lucida Sans Unicode': - $fontFile = self::LUCIDA_SANS_UNICODE; - - break; - case 'Microsoft Sans Serif': - $fontFile = self::MICROSOFT_SANS_SERIF; - - break; - case 'Palatino Linotype': - $fontFile = ( - $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) - : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) - ); - - break; - case 'Symbol': - $fontFile = self::SYMBOL; - - break; - case 'Tahoma': - $fontFile = ( - $bold ? self::TAHOMA_BOLD : self::TAHOMA - ); - - break; - case 'Times New Roman': - $fontFile = ( - $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD) - : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN) - ); - - break; - case 'Trebuchet MS': - $fontFile = ( - $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD) - : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS) - ); - - break; - case 'Verdana': - $fontFile = ( - $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) - : ($italic ? self::VERDANA_ITALIC : self::VERDANA) - ); - - break; - default: - throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); - - break; + $index = 'x'; + if ($bold) { + $index .= 'b'; } + if ($italic) { + $index .= 'i'; + } + $fontFile = self::FONT_FILE_NAMES[$name][$index]; - $fontFile = self::$trueTypeFontPath . $fontFile; + $separator = ''; + if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') { + $separator = DIRECTORY_SEPARATOR; + } + $fontFile = self::$trueTypeFontPath . $separator . $fontFile; // Check if file actually exists - if (!file_exists($fontFile)) { + if ($checkPath && !file_exists($fontFile)) { throw new PhpSpreadsheetException('TrueType Font file not found'); } return $fontFile; } + public const CHARSET_FROM_FONT_NAME = [ + 'EucrosiaUPC' => self::CHARSET_ANSI_THAI, + 'Wingdings' => self::CHARSET_SYMBOL, + 'Wingdings 2' => self::CHARSET_SYMBOL, + 'Wingdings 3' => self::CHARSET_SYMBOL, + ]; + /** * Returns the associated charset for the font name. * @@ -541,19 +576,7 @@ class Font */ public static function getCharsetFromFontName($fontName) { - switch ($fontName) { - // Add more cases. Check FONT records in real Excel files. - case 'EucrosiaUPC': - return self::CHARSET_ANSI_THAI; - case 'Wingdings': - return self::CHARSET_SYMBOL; - case 'Wingdings 2': - return self::CHARSET_SYMBOL; - case 'Wingdings 3': - return self::CHARSET_SYMBOL; - default: - return self::CHARSET_ANSI_LATIN; - } + return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN; } /** @@ -567,17 +590,17 @@ class Font */ public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false) { - if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) { + if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()])) { // Exact width can be determined $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] - : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['px'] + : self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['width']; } else { // We don't have data for this particular font and size, use approximation by // extrapolating from Calibri 11 $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths['Calibri'][11]['px'] - : self::$defaultColumnWidths['Calibri'][11]['width']; + self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] + : self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width']; $columnWidth = $columnWidth * $font->getSize() / 11; // Round pixels to closest integer @@ -599,173 +622,14 @@ class Font */ public static function getDefaultRowHeightByFont(FontStyle $font) { - switch ($font->getName()) { - case 'Arial': - switch ($font->getSize()) { - case 10: - // inspection of Arial 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Arial 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Arial 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Arial 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Arial 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Arial 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Arial 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Arial 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Arial 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - case 'Calibri': - switch ($font->getSize()) { - case 11: - // inspection of Calibri 11 workbook says 15.00pt ~20px - $rowHeight = 15; - - break; - case 10: - // inspection of Calibri 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Calibri 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Calibri 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Calibri 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Calibri 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Calibri 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Calibri 3 workbook says 6.00pt ~8px - $rowHeight = 6.00; - - break; - case 2: - case 1: - // inspection of Calibri 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Calibri 11 workbook as an approximation, extrapolation - $rowHeight = 15 * $font->getSize() / 11; - - break; - } - - break; - case 'Verdana': - switch ($font->getSize()) { - case 10: - // inspection of Verdana 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Verdana 9 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 8: - // inspection of Verdana 8 workbook says 10.50pt ~14px - $rowHeight = 10.50; - - break; - case 7: - // inspection of Verdana 7 workbook says 9.00pt ~12px - $rowHeight = 9.00; - - break; - case 6: - case 5: - // inspection of Verdana 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Verdana 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Verdana 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Verdana 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Verdana 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - default: - // just use Calibri as an approximation - $rowHeight = 15 * $font->getSize() / 11; - - break; + $name = $font->getName(); + $size = $font->getSize(); + if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$size])) { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$size]['height']; + } elseif ($name === 'Arial' || $name === 'Verdana') { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0; + } else { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0; } return $rowHeight; diff --git a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php index 66111b6c..1ec7f6ab 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php @@ -495,7 +495,7 @@ class EigenvalueDecomposition $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; } - // Complex pair + // Complex pair } else { $this->d[$n - 1] = $x + $p; $this->d[$n] = $x + $p; @@ -671,7 +671,7 @@ class EigenvalueDecomposition } else { $this->H[$i][$n] = -$r / ($eps * $norm); } - // Solve real equations + // Solve real equations } else { $x = $this->H[$i][$i + 1]; $y = $this->H[$i + 1][$i]; @@ -693,7 +693,7 @@ class EigenvalueDecomposition } } } - // Complex vector + // Complex vector } elseif ($q < 0) { $l = $n - 1; // Last vector component imaginary so matrix is triangular diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index ab78ef18..5e35d491 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared\JAMA; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -66,21 +67,21 @@ class Matrix $this->A = $args[0]; break; - //Square matrix - n x n + //Square matrix - n x n case 'integer': $this->m = $args[0]; $this->n = $args[0]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n + //Rectangular matrix - m x n case 'integer,integer': $this->m = $args[0]; $this->n = $args[1]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n initialized from packed array + //Rectangular matrix - m x n initialized from packed array case 'array,integer': $this->m = $args[1]; if ($this->m != 0) { @@ -190,7 +191,7 @@ class Matrix return $R; break; - //A($i0...$iF; $j0...$jF) + //A($i0...$iF; $j0...$jF) case 'integer,integer,integer,integer': [$i0, $iF, $j0, $jF] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -213,7 +214,7 @@ class Matrix return $R; break; - //$R = array of row indices; $C = array of column indices + //$R = array of row indices; $C = array of column indices case 'array,array': [$RL, $CL] = $args; if (count($RL) > 0) { @@ -236,7 +237,7 @@ class Matrix return $R; break; - //A($i0...$iF); $CL = array of column indices + //A($i0...$iF); $CL = array of column indices case 'integer,integer,array': [$i0, $iF, $CL] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -259,7 +260,7 @@ class Matrix return $R; break; - //$RL = array of row indices + //$RL = array of row indices case 'array,integer,integer': [$RL, $j0, $jF] = $args; if (count($RL) > 0) { @@ -532,14 +533,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] += $value; } else { @@ -632,14 +627,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] -= $value; } else { @@ -734,14 +723,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] *= $value; } else { @@ -792,14 +775,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { if ($value == 0) { // Trap for Divide by Zero error @@ -1079,14 +1056,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] = $this->A[$i][$j] ** $value; } else { @@ -1187,4 +1158,20 @@ class Matrix return $L->det(); } + + /** + * @param mixed $value + */ + private function validateExtractedValue($value, bool $validValues): array + { + if (!is_numeric($value) && is_array($value)) { + $value = Functions::flattenArray($value)[0]; + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + + return [$value, $validValues]; + } } diff --git a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php index 6c8999d0..b809bfa1 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -315,7 +315,7 @@ class SingularValueDecomposition } break; - // Split at negligible s(k). + // Split at negligible s(k). case 2: $f = $e[$k - 1]; $e[$k - 1] = 0.0; @@ -336,7 +336,7 @@ class SingularValueDecomposition } break; - // Perform one qr step. + // Perform one qr step. case 3: // Calculate the shift. $scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k])); @@ -396,7 +396,7 @@ class SingularValueDecomposition $iter = $iter + 1; break; - // Convergence. + // Convergence. case 4: // Make the singular values positive. if ($this->s[$k] <= 0.0) { diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index 7d6a990f..16026c3c 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -9,7 +9,7 @@ class StringHelper /** Constants */ /** Regular Expressions */ // Fraction - const STRING_REGEXP_FRACTION = '(-?)(\d+)\s+(\d+\/\d+)'; + const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~'; /** * Control characters array. @@ -28,14 +28,14 @@ class StringHelper /** * Decimal separator. * - * @var string + * @var ?string */ private static $decimalSeparator; /** * Thousands separator. * - * @var string + * @var ?string */ private static $thousandsSeparator; @@ -328,49 +328,48 @@ class StringHelper } /** - * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. - * - * @param string $textValue - * - * @return string + * Try to sanitize UTF8, replacing invalid sequences with Unicode substitution characters. */ - public static function sanitizeUTF8($textValue) + public static function sanitizeUTF8(string $textValue): string { - if (self::getIsIconvEnabled()) { - $textValue = @iconv('UTF-8', 'UTF-8', $textValue); + $textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue); + $subst = mb_substitute_character(); // default is question mark + mb_substitute_character(65533); // Unicode substitution character + // Phpstan does not think this can return false. + $returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); + mb_substitute_character(/** @scrutinizer ignore-type */ $subst); - return $textValue; - } + return self::returnString($returnValue); + } - $textValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); - - return $textValue; + /** + * Strictly to satisfy Scrutinizer. + * + * @param mixed $value + */ + private static function returnString($value): string + { + return is_string($value) ? $value : ''; } /** * Check if a string contains UTF8 data. - * - * @param string $textValue - * - * @return bool */ - public static function isUTF8($textValue) + public static function isUTF8(string $textValue): bool { - return $textValue === '' || preg_match('/^./su', $textValue) === 1; + return $textValue === self::sanitizeUTF8($textValue); } /** * Formats a numeric value as a string for output in various output writers forcing * point as decimal separator in case locale is other than English. * - * @param mixed $numericValue - * - * @return string + * @param float|int|string $numericValue */ - public static function formatNumber($numericValue) + public static function formatNumber($numericValue): string { if (is_float($numericValue)) { - return str_replace(',', '.', $numericValue); + return str_replace(',', '.', (string) $numericValue); } return (string) $numericValue; @@ -385,10 +384,8 @@ class StringHelper * * @param string $textValue UTF-8 encoded string * @param mixed[] $arrcRuns Details of rich text runs in $value - * - * @return string */ - public static function UTF8toBIFF8UnicodeShort($textValue, $arrcRuns = []) + public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -419,10 +416,8 @@ class StringHelper * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function UTF8toBIFF8UnicodeLong($textValue) + public static function UTF8toBIFF8UnicodeLong(string $textValue): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -436,13 +431,10 @@ class StringHelper /** * Convert string from one encoding to another. * - * @param string $textValue * @param string $to Encoding to convert to, e.g. 'UTF-8' * @param string $from Encoding to convert from, e.g. 'UTF-16LE' - * - * @return string */ - public static function convertEncoding($textValue, $to, $from) + public static function convertEncoding(string $textValue, string $to, string $from): string { if (self::getIsIconvEnabled()) { $result = iconv($from, $to . self::$iconvOptions, $textValue); @@ -451,58 +443,51 @@ class StringHelper } } - return mb_convert_encoding($textValue, $to, $from); + return self::returnString(mb_convert_encoding($textValue, $to, $from)); } /** * Get character count. * - * @param string $textValue * @param string $encoding Encoding * * @return int Character count */ - public static function countCharacters($textValue, $encoding = 'UTF-8') + public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int { - return mb_strlen($textValue ?? '', $encoding); + return mb_strlen($textValue, $encoding); } /** * Get a substring of a UTF-8 encoded string. * - * @param null|string $textValue UTF-8 encoded string + * @param string $textValue UTF-8 encoded string * @param int $offset Start offset - * @param int $length Maximum number of characters in substring - * - * @return string + * @param ?int $length Maximum number of characters in substring */ - public static function substring($textValue, $offset, $length = 0) + public static function substring(string $textValue, int $offset, ?int $length = 0): string { - return mb_substr($textValue ?? '', $offset, $length, 'UTF-8'); + return mb_substr($textValue, $offset, $length, 'UTF-8'); } /** * Convert a UTF-8 encoded string to upper case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToUpper($textValue) + public static function strToUpper(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_UPPER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8'); } /** * Convert a UTF-8 encoded string to lower case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToLower($textValue) + public static function strToLower(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_LOWER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_LOWER, 'UTF-8'); } /** @@ -510,24 +495,27 @@ class StringHelper * (uppercase every first character in each word, lower case all other characters). * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToTitle($textValue) + public static function strToTitle(string $textValue): string { return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8'); } - public static function mbIsUpper($character) + public static function mbIsUpper(string $character): bool { - return mb_strtolower($character, 'UTF-8') != $character; + return mb_strtolower($character, 'UTF-8') !== $character; } - public static function mbStrSplit($string) + /** + * Splits a UTF-8 string into an array of individual characters. + */ + public static function mbStrSplit(string $string): array { // Split at all position not after the start: ^ // and not before the end: $ - return preg_split('/(?_calculateFormulaValue($fractionFormula); return true; @@ -578,10 +563,8 @@ class StringHelper /** * Get the decimal separator. If it has not yet been set explicitly, try to obtain number * formatting information from locale. - * - * @return string */ - public static function getDecimalSeparator() + public static function getDecimalSeparator(): string { if (!isset(self::$decimalSeparator)) { $localeconv = localeconv(); @@ -603,7 +586,7 @@ class StringHelper * * @param string $separator Character for decimal separator */ - public static function setDecimalSeparator($separator): void + public static function setDecimalSeparator(string $separator): void { self::$decimalSeparator = $separator; } @@ -611,10 +594,8 @@ class StringHelper /** * Get the thousands separator. If it has not yet been set explicitly, try to obtain number * formatting information from locale. - * - * @return string */ - public static function getThousandsSeparator() + public static function getThousandsSeparator(): string { if (!isset(self::$thousandsSeparator)) { $localeconv = localeconv(); @@ -636,7 +617,7 @@ class StringHelper * * @param string $separator Character for thousands separator */ - public static function setThousandsSeparator($separator): void + public static function setThousandsSeparator(string $separator): void { self::$thousandsSeparator = $separator; } @@ -644,10 +625,8 @@ class StringHelper /** * Get the currency code. If it has not yet been set explicitly, try to obtain the * symbol information from locale. - * - * @return string */ - public static function getCurrencyCode() + public static function getCurrencyCode(): string { if (!empty(self::$currencyCode)) { return self::$currencyCode; @@ -674,7 +653,7 @@ class StringHelper * * @param string $currencyCode Character for currency code */ - public static function setCurrencyCode($currencyCode): void + public static function setCurrencyCode(string $currencyCode): void { self::$currencyCode = $currencyCode; } @@ -682,11 +661,11 @@ class StringHelper /** * Convert SYLK encoded string to UTF-8. * - * @param string $textValue + * @param string $textValue SYLK encoded string * * @return string UTF-8 encoded string */ - public static function SYLKtoUTF8($textValue) + public static function SYLKtoUTF8(string $textValue): string { self::buildCharacterSets(); @@ -717,6 +696,6 @@ class StringHelper } $v = (float) $textValue; - return (is_numeric(substr($textValue, 0, strlen($v)))) ? $v : $textValue; + return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue; } } diff --git a/src/PhpSpreadsheet/Shared/TimeZone.php b/src/PhpSpreadsheet/Shared/TimeZone.php index dabb88f2..324e3424 100644 --- a/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/src/PhpSpreadsheet/Shared/TimeZone.php @@ -21,9 +21,9 @@ class TimeZone * * @return bool Success or failure */ - private static function validateTimeZone($timezoneName) + private static function validateTimeZone(string $timezoneName): bool { - return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)); + return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true); } /** @@ -33,9 +33,9 @@ class TimeZone * * @return bool Success or failure */ - public static function setTimeZone($timezoneName) + public static function setTimeZone(string $timezoneName): bool { - if (self::validateTimezone($timezoneName)) { + if (self::validateTimeZone($timezoneName)) { self::$timezone = $timezoneName; return true; @@ -49,7 +49,7 @@ class TimeZone * * @return string Timezone (e.g. 'Europe/London') */ - public static function getTimeZone() + public static function getTimeZone(): string { return self::$timezone; } @@ -63,11 +63,11 @@ class TimeZone * * @return int Number of seconds for timezone adjustment */ - public static function getTimeZoneAdjustment($timezoneName, $timestamp) + public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int { $timezoneName = $timezoneName ?? self::$timezone; $dtobj = Date::dateTimeFromTimestamp("$timestamp"); - if (!self::validateTimezone($timezoneName)) { + if (!self::validateTimeZone($timezoneName)) { throw new PhpSpreadsheetException("Invalid timezone $timezoneName"); } $dtobj->setTimeZone(new DateTimeZone($timezoneName)); diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 33b4fe0c..364700e2 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet { @@ -721,6 +724,19 @@ class Spreadsheet return null; } + /** + * Get sheet by name, throwing exception if not found. + */ + public function getSheetByNameOrThrow(string $worksheetName): Worksheet + { + $worksheet = $this->getSheetByName($worksheetName); + if ($worksheet === null) { + throw new Exception("Sheet $worksheetName does not exist."); + } + + return $worksheet; + } + /** * Get index for sheet. * @@ -1120,28 +1136,24 @@ class Spreadsheet */ public function copy() { - $copied = clone $this; + $filename = File::temporaryFilename(); + $writer = new XlsxWriter($this); + $writer->setIncludeCharts(true); + $writer->save($filename); - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); - $this->workSheetCollection[$i]->rebindParent($this); - } + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $reloadedSpreadsheet = $reader->load($filename); + unlink($filename); - return $copied; + return $reloadedSpreadsheet; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ public function __clone() { - // @phpstan-ignore-next-line - foreach ($this as $key => $val) { - if (is_object($val) || (is_array($val))) { - $this->{$key} = unserialize(serialize($val)); - } - } + throw new Exception( + 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.' + ); } /** @@ -1562,7 +1574,7 @@ class Spreadsheet * Workbook window is hidden and cannot be shown in the * user interface. * - * @param string $visibility visibility status of the workbook + * @param null|string $visibility visibility status of the workbook */ public function setVisibility($visibility): void { @@ -1596,7 +1608,7 @@ class Spreadsheet */ public function setTabRatio($tabRatio): void { - if ($tabRatio >= 0 || $tabRatio <= 1000) { + if ($tabRatio >= 0 && $tabRatio <= 1000) { $this->tabRatio = (int) $tabRatio; } else { throw new Exception('Tab ratio must be between 0 and 1000.'); diff --git a/src/PhpSpreadsheet/Style/Alignment.php b/src/PhpSpreadsheet/Style/Alignment.php index 83ac5b0d..68edfaca 100644 --- a/src/PhpSpreadsheet/Style/Alignment.php +++ b/src/PhpSpreadsheet/Style/Alignment.php @@ -15,6 +15,27 @@ class Alignment extends Supervisor const HORIZONTAL_JUSTIFY = 'justify'; const HORIZONTAL_FILL = 'fill'; const HORIZONTAL_DISTRIBUTED = 'distributed'; // Excel2007 only + private const HORIZONTAL_CENTER_CONTINUOUS_LC = 'centercontinuous'; + // Mapping for horizontal alignment + const HORIZONTAL_ALIGNMENT_FOR_XLSX = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER_CONTINUOUS, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_DISTRIBUTED, + ]; + // Mapping for horizontal alignment CSS + const HORIZONTAL_ALIGNMENT_FOR_HTML = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + //self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, // no reasonable equivalent for fill + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_JUSTIFY, + ]; // Vertical alignment styles const VERTICAL_BOTTOM = 'bottom'; @@ -22,6 +43,45 @@ class Alignment extends Supervisor const VERTICAL_CENTER = 'center'; const VERTICAL_JUSTIFY = 'justify'; const VERTICAL_DISTRIBUTED = 'distributed'; // Excel2007 only + // Vertical alignment CSS + private const VERTICAL_BASELINE = 'baseline'; + private const VERTICAL_MIDDLE = 'middle'; + private const VERTICAL_SUB = 'sub'; + private const VERTICAL_SUPER = 'super'; + private const VERTICAL_TEXT_BOTTOM = 'text-bottom'; + private const VERTICAL_TEXT_TOP = 'text-top'; + + // Mapping for vertical alignment + const VERTICAL_ALIGNMENT_FOR_XLSX = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_CENTER, + self::VERTICAL_JUSTIFY => self::VERTICAL_JUSTIFY, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_DISTRIBUTED, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BOTTOM, + self::VERTICAL_MIDDLE => self::VERTICAL_CENTER, + self::VERTICAL_SUB => self::VERTICAL_BOTTOM, + self::VERTICAL_SUPER => self::VERTICAL_TOP, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TOP, + ]; + + // Mapping for vertical alignment for Html + const VERTICAL_ALIGNMENT_FOR_HTML = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_MIDDLE, + self::VERTICAL_JUSTIFY => self::VERTICAL_MIDDLE, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_MIDDLE, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BASELINE, + self::VERTICAL_MIDDLE => self::VERTICAL_MIDDLE, + self::VERTICAL_SUB => self::VERTICAL_SUB, + self::VERTICAL_SUPER => self::VERTICAL_SUPER, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_TEXT_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TEXT_TOP, + ]; // Read order const READORDER_CONTEXT = 0; @@ -202,8 +262,9 @@ class Alignment extends Supervisor */ public function setHorizontal(string $horizontalAlignment) { - if ($horizontalAlignment == '') { - $horizontalAlignment = self::HORIZONTAL_GENERAL; + $horizontalAlignment = strtolower($horizontalAlignment); + if ($horizontalAlignment === self::HORIZONTAL_CENTER_CONTINUOUS_LC) { + $horizontalAlignment = self::HORIZONTAL_CENTER_CONTINUOUS; } if ($this->isSupervisor) { @@ -239,9 +300,7 @@ class Alignment extends Supervisor */ public function setVertical($verticalAlignment) { - if ($verticalAlignment == '') { - $verticalAlignment = self::VERTICAL_BOTTOM; - } + $verticalAlignment = strtolower($verticalAlignment); if ($this->isSupervisor) { $styleArray = $this->getStyleArray(['vertical' => $verticalAlignment]); diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index 9ab0c98f..922be803 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -387,8 +387,6 @@ class Color extends Supervisor * @param int $colorIndex Index entry point into the colour array * @param bool $background Flag to indicate whether default background or foreground colour * should be returned if the indexed colour doesn't exist - * - * @return Color */ public static function indexedColor($colorIndex, $background = false, ?array $palette = null): self { diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php index 54513670..f7a2eee1 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -11,10 +11,10 @@ class ConditionalDataBar /** children */ - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $minimumConditionalFormatValueObject; - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $maximumConditionalFormatValueObject; /** @var string */ @@ -22,7 +22,7 @@ class ConditionalDataBar /** */ - /** @var ConditionalFormattingRuleExtension */ + /** @var ?ConditionalFormattingRuleExtension */ private $conditionalFormattingRuleExt; /** @@ -43,10 +43,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMinimumConditionalFormatValueObject() + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->minimumConditionalFormatValueObject; } @@ -58,10 +55,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMaximumConditionalFormatValueObject() + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->maximumConditionalFormatValueObject; } @@ -85,10 +79,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormattingRuleExtension - */ - public function getConditionalFormattingRuleExt() + public function getConditionalFormattingRuleExt(): ?ConditionalFormattingRuleExtension { return $this->conditionalFormattingRuleExt; } diff --git a/src/PhpSpreadsheet/Style/Font.php b/src/PhpSpreadsheet/Style/Font.php index 7b1ced63..3d7bc1bc 100644 --- a/src/PhpSpreadsheet/Style/Font.php +++ b/src/PhpSpreadsheet/Style/Font.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Style; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; + class Font extends Supervisor { // Underline types @@ -37,11 +39,11 @@ class Font extends Supervisor /** @var string */ private $strikeType = ''; - /** @var string */ - private $uSchemeClr = ''; + /** @var ?ChartColor */ + private $underlineColor; - /** @var string */ - private $uSrgbClr = ''; + /** @var ?ChartColor */ + private $chartColor; // end of chart title items /** @@ -372,7 +374,7 @@ class Font extends Supervisor * * @return $this */ - public function setSize($sizeInPoints) + public function setSize($sizeInPoints, bool $nullOk = false) { if (is_string($sizeInPoints) || is_int($sizeInPoints)) { $sizeInPoints = (float) $sizeInPoints; // $pValue = 0 if given string is not numeric @@ -381,7 +383,9 @@ class Font extends Supervisor // Size must be a positive floating point number // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 if (!is_float($sizeInPoints) || !($sizeInPoints > 0)) { - $sizeInPoints = 10.0; + if (!$nullOk || $sizeInPoints !== null) { + $sizeInPoints = 10.0; + } } if ($this->isSupervisor) { @@ -582,23 +586,23 @@ class Font extends Supervisor return $this; } - public function getUSchemeClr(): string + public function getUnderlineColor(): ?ChartColor { if ($this->isSupervisor) { - return $this->getSharedComponent()->getUSchemeClr(); + return $this->getSharedComponent()->getUnderlineColor(); } - return $this->uSchemeClr; + return $this->underlineColor; } - public function setUSchemeClr(string $uSchemeClr): self + public function setUnderlineColor(array $colorArray): self { if (!$this->isSupervisor) { - $this->uSchemeClr = $uSchemeClr; + $this->underlineColor = new ChartColor($colorArray); } else { // should never be true // @codeCoverageIgnoreStart - $styleArray = $this->getStyleArray(['uSchemeClr' => $uSchemeClr]); + $styleArray = $this->getStyleArray(['underlineColor' => $colorArray]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); // @codeCoverageIgnoreEnd } @@ -606,23 +610,23 @@ class Font extends Supervisor return $this; } - public function getUSrgbClr(): string + public function getChartColor(): ?ChartColor { if ($this->isSupervisor) { - return $this->getSharedComponent()->getUSrgbClr(); + return $this->getSharedComponent()->getChartColor(); } - return $this->uSrgbClr; + return $this->chartColor; } - public function setUSrgbClr(string $uSrgbClr): self + public function setChartColor(array $colorArray): self { if (!$this->isSupervisor) { - $this->uSrgbClr = $uSrgbClr; + $this->chartColor = new ChartColor($colorArray); } else { // should never be true // @codeCoverageIgnoreStart - $styleArray = $this->getStyleArray(['uSrgbClr' => $uSrgbClr]); + $styleArray = $this->getStyleArray(['chartColor' => $colorArray]); $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); // @codeCoverageIgnoreEnd } @@ -737,6 +741,18 @@ class Font extends Supervisor return $this; } + private function hashChartColor(?ChartColor $underlineColor): string + { + if ($underlineColor === null) { + return ''; + } + + return + $underlineColor->getValue() + . $underlineColor->getType() + . (string) $underlineColor->getAlpha(); + } + /** * Get hash code. * @@ -765,8 +781,8 @@ class Font extends Supervisor $this->eastAsian, $this->complexScript, $this->strikeType, - $this->uSchemeClr, - $this->uSrgbClr, + $this->hashChartColor($this->chartColor), + $this->hashChartColor($this->underlineColor), (string) $this->baseLine, ] ) . @@ -779,6 +795,7 @@ class Font extends Supervisor $exportedArray = []; $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); $this->exportArray2($exportedArray, 'color', $this->getColor()); $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); @@ -791,8 +808,7 @@ class Font extends Supervisor $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); - $this->exportArray2($exportedArray, 'uSchemeClr', $this->getUSchemeClr()); - $this->exportArray2($exportedArray, 'uSrgbClr', $this->getUSrgbClr()); + $this->exportArray2($exportedArray, 'underlineColor', $this->getUnderlineColor()); return $exportedArray; } diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 3e4bdc46..be195a88 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -112,6 +112,9 @@ class Formatter return $value; } + // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc + $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format); + $format = (string) preg_replace_callback( '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', function ($matches) { diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index f4d3412b..07aaff1f 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -20,7 +20,8 @@ class PercentageFormatter extends BaseFormatter $format = str_replace('%', '%%', $format); $wholePartSize = strlen((string) floor($value)); - $decimalPartSize = $placeHolders = 0; + $decimalPartSize = 0; + $placeHolders = ''; // Number of decimals if (preg_match('/\.([?0]+)/u', $format, $matches)) { $decimalPartSize = strlen($matches[1]); @@ -29,12 +30,13 @@ class PercentageFormatter extends BaseFormatter $placeHolders = str_repeat(' ', strlen($matches[1]) - $decimalPartSize); } // Number of digits to display before the decimal - if (preg_match('/([#0,]+)\./u', $format, $matches)) { - $wholePartSize = max($wholePartSize, strlen($matches[1])); + if (preg_match('/([#0,]+)\.?/u', $format, $matches)) { + $firstZero = preg_replace('/^[#,]*/', '', $matches[1]); + $wholePartSize = max($wholePartSize, strlen($firstZero)); } - $wholePartSize += $decimalPartSize; - $replacement = "{$wholePartSize}.{$decimalPartSize}"; + $wholePartSize += $decimalPartSize + (int) ($decimalPartSize > 0); + $replacement = "0{$wholePartSize}.{$decimalPartSize}"; $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); /** @var float */ diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 05b2e9a0..d6041985 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -9,7 +9,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; @@ -104,7 +104,7 @@ class AutoFilter * Set AutoFilter Cell Range. * * @param AddressRange|array|string $range - * A simple string containing a Cell range like 'A1:E10' is permitted + * A simple string containing a Cell range like 'A1:E10' or a Cell address like 'A1' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. */ @@ -115,6 +115,7 @@ class AutoFilter if ($range !== '') { [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); } + if (empty($range)) { // Discard all column rules $this->columns = []; @@ -123,8 +124,8 @@ class AutoFilter return $this; } - if (strpos($range, ':') === false) { - throw new PhpSpreadsheetException('Autofilter must be set on a range of cells.'); + if (ctype_digit($range) || ctype_alpha($range)) { + throw new Exception("{$range} is an invalid range for AutoFilter"); } $this->range = $range; @@ -174,13 +175,13 @@ class AutoFilter public function testColumnInRange($column) { if (empty($this->range)) { - throw new PhpSpreadsheetException('No autofilter range is defined.'); + throw new Exception('No autofilter range is defined.'); } $columnIndex = Coordinate::columnIndexFromString($column); [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) { - throw new PhpSpreadsheetException('Column is outside of current autofilter range.'); + throw new Exception('Column is outside of current autofilter range.'); } return $columnIndex - $rangeStart[0]; @@ -247,7 +248,7 @@ class AutoFilter } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof AutoFilter\Column)) { $column = $columnObjectOrString->getColumnIndex(); } else { - throw new PhpSpreadsheetException('Column is not within the autofilter range.'); + throw new Exception('Column is not within the autofilter range.'); } $this->testColumnInRange($column); @@ -1031,6 +1032,8 @@ class AutoFilter } } + $rangeEnd[1] = $this->autoExtendRange($rangeStart[1], $rangeEnd[1]); + // Execute the column tests for each row in the autoFilter range to determine show/hide, for ($row = $rangeStart[1] + 1; $row <= $rangeEnd[1]; ++$row) { $result = true; @@ -1053,6 +1056,29 @@ class AutoFilter return $this; } + /** + * Magic Range Auto-sizing. + * For a single row rangeSet, we follow MS Excel rules, and search for the first empty row to determine our range. + */ + public function autoExtendRange(int $startRow, int $endRow): int + { + if ($startRow === $endRow && $this->workSheet !== null) { + try { + $rowIterator = $this->workSheet->getRowIterator($startRow + 1); + } catch (Exception $e) { + // If there are no rows below $startRow + return $startRow; + } + foreach ($rowIterator as $row) { + if ($row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL) === true) { + return $row->getRowIndex() - 1; + } + } + } + + return $endRow; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 815536b5..369e4162 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -9,8 +9,8 @@ use PhpOffice\PhpSpreadsheet\IComparable; class BaseDrawing implements IComparable { const EDIT_AS_ABSOLUTE = 'absolute'; - const EDIT_AS_ONECELL = 'onecell'; - const EDIT_AS_TWOCELL = 'twocell'; + const EDIT_AS_ONECELL = 'oneCell'; + const EDIT_AS_TWOCELL = 'twoCell'; private const VALID_EDIT_AS = [ self::EDIT_AS_ABSOLUTE, self::EDIT_AS_ONECELL, @@ -530,6 +530,6 @@ class BaseDrawing implements IComparable public function validEditAs(): bool { - return in_array($this->editAs, self::VALID_EDIT_AS); + return in_array($this->editAs, self::VALID_EDIT_AS, true); } } diff --git a/src/PhpSpreadsheet/Worksheet/CellIterator.php b/src/PhpSpreadsheet/Worksheet/CellIterator.php index 142639ce..94877f66 100644 --- a/src/PhpSpreadsheet/Worksheet/CellIterator.php +++ b/src/PhpSpreadsheet/Worksheet/CellIterator.php @@ -8,10 +8,15 @@ use PhpOffice\PhpSpreadsheet\Collection\Cells; /** * @template TKey + * * @implements Iterator */ abstract class CellIterator implements Iterator { + public const TREAT_NULL_VALUE_AS_EMPTY_CELL = 1; + + public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 2; + /** * Worksheet to iterate. * diff --git a/src/PhpSpreadsheet/Worksheet/Column.php b/src/PhpSpreadsheet/Worksheet/Column.php index b6f30f1f..e6e332ae 100644 --- a/src/PhpSpreadsheet/Worksheet/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Column.php @@ -9,7 +9,7 @@ class Column * * @var Worksheet */ - private $parent; + private $worksheet; /** * Column index. @@ -23,10 +23,10 @@ class Column * * @param string $columnIndex */ - public function __construct(?Worksheet $parent = null, $columnIndex = 'A') + public function __construct(Worksheet $worksheet, $columnIndex = 'A') { // Set parent and column index - $this->parent = $parent; + $this->worksheet = $worksheet; $this->columnIndex = $columnIndex; } @@ -36,7 +36,7 @@ class Column public function __destruct() { // @phpstan-ignore-next-line - $this->parent = null; + $this->worksheet = null; } /** @@ -57,6 +57,53 @@ class Column */ public function getCellIterator($startRow = 1, $endRow = null) { - return new ColumnCellIterator($this->parent, $this->columnIndex, $startRow, $endRow); + return new ColumnCellIterator($this->worksheet, $this->columnIndex, $startRow, $endRow); + } + + /** + * Returns a boolean true if the column contains no cells. By default, this means that no cell records exist in the + * collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + + /** + * Returns bound worksheet. + */ + public function getWorksheet(): Worksheet + { + return $this->worksheet; } } diff --git a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index e65541dc..b2d1aa69 100644 --- a/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -86,8 +86,8 @@ class MemoryDrawing extends BaseDrawing return; } - $width = imagesx($this->imageResource); - $height = imagesy($this->imageResource); + $width = (int) imagesx($this->imageResource); + $height = (int) imagesy($this->imageResource); if (imageistruecolor($this->imageResource)) { $clone = imagecreatetruecolor($width, $height); @@ -150,8 +150,8 @@ class MemoryDrawing extends BaseDrawing if ($this->imageResource !== null) { // Get width/height - $this->width = imagesx($this->imageResource); - $this->height = imagesy($this->imageResource); + $this->width = (int) imagesx($this->imageResource); + $this->height = (int) imagesy($this->imageResource); } return $this; diff --git a/src/PhpSpreadsheet/Worksheet/PageSetup.php b/src/PhpSpreadsheet/Worksheet/PageSetup.php index c23bfc59..4bdc2d4c 100644 --- a/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -259,10 +259,11 @@ class PageSetup /** * First page number. * - * @var int + * @var ?int */ private $firstPageNumber; + /** @var string */ private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER; /** @@ -375,7 +376,7 @@ class PageSetup { // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, // but it is apparently still able to handle any scale >= 0, where 0 results in 100 - if (($scale >= 0) || $scale === null) { + if ($scale === null || $scale >= 0) { $this->scale = $scale; if ($update) { $this->fitToPage = false; @@ -845,7 +846,7 @@ class PageSetup /** * Get first page number. * - * @return int + * @return ?int */ public function getFirstPageNumber() { @@ -855,7 +856,7 @@ class PageSetup /** * Set first page number. * - * @param int $value + * @param ?int $value * * @return $this */ diff --git a/src/PhpSpreadsheet/Worksheet/Row.php b/src/PhpSpreadsheet/Worksheet/Row.php index a5f8f326..5c162752 100644 --- a/src/PhpSpreadsheet/Worksheet/Row.php +++ b/src/PhpSpreadsheet/Worksheet/Row.php @@ -23,7 +23,7 @@ class Row * * @param int $rowIndex */ - public function __construct(?Worksheet $worksheet = null, $rowIndex = 1) + public function __construct(Worksheet $worksheet, $rowIndex = 1) { // Set parent and row index $this->worksheet = $worksheet; @@ -59,6 +59,45 @@ class Row return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn); } + /** + * Returns a boolean true if the row contains no cells. By default, this means that no cell records exist in the + * collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + /** * Returns bound worksheet. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index d047380a..8235ccf1 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -32,14 +32,20 @@ use PhpOffice\PhpSpreadsheet\Style\Style; class Worksheet implements IComparable { // Break types - const BREAK_NONE = 0; - const BREAK_ROW = 1; - const BREAK_COLUMN = 2; + public const BREAK_NONE = 0; + public const BREAK_ROW = 1; + public const BREAK_COLUMN = 2; // Sheet state - const SHEETSTATE_VISIBLE = 'visible'; - const SHEETSTATE_HIDDEN = 'hidden'; - const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const SHEETSTATE_VISIBLE = 'visible'; + public const SHEETSTATE_HIDDEN = 'hidden'; + public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + + public const MERGE_CELL_CONTENT_EMPTY = 'empty'; + public const MERGE_CELL_CONTENT_HIDE = 'hide'; + public const MERGE_CELL_CONTENT_MERGE = 'merge'; + + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** * Maximum 31 characters allowed for sheet title. @@ -658,10 +664,8 @@ class Worksheet implements IComparable */ public function refreshColumnDimensions() { - $currentColumnDimensions = $this->getColumnDimensions(); $newColumnDimensions = []; - - foreach ($currentColumnDimensions as $objColumnDimension) { + foreach ($this->getColumnDimensions() as $objColumnDimension) { $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension; } @@ -677,10 +681,8 @@ class Worksheet implements IComparable */ public function refreshRowDimensions() { - $currentRowDimensions = $this->getRowDimensions(); $newRowDimensions = []; - - foreach ($currentRowDimensions as $objRowDimension) { + foreach ($this->getRowDimensions() as $objRowDimension) { $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension; } @@ -921,7 +923,7 @@ class Worksheet implements IComparable $this->parent->getCalculationEngine() ->renameCalculationCacheForWorksheet($oldTitle, $newTitle); if ($updateFormulaCellReferences) { - ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle); + ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle); } } @@ -1269,7 +1271,7 @@ class Worksheet implements IComparable } } elseif ( !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) && - preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $coordinate) + preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate) ) { // Named range? $namedRange = $this->validateNamedRange($coordinate, true); @@ -1340,7 +1342,7 @@ class Worksheet implements IComparable * * @return Cell Cell that was created */ - public function createNewCell($coordinate) + public function createNewCell($coordinate): Cell { [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate); $cell = new Cell(null, DataType::TYPE_NULL, $this); @@ -1361,7 +1363,7 @@ class Worksheet implements IComparable if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) { // then there is a row dimension with explicit style, assign it to the cell - $cell->setXfIndex($rowDimension->getXfIndex()); + $cell->setXfIndex(/** @scrutinizer ignore-type */ $rowDimension->getXfIndex()); } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) { // then there is a column dimension, assign it to the cell $cell->setXfIndex($columnDimension->getXfIndex()); @@ -1417,6 +1419,11 @@ class Worksheet implements IComparable return $this->rowDimensions[$row]; } + public function rowDimensionExists(int $row): bool + { + return isset($this->rowDimensions[$row]); + } + /** * Get column dimension at a specific column. * @@ -1755,10 +1762,15 @@ class Worksheet implements IComparable * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCells($range) + public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); @@ -1790,18 +1802,22 @@ class Worksheet implements IComparable $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); } - // Blank out the rest of the cells in the range (if they exist) - if ($numberRows > $numberColumns) { - $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft); - } else { - $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft); + if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) { + // Blank out the rest of the cells in the range (if they exist) + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour); + } else { + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour); + } } return $this; } - private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) { $iterator = $column->getCellIterator($firstRow); $iterator->setIterateOnlyExistingCells(true); @@ -1811,17 +1827,21 @@ class Worksheet implements IComparable if ($row > $lastRow) { break; } - $thisCell = $cell->getColumn() . $row; - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } } - private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft): void + private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + foreach ($this->getRowIterator($firstRow, $lastRow) as $row) { $iterator = $row->getCellIterator($firstColumn); $iterator->setIterateOnlyExistingCells(true); @@ -1832,13 +1852,31 @@ class Worksheet implements IComparable if ($columnIndex > $lastColumnIndex) { break; } - $thisCell = $column . $cell->getRow(); - if ($upperLeft !== $thisCell) { - $cell->setValueExplicit(null, DataType::TYPE_NULL); - } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); } } } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } + } + + public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array + { + if ($cell->getCoordinate() !== $upperLeft) { + Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance(); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $cellValue = $cell->getFormattedValue(); + if ($cellValue !== '') { + $leftCellValue[] = $cellValue; + } + } + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + + return $leftCellValue; } /** @@ -1853,17 +1891,22 @@ class Worksheet implements IComparable * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $row2 Numeric row coordinate of the last cell + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { $cellRange = new CellRange( CellAddress::fromColumnAndRow($columnIndex1, $row1), CellAddress::fromColumnAndRow($columnIndex2, $row2) ); - return $this->mergeCells($cellRange); + return $this->mergeCells($cellRange, $behaviour); } /** @@ -2458,10 +2501,8 @@ class Worksheet implements IComparable /** * Show gridlines? - * - * @return bool */ - public function getShowGridlines() + public function getShowGridlines(): bool { return $this->showGridlines; } @@ -2473,7 +2514,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowGridlines($showGridLines) + public function setShowGridlines(bool $showGridLines): self { $this->showGridlines = $showGridLines; @@ -2482,10 +2523,8 @@ class Worksheet implements IComparable /** * Print gridlines? - * - * @return bool */ - public function getPrintGridlines() + public function getPrintGridlines(): bool { return $this->printGridlines; } @@ -2497,7 +2536,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setPrintGridlines($printGridLines) + public function setPrintGridlines(bool $printGridLines): self { $this->printGridlines = $printGridLines; @@ -2506,10 +2545,8 @@ class Worksheet implements IComparable /** * Show row and column headers? - * - * @return bool */ - public function getShowRowColHeaders() + public function getShowRowColHeaders(): bool { return $this->showRowColHeaders; } @@ -2521,7 +2558,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowRowColHeaders($showRowColHeaders) + public function setShowRowColHeaders(bool $showRowColHeaders): self { $this->showRowColHeaders = $showRowColHeaders; @@ -2530,10 +2567,8 @@ class Worksheet implements IComparable /** * Show summary below? (Row/Column outlining). - * - * @return bool */ - public function getShowSummaryBelow() + public function getShowSummaryBelow(): bool { return $this->showSummaryBelow; } @@ -2545,7 +2580,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowSummaryBelow($showSummaryBelow) + public function setShowSummaryBelow(bool $showSummaryBelow): self { $this->showSummaryBelow = $showSummaryBelow; @@ -2554,10 +2589,8 @@ class Worksheet implements IComparable /** * Show summary right? (Row/Column outlining). - * - * @return bool */ - public function getShowSummaryRight() + public function getShowSummaryRight(): bool { return $this->showSummaryRight; } @@ -2569,7 +2602,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowSummaryRight($showSummaryRight) + public function setShowSummaryRight(bool $showSummaryRight): self { $this->showSummaryRight = $showSummaryRight; @@ -2593,22 +2626,47 @@ class Worksheet implements IComparable * * @return $this */ - public function setComments(array $comments) + public function setComments(array $comments): self { $this->comments = $comments; return $this; } + /** + * Remove comment from cell. + * + * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * + * @return $this + */ + public function removeComment($cellCoordinate): self + { + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); + + if (Coordinate::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells.'); + } elseif (strpos($cellAddress, '$') !== false) { + throw new Exception('Cell coordinate string must not be absolute.'); + } elseif ($cellAddress == '') { + throw new Exception('Cell coordinate can not be zero-length string.'); + } + // Check if we have a comment for this cell and delete it + if (isset($this->comments[$cellAddress])) { + unset($this->comments[$cellAddress]); + } + + return $this; + } + /** * Get comment for cell. * * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. - * - * @return Comment */ - public function getComment($cellCoordinate) + public function getComment($cellCoordinate): Comment { $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); @@ -2641,10 +2699,8 @@ class Worksheet implements IComparable * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell - * - * @return Comment */ - public function getCommentByColumnAndRow($columnIndex, $row) + public function getCommentByColumnAndRow($columnIndex, $row): Comment { return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row); } @@ -3023,7 +3079,11 @@ class Worksheet implements IComparable * Extract worksheet title from range. * * Example: extractSheetTitle("testSheet!A1") ==> 'A1' + * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3' * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1']; + * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3']; + * Example: extractSheetTitle("A1", true) ==> ['', 'A1']; + * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3'] * * @param string $range Range to extract title from * @param bool $returnRange Return range? (see example) @@ -3250,6 +3310,66 @@ class Worksheet implements IComparable return clone $this; } + /** + * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records + * exist in the collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new RowIterator($this, $rowId, $rowId); + $iterator->seek($rowId); + $row = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $row->isEmpty($definitionOfEmptyFlags); + } + + /** + * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records + * exist in the collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new ColumnIterator($this, $columnId, $columnId); + $iterator->seek($columnId); + $column = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $column->isEmpty($definitionOfEmptyFlags); + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ @@ -3362,4 +3482,9 @@ class Worksheet implements IComparable { return $this->codeName !== null; } + + public static function nameRequiresQuotes(string $sheetName): bool + { + return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1; + } } diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 3ef65928..0fef0f60 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -24,6 +24,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Html extends BaseWriter @@ -54,7 +55,7 @@ class Html extends BaseWriter * * @var bool */ - private $embedImages = false; + protected $embedImages = false; /** * Use inline CSS? @@ -126,6 +127,13 @@ class Html extends BaseWriter */ protected $isPdf = false; + /** + * Is the current writer creating mPDF? + * + * @var bool + */ + protected $isMPdf = false; + /** * Generate the Navigation block. * @@ -223,13 +231,6 @@ class Html extends BaseWriter $this->editHtmlCallback = $callback; } - const VALIGN_ARR = [ - Alignment::VERTICAL_BOTTOM => 'bottom', - Alignment::VERTICAL_TOP => 'top', - Alignment::VERTICAL_CENTER => 'middle', - Alignment::VERTICAL_JUSTIFY => 'middle', - ]; - /** * Map VAlign. * @@ -239,17 +240,9 @@ class Html extends BaseWriter */ private function mapVAlign($vAlign) { - return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline'; + return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? ''; } - const HALIGN_ARR = [ - Alignment::HORIZONTAL_LEFT => 'left', - Alignment::HORIZONTAL_RIGHT => 'right', - Alignment::HORIZONTAL_CENTER => 'center', - Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center', - Alignment::HORIZONTAL_JUSTIFY => 'justify', - ]; - /** * Map HAlign. * @@ -259,7 +252,7 @@ class Html extends BaseWriter */ private function mapHAlign($hAlign) { - return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : ''; + return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? ''; } const BORDER_ARR = [ @@ -280,7 +273,7 @@ class Html extends BaseWriter /** * Map border style. * - * @param int $borderStyle Sheet index + * @param int|string $borderStyle Sheet index * * @return string */ @@ -347,7 +340,7 @@ class Html extends BaseWriter return $this; } - private static function generateMeta($val, $desc) + private static function generateMeta(?string $val, string $desc): string { return $val ? (' ' . PHP_EOL) @@ -391,7 +384,7 @@ class Html extends BaseWriter return $html; } - private function generateSheetPrep() + private function generateSheetPrep(): array { // Ensure that Spans have been calculated? $this->calculateSpans(); @@ -406,7 +399,7 @@ class Html extends BaseWriter return $sheets; } - private function generateSheetStarts($sheet, $rowMin) + private function generateSheetStarts(Worksheet $sheet, int $rowMin): array { // calculate start of , $tbodyStart = $rowMin; @@ -425,7 +418,7 @@ class Html extends BaseWriter return [$theadStart, $theadEnd, $tbodyStart]; } - private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart) + private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array { // ? $startTag = ($row == $theadStart) ? (' ' . PHP_EOL) : ''; @@ -544,15 +537,10 @@ class Html extends BaseWriter * Extend Row if chart is placed after nominal end of row. * This code should be exercised by sample: * Chart/32_Chart_read_write_PDF.php. - * However, that test is suppressed due to out-of-date - * Jpgraph code issuing warnings. So, don't measure - * code coverage for this function till that is fixed. * * @param int $row Row to check for charts * * @return array - * - * @codeCoverageIgnore */ private function extendRowsForCharts(Worksheet $worksheet, int $row) { @@ -628,11 +616,12 @@ class Html extends BaseWriter * * @return string */ - public static function winFileToUrl($filename) + public static function winFileToUrl($filename, bool $mpdf = false) { // Windows filename if (substr($filename, 1, 2) === ':\\') { - $filename = 'file:///' . str_replace('\\', '/', $filename); + $protocol = $mpdf ? '' : 'file:///'; + $filename = $protocol . str_replace('\\', '/', $filename); } return $filename; @@ -674,12 +663,12 @@ class Html extends BaseWriter $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); $html .= PHP_EOL; - $imageData = self::winFileToUrl($filename); + $imageData = self::winFileToUrl($filename, $this->isMPdf); - if (($this->embedImages && !$this->isPdf) || substr($imageData, 0, 6) === 'zip://') { + if ($this->embedImages || substr($imageData, 0, 6) === 'zip://') { $picture = @file_get_contents($filename); if ($picture !== false) { - $imageDetails = getimagesize($filename); + $imageDetails = getimagesize($filename) ?: []; // base64 encode the binary data $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; @@ -694,12 +683,10 @@ class Html extends BaseWriter $imageResource = $drawing->getImageResource(); if ($imageResource) { ob_start(); // Let's start output buffering. - // @phpstan-ignore-next-line imagepng($imageResource); // This will normally output the image, but because of ob_start(), it won't. - $contents = ob_get_contents(); // Instead, output above is saved to $contents + $contents = (string) ob_get_contents(); // Instead, output above is saved to $contents ob_end_clean(); // End the output buffer. - /** @phpstan-ignore-next-line */ $dataUri = 'data:image/png;base64,' . base64_encode($contents); // Because of the nature of tables, width is more important than height. @@ -718,11 +705,6 @@ class Html extends BaseWriter * Generate chart tag in cell. * This code should be exercised by sample: * Chart/32_Chart_read_write_PDF.php. - * However, that test is suppressed due to out-of-date - * Jpgraph code issuing warnings. So, don't measure - * code coverage for this function till that is fixed. - * - * @codeCoverageIgnore */ private function writeChartInCell(Worksheet $worksheet, string $coordinates): string { @@ -740,21 +722,18 @@ class Html extends BaseWriter } $html .= PHP_EOL; - $imageDetails = getimagesize($chartFileName); + $imageDetails = getimagesize($chartFileName) ?: []; $filedesc = $chart->getTitle(); $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; - if ($fp = fopen($chartFileName, 'rb', 0)) { - $picture = fread($fp, filesize($chartFileName)); - fclose($fp); - /** @phpstan-ignore-next-line */ + $picture = file_get_contents($chartFileName); + if ($picture !== false) { $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; $html .= '' . $filedesc . '' . PHP_EOL; - - unlink($chartFileName); } + unlink($chartFileName); } } } @@ -942,8 +921,8 @@ class Html extends BaseWriter // Calculate cell style hashes foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { - $css['td.style' . $index] = $this->createCSSStyle($style); - $css['th.style' . $index] = $this->createCSSStyle($style); + $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style); + //$css['th.style' . $index] = $this->createCSSStyle($style); } // Fetch sheets @@ -995,14 +974,25 @@ class Html extends BaseWriter $css = []; // Create CSS - $css['vertical-align'] = $this->mapVAlign($alignment->getVertical()); - $textAlign = $this->mapHAlign($alignment->getHorizontal()); + $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? ''); + if ($verticalAlign) { + $css['vertical-align'] = $verticalAlign; + } + $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? ''); if ($textAlign) { $css['text-align'] = $textAlign; if (in_array($textAlign, ['left', 'right'])) { $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; } } + $rotation = $alignment->getTextRotation(); + if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { + if ($this->isMPdf) { + $css['text-rotate'] = "$rotation"; + } else { + $css['transform'] = "rotate({$rotation}deg)"; + } + } return $css; } @@ -1064,10 +1054,8 @@ class Html extends BaseWriter * Create CSS style. * * @param Border $border Border - * - * @return string */ - private function createCSSStyleBorder(Border $border) + private function createCSSStyleBorder(Border $border): string { // Create CSS - add !important to non-none border styles for merged cells $borderStyle = $this->mapBorderStyle($border->getBorderStyle()); @@ -1088,9 +1076,11 @@ class Html extends BaseWriter $css = []; // Create CSS - $value = $fill->getFillType() == Fill::FILL_NONE ? - 'white' : '#' . $fill->getStartColor()->getRGB(); - $css['background-color'] = $value; + if ($fill->getFillType() !== Fill::FILL_NONE) { + $value = $fill->getFillType() == Fill::FILL_NONE ? + 'white' : '#' . $fill->getStartColor()->getRGB(); + $css['background-color'] = $value; + } return $css; } @@ -1098,7 +1088,7 @@ class Html extends BaseWriter /** * Generate HTML footer. */ - public function generateHTMLFooter() + public function generateHTMLFooter(): string { // Construct HTML $html = ''; @@ -1108,7 +1098,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTagInline(Worksheet $worksheet, $id) + private function generateTableTagInline(Worksheet $worksheet, string $id): string { $style = isset($this->cssStyles['table']) ? $this->assembleCSS($this->cssStyles['table']) : ''; @@ -1128,7 +1118,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void + private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void { if (!$this->useInlineCss) { $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; @@ -1155,9 +1145,9 @@ class Html extends BaseWriter $html = ''; $id = $showid ? "id='sheet$sheetIndex'" : ''; if ($showid) { - $html .= "
\n"; + $html .= "
" . PHP_EOL; } else { - $html .= "
\n"; + $html .= "
" . PHP_EOL; } $this->generateTableTag($worksheet, $id, $html, $sheetIndex); @@ -1181,7 +1171,7 @@ class Html extends BaseWriter /** * Generate table footer. */ - private function generateTableFooter() + private function generateTableFooter(): string { return ' ' . PHP_EOL . '
' . PHP_EOL; } @@ -1227,7 +1217,7 @@ class Html extends BaseWriter return $html; } - private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $row, $columnNumber) + private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array { $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : ''; $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1); @@ -1253,22 +1243,24 @@ class Html extends BaseWriter return [$cell, $cssClass, $coordinate]; } - private function generateRowCellDataValueRich($cell, &$cellData): void + private function generateRowCellDataValueRich(Cell $cell, string &$cellData): void { // Loop through rich text elements $elements = $cell->getValue()->getRichTextElements(); foreach ($elements as $element) { // Rich text start? if ($element instanceof Run) { - $cellData .= ''; - $cellEnd = ''; - if ($element->getFont()->getSuperscript()) { - $cellData .= ''; - $cellEnd = ''; - } elseif ($element->getFont()->getSubscript()) { - $cellData .= ''; - $cellEnd = ''; + if ($element->getFont() !== null) { + $cellData .= ''; + + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + $cellEnd = ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + $cellEnd = ''; + } } // Convert UTF8 data to PCDATA @@ -1286,23 +1278,22 @@ class Html extends BaseWriter } } - private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void + private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void { if ($cell->getValue() instanceof RichText) { $this->generateRowCellDataValueRich($cell, $cellData); } else { $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); - if ($formatCode !== null) { - $cellData = NumberFormat::toFormattedString( - $origData, - $formatCode, - [$this, 'formatColor'] - ); - } + + $cellData = NumberFormat::toFormattedString( + $origData ?? '', + $formatCode ?? NumberFormat::FORMAT_GENERAL, + [$this, 'formatColor'] + ); if ($cellData === $origData) { - $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags()); + $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags()); } if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { $cellData = '' . $cellData . ''; @@ -1312,7 +1303,11 @@ class Html extends BaseWriter } } - private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType) + /** + * @param null|Cell|string $cell + * @param array|string $cssClass + */ + private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, string $cellType): string { $cellData = ' '; if ($cell instanceof Cell) { @@ -1332,10 +1327,10 @@ class Html extends BaseWriter $cellData = nl2br($cellData); // Extend CSS class? - if (!$this->useInlineCss) { + if (!$this->useInlineCss && is_string($cssClass)) { $cssClass .= ' style' . $cell->getXfIndex(); $cssClass .= ' ' . $cell->getDataType(); - } else { + } elseif (is_array($cssClass)) { if ($cellType == 'th') { if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); @@ -1365,12 +1360,12 @@ class Html extends BaseWriter return $cellData; } - private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate) + private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string { return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : ''; } - private function generateRowSpans($html, $rowSpan, $colSpan) + private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string { $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : ''; $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : ''; @@ -1378,7 +1373,10 @@ class Html extends BaseWriter return $html; } - private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row): void + /** + * @param array|string $cssClass + */ + private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void { // Image? $htmlx = $this->writeImageInCell($worksheet, $coordinate); @@ -1386,7 +1384,7 @@ class Html extends BaseWriter $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start $html .= ' <' . $cellType; - if (!$this->useInlineCss && !$this->isPdf) { + if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) { $html .= ' class="' . $cssClass . '"'; if ($htmlx) { $html .= " style='position: relative;'"; @@ -1396,9 +1394,11 @@ class Html extends BaseWriter // We must explicitly write the width of the element because TCPDF // does not recognize e.g. if ($this->useInlineCss) { - $xcssClass = $cssClass; + $xcssClass = is_array($cssClass) ? $cssClass : []; } else { - $html .= ' class="' . $cssClass . '"'; + if (is_string($cssClass)) { + $html .= ' class="' . $cssClass . '"'; + } $xcssClass = []; } $width = 0; @@ -1409,8 +1409,7 @@ class Html extends BaseWriter $width += $this->columnWidths[$sheetIndex][$i]; } } - $xcssClass['width'] = $width . 'pt'; - + $xcssClass['width'] = (string) $width . 'pt'; // We must also explicitly write the height of the element because TCPDF // does not recognize e.g. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) { @@ -1452,15 +1451,16 @@ class Html extends BaseWriter // Sheet index $sheetIndex = $worksheet->getParent()->getIndex($worksheet); $html = $this->generateRowStart($worksheet, $sheetIndex, $row); + $generateDiv = $this->isMPdf && $worksheet->getRowDimension($row + 1)->getVisible() === false; + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } // Write cells $colNum = 0; foreach ($values as $cellAddress) { [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum); - $colSpan = 1; - $rowSpan = 1; - // Cell Data $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType); @@ -1474,8 +1474,8 @@ class Html extends BaseWriter && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]); // Colspan and Rowspan - $colspan = 1; - $rowspan = 1; + $colSpan = 1; + $rowSpan = 1; if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum])) { $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]; $rowSpan = $spans['rowspan']; @@ -1499,6 +1499,9 @@ class Html extends BaseWriter } // Write row end + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } $html .= ' ' . PHP_EOL; // Return @@ -1722,7 +1725,7 @@ class Html extends BaseWriter $this->spansAreCalculated = true; } - private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void + private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void { // Identify which rows should be omitted in HTML. These are the rows where all the cells // participate in a merge and the where base cells are somewhere above. @@ -1744,7 +1747,7 @@ class Html extends BaseWriter while ($c++ < $e) { $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; - if (!in_array($baseCell, $adjustedBaseCells)) { + if (!in_array($baseCell, $adjustedBaseCells, true)) { // subtract rowspan by 1 --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; $adjustedBaseCells[] = $baseCell; @@ -1785,7 +1788,8 @@ class Html extends BaseWriter public function getOrientation(): ?string { - return null; + // Expect Pdf classes to override this method. + return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null; } /** @@ -1824,31 +1828,31 @@ class Html extends BaseWriter $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; '; $htmlPage .= 'margin-bottom: ' . $bottom; $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation(); - if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) { + if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) { $htmlPage .= 'size: landscape; '; - } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { + } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) { $htmlPage .= 'size: portrait; '; } - $htmlPage .= "}\n"; + $htmlPage .= '}' . PHP_EOL; ++$sheetId; } - $htmlPage .= <<div {margin-top: 5px;} - body>div:first-child {margin-top: 0;} - .scrpgbrk {margin-top: 1px;} -} -@media print { - .gridlinesp td {border: 1px solid black;} - .gridlinesp th {border: 1px solid black;} - .navigation {display: none;} -} - -EOF; + $htmlPage .= implode(PHP_EOL, [ + '.navigation {page-break-after: always;}', + '.scrpgbrk, div + div {page-break-before: always;}', + '@media screen {', + ' .gridlines td {border: 1px solid black;}', + ' .gridlines th {border: 1px solid black;}', + ' body>div {margin-top: 5px;}', + ' body>div:first-child {margin-top: 0;}', + ' .scrpgbrk {margin-top: 1px;}', + '}', + '@media print {', + ' .gridlinesp td {border: 1px solid black;}', + ' .gridlinesp th {border: 1px solid black;}', + ' .navigation {display: none;}', + '}', + '', + ]); $htmlPage .= $generateSurroundingHTML ? ('' . PHP_EOL) : ''; return $htmlPage; diff --git a/src/PhpSpreadsheet/Writer/IWriter.php b/src/PhpSpreadsheet/Writer/IWriter.php index b0a62726..d8bf1f0c 100644 --- a/src/PhpSpreadsheet/Writer/IWriter.php +++ b/src/PhpSpreadsheet/Writer/IWriter.php @@ -62,6 +62,8 @@ interface IWriter * Save PhpSpreadsheet to file. * * @param resource|string $filename Name of the file to save + * + * @throws Exception */ public function save($filename, int $flags = 0): void; diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index fc96f904..690b0c54 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -7,6 +7,13 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Dompdf extends Pdf { + /** + * embed images, or link to images. + * + * @var bool + */ + protected $embedImages = true; + /** * Gets the implementation of external PDF library that should be used. * @@ -26,15 +33,15 @@ class Dompdf extends Pdf { $fileHandle = parent::prepareForSave($filename); - // Default PDF paper size - $paperSize = 'LETTER'; // Letter (8.5 in. by 11 in.) - // Check for paper size and page orientation $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup(); $orientation = $this->getOrientation() ?? $setup->getOrientation(); $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize(); $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault(); + if (is_array($paperSize) && count($paperSize) === 2) { + $paperSize = [0.0, 0.0, $paperSize[0], $paperSize[1]]; + } $orientation = ($orientation == 'L') ? 'landscape' : 'portrait'; diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 281e1a4f..d0ce9ed4 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -8,6 +8,9 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Mpdf extends Pdf { + /** @var bool */ + protected $isMPdf = true; + /** * Gets the implementation of external PDF library that should be used. * diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index d29d4764..aefc6b56 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -77,7 +77,7 @@ class Tcpdf extends Pdf $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); // Write to file - fwrite($fileHandle, $pdf->output($filename, 'S')); + fwrite($fileHandle, $pdf->output('', 'S')); parent::restoreStateAfterSave(); } diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index eadf0083..69457357 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -732,7 +732,6 @@ class Xls extends BaseWriter } elseif ($dataProp['type']['data'] == 0x1E) { // null-terminated string prepended by dword string length // Null-terminated string $dataProp['data']['data'] .= chr(0); - // @phpstan-ignore-next-line ++$dataProp['data']['length']; // Complete the string with null string for being a %4 $dataProp['data']['length'] = $dataProp['data']['length'] + ((4 - $dataProp['data']['length'] % 4) == 4 ? 0 : (4 - $dataProp['data']['length'] % 4)); @@ -750,7 +749,6 @@ class Xls extends BaseWriter } else { $dataSection_Content .= $dataProp['data']['data']; - // @phpstan-ignore-next-line $dataSection_Content_Offset += 4 + $dataProp['data']['length']; } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 2f75f908..ca407d2a 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -78,7 +78,7 @@ class Parser /** * The parse tree to be generated. * - * @var string + * @var array|string */ public $parseTree; @@ -531,7 +531,7 @@ class Parser { return($this->convertFunction($token, $this->_func_args)); }*/ - // if it's an argument, ignore the token (the argument remains) + // if it's an argument, ignore the token (the argument remains) } elseif ($token == 'arg') { return ''; } @@ -1445,6 +1445,9 @@ class Parser if (empty($tree)) { // If it's the first call use parseTree $tree = $this->parseTree; } + if (!is_array($tree) || !isset($tree['left'], $tree['right'], $tree['value'])) { + throw new WriterException('Unexpected non-array'); + } if (is_array($tree['left'])) { $converted_tree = $this->toReversePolish($tree['left']); @@ -1475,7 +1478,7 @@ class Parser $left_tree = ''; } - // add it's left subtree and return. + // add its left subtree and return. return $left_tree . $this->convertFunction($tree['value'], $tree['right']); } $converted_tree = $this->convert($tree['value']); diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 37865518..78fda517 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -465,7 +465,7 @@ class Worksheet extends BIFFwriter switch ($calctype) { case 'integer': case 'double': - $this->writeNumber($row, $column, $calculatedValue, $xfIndex); + $this->writeNumber($row, $column, (float) $calculatedValue, $xfIndex); break; case 'string': @@ -473,7 +473,7 @@ class Worksheet extends BIFFwriter break; case 'boolean': - $this->writeBoolErr($row, $column, $calculatedValue, 0, $xfIndex); + $this->writeBoolErr($row, $column, (int) $calculatedValue, 0, $xfIndex); break; default: @@ -2405,10 +2405,12 @@ class Worksheet extends BIFFwriter for ($i = 0; $i < $width; ++$i) { /** @phpstan-ignore-next-line */ $color = imagecolorsforindex($image, imagecolorat($image, $i, $j)); - foreach (['red', 'green', 'blue'] as $key) { - $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); + if ($color !== false) { + foreach (['red', 'green', 'blue'] as $key) { + $color[$key] = $color[$key] + (int) round((255 - $color[$key]) * $color['alpha'] / 127); + } + $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } - $data .= chr($color['blue']) . chr($color['green']) . chr($color['red']); } if (3 * $width % 4) { $data .= str_repeat("\x00", 4 - 3 * $width % 4); @@ -2836,7 +2838,7 @@ class Worksheet extends BIFFwriter $operatorType = 0x01; break; - // not OPERATOR_NOTBETWEEN 0x02 + // not OPERATOR_NOTBETWEEN 0x02 } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 5aca5117..c9446e70 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -349,12 +349,15 @@ class Xlsx extends BaseWriter //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) if ($this->spreadSheet->hasRibbon()) { $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); + $tmpRibbonTarget = is_string($tmpRibbonTarget) ? $tmpRibbonTarget : ''; $zipContent[$tmpRibbonTarget] = $this->spreadSheet->getRibbonXMLData('data'); if ($this->spreadSheet->hasRibbonBinObjects()) { $tmpRootPath = dirname($tmpRibbonTarget) . '/'; $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write - foreach ($ribbonBinObjects as $aPath => $aContent) { - $zipContent[$tmpRootPath . $aPath] = $aContent; + if (is_array($ribbonBinObjects)) { + foreach ($ribbonBinObjects as $aPath => $aContent) { + $zipContent[$tmpRootPath . $aPath] = $aContent; + } } //the rels for files $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet); @@ -684,6 +687,7 @@ class Xlsx extends BaseWriter return $this; } + /** @var array */ private $pathNames = []; private function addZipFile(string $path, string $content): void diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index e24afbac..3b64bd8e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -3,21 +3,20 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; class Chart extends WriterPart { - protected $calculateCellValues; - /** * @var int */ @@ -32,8 +31,6 @@ class Chart extends WriterPart */ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $calculateCellValues = true) { - $this->calculateCellValues = $calculateCellValues; - // Create XML writer $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { @@ -42,7 +39,7 @@ class Chart extends WriterPart $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); } // Ensure that data series values are up-to-date before we save - if ($this->calculateCellValues) { + if ($calculateCellValues) { $chart->refresh(); } @@ -56,13 +53,13 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); $objWriter->startElement('c:date1904'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->startElement('c:lang'); $objWriter->writeAttribute('val', 'en-GB'); $objWriter->endElement(); $objWriter->startElement('c:roundedCorners'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0'); $objWriter->endElement(); $this->writeAlternateContent($objWriter); @@ -72,42 +69,34 @@ class Chart extends WriterPart $this->writeTitle($objWriter, $chart->getTitle()); $objWriter->startElement('c:autoTitleDeleted'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted()); $objWriter->endElement(); $objWriter->startElement('c:view3D'); - $rotX = $chart->getRotX(); - if (is_int($rotX)) { - $objWriter->startElement('c:rotX'); - $objWriter->writeAttribute('val', "$rotX"); - $objWriter->endElement(); - } - $rotY = $chart->getRotY(); - if (is_int($rotY)) { - $objWriter->startElement('c:rotY'); - $objWriter->writeAttribute('val', "$rotY"); - $objWriter->endElement(); - } - $rAngAx = $chart->getRAngAx(); - if (is_int($rAngAx)) { - $objWriter->startElement('c:rAngAx'); - $objWriter->writeAttribute('val', "$rAngAx"); - $objWriter->endElement(); - } - $perspective = $chart->getPerspective(); - if (is_int($perspective)) { - $objWriter->startElement('c:perspective'); - $objWriter->writeAttribute('val', "$perspective"); - $objWriter->endElement(); + $surface2D = false; + $plotArea = $chart->getPlotArea(); + if ($plotArea !== null) { + $seriesArray = $plotArea->getPlotGroup(); + foreach ($seriesArray as $series) { + if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) { + $surface2D = true; + + break; + } + } } + $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 - $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY(), $chart->getMajorGridlines(), $chart->getMinorGridlines()); + $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); $this->writeLegend($objWriter, $chart->getLegend()); $objWriter->startElement('c:plotVisOnly'); - $objWriter->writeAttribute('val', (int) $chart->getPlotVisibleOnly()); + $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly()); $objWriter->endElement(); $objWriter->startElement('c:dispBlanksAs'); @@ -115,19 +104,37 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:showDLblsOverMax'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:chart + if ($chart->getNoFill()) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + $objWriter->endElement(); // c:spPr + } $this->writePrintSettings($objWriter); - $objWriter->endElement(); + $objWriter->endElement(); // c:chartSpace // Return return $objWriter->getData(); } + private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void + { + if ($value === null && $surface2D) { + $value = $default; + } + if ($value !== null) { + $objWriter->startElement($tag); + $objWriter->writeAttribute('val', "$value"); + $objWriter->endElement(); + } + } + /** * Write Chart Title. */ @@ -166,7 +173,7 @@ class Chart extends WriterPart $this->writeLayout($objWriter, $title->getLayout()); $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -202,7 +209,7 @@ class Chart extends WriterPart $objWriter->startElement('a:p'); $objWriter->startElement('a:pPr'); - $objWriter->writeAttribute('rtl', 0); + $objWriter->writeAttribute('rtl', '0'); $objWriter->startElement('a:defRPr'); $objWriter->endElement(); @@ -221,7 +228,7 @@ class Chart extends WriterPart /** * Write Chart Plot Area. */ - private function writePlotArea(XMLWriter $objWriter, PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null): void + private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null): void { if ($plotArea === null) { return; @@ -249,11 +256,11 @@ class Chart extends WriterPart $groupType = $plotGroup->getPlotType(); if ($groupType == $chartType) { $plotStyle = $plotGroup->getPlotStyle(); - if ($groupType === DataSeries::TYPE_RADARCHART) { + if (!empty($plotStyle) && $groupType === DataSeries::TYPE_RADARCHART) { $objWriter->startElement('c:radarStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); - } elseif ($groupType === DataSeries::TYPE_SCATTERCHART) { + } elseif (!empty($plotStyle) && $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); @@ -272,16 +279,16 @@ class Chart extends WriterPart if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) { // Line only, Line3D can't be smoothed $objWriter->startElement('c:smooth'); - $objWriter->writeAttribute('val', (int) $plotGroup->getSmoothLine()); + $objWriter->writeAttribute('val', (string) (int) $plotGroup->getSmoothLine()); $objWriter->endElement(); } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) { $objWriter->startElement('c:gapWidth'); - $objWriter->writeAttribute('val', 150); + $objWriter->writeAttribute('val', '150'); $objWriter->endElement(); if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') { $objWriter->startElement('c:overlap'); - $objWriter->writeAttribute('val', 100); + $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); } } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) { @@ -293,7 +300,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:showNegBubbles'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } elseif ($chartType === DataSeries::TYPE_STOCKCHART) { $objWriter->startElement('c:hiLowLines'); @@ -302,7 +309,7 @@ class Chart extends WriterPart $objWriter->startElement('c:upDownBars'); $objWriter->startElement('c:gapWidth'); - $objWriter->writeAttribute('val', 300); + $objWriter->writeAttribute('val', '300'); $objWriter->endElement(); $objWriter->startElement('c:upBars'); @@ -333,12 +340,12 @@ class Chart extends WriterPart } } else { $objWriter->startElement('c:firstSliceAng'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); if ($chartType === DataSeries::TYPE_DONUTCHART) { $objWriter->startElement('c:holeSize'); - $objWriter->writeAttribute('val', 50); + $objWriter->writeAttribute('val', '50'); $objWriter->endElement(); } } @@ -348,18 +355,54 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } - $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis()); if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { $this->writeSerAxis($objWriter, $id2, $id3); } } + $stops = $plotArea->getGradientFillStops(); + if ($plotArea->getNoFill() || !empty($stops)) { + $objWriter->startElement('c:spPr'); + if ($plotArea->getNoFill()) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + } + if (!empty($stops)) { + $objWriter->startElement('a:gradFill'); + $objWriter->startElement('a:gsLst'); + foreach ($stops as $stop) { + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0])); + $this->writeColor($objWriter, $stop[1], false); + $objWriter->endElement(); // a:gs + } + $objWriter->endElement(); // a:gsLst + $angle = $plotArea->getGradientFillAngle(); + if ($angle !== null) { + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', Properties::angleToXml($angle)); + $objWriter->endElement(); // a:lin + } + $objWriter->endElement(); // a:gradFill + } + $objWriter->endElement(); // c:spPr + } - $objWriter->endElement(); + $objWriter->endElement(); // c:plotArea + } + + private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void + { + if ($value !== null) { + $objWriter->startElement("c:$name"); + $objWriter->writeAttribute('val', $value ? '1' : '0'); + $objWriter->endElement(); + } } /** @@ -367,44 +410,72 @@ class Chart extends WriterPart */ private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = null): void { + if (!isset($chartLayout)) { + return; + } $objWriter->startElement('c:dLbls'); - $objWriter->startElement('c:showLegendKey'); - $showLegendKey = (empty($chartLayout)) ? 0 : $chartLayout->getShowLegendKey(); - $objWriter->writeAttribute('val', ((empty($showLegendKey)) ? 0 : 1)); - $objWriter->endElement(); + $fillColor = $chartLayout->getLabelFillColor(); + $borderColor = $chartLayout->getLabelBorderColor(); + if ($fillColor && $fillColor->isUsable()) { + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $fillColor); + if ($borderColor && $borderColor->isUsable()) { + $objWriter->startElement('a:ln'); + $this->writeColor($objWriter, $borderColor); + $objWriter->endElement(); // a:ln + } + $objWriter->endElement(); // c:spPr + } + $fontColor = $chartLayout->getLabelFontColor(); + if ($fontColor && $fontColor->isUsable()) { + $objWriter->startElement('c:txPr'); - $objWriter->startElement('c:showVal'); - $showVal = (empty($chartLayout)) ? 0 : $chartLayout->getShowVal(); - $objWriter->writeAttribute('val', ((empty($showVal)) ? 0 : 1)); - $objWriter->endElement(); + $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'); + $objWriter->startElement('a:spAutoFit'); + $objWriter->endElement(); // a:spAutoFit + $objWriter->endElement(); // a:bodyPr - $objWriter->startElement('c:showCatName'); - $showCatName = (empty($chartLayout)) ? 0 : $chartLayout->getShowCatName(); - $objWriter->writeAttribute('val', ((empty($showCatName)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle - $objWriter->startElement('c:showSerName'); - $showSerName = (empty($chartLayout)) ? 0 : $chartLayout->getShowSerName(); - $objWriter->writeAttribute('val', ((empty($showSerName)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $this->writeColor($objWriter, $fontColor); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p - $objWriter->startElement('c:showPercent'); - $showPercent = (empty($chartLayout)) ? 0 : $chartLayout->getShowPercent(); - $objWriter->writeAttribute('val', ((empty($showPercent)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->endElement(); // c:txPr + } - $objWriter->startElement('c:showBubbleSize'); - $showBubbleSize = (empty($chartLayout)) ? 0 : $chartLayout->getShowBubbleSize(); - $objWriter->writeAttribute('val', ((empty($showBubbleSize)) ? 0 : 1)); - $objWriter->endElement(); + if ($chartLayout->getNumFmtCode() !== '') { + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', $chartLayout->getnumFmtCode()); + $objWriter->writeAttribute('sourceLinked', (string) (int) $chartLayout->getnumFmtLinked()); + $objWriter->endElement(); // c:numFmt + } + if ($chartLayout->getDLblPos() !== '') { + $objWriter->startElement('c:dLblPos'); + $objWriter->writeAttribute('val', $chartLayout->getDLblPos()); + $objWriter->endElement(); // c:dLblPos + } + $this->writeDataLabelsBool($objWriter, 'showLegendKey', $chartLayout->getShowLegendKey()); + $this->writeDataLabelsBool($objWriter, 'showVal', $chartLayout->getShowVal()); + $this->writeDataLabelsBool($objWriter, 'showCatName', $chartLayout->getShowCatName()); + $this->writeDataLabelsBool($objWriter, 'showSerName', $chartLayout->getShowSerName()); + $this->writeDataLabelsBool($objWriter, 'showPercent', $chartLayout->getShowPercent()); + $this->writeDataLabelsBool($objWriter, 'showBubbleSize', $chartLayout->getShowBubbleSize()); + $this->writeDataLabelsBool($objWriter, 'showLeaderLines', $chartLayout->getShowLeaderLines()); - $objWriter->startElement('c:showLeaderLines'); - $showLeaderLines = (empty($chartLayout)) ? 1 : $chartLayout->getShowLeaderLines(); - $objWriter->writeAttribute('val', ((empty($showLeaderLines)) ? 0 : 1)); - $objWriter->endElement(); - - $objWriter->endElement(); + $objWriter->endElement(); // c:dLbls } /** @@ -417,12 +488,17 @@ class Chart extends WriterPart private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc - // In that case, xAxis is NOT a category. - if ($yAxis->getAxisIsNumericFormat()) { - $objWriter->startElement('c:valAx'); + // In that case, xAxis may contain values like the yAxis, or it may be a date axis (LINECHART). + $axisType = $yAxis->getAxisType(); + if ($axisType !== '') { + $objWriter->startElement("c:$axisType"); + } elseif ($yAxis->getAxisIsNumericFormat()) { + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); } else { - $objWriter->startElement('c:catAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_CATEGORY); } + $majorGridlines = $yAxis->getMajorGridlines(); + $minorGridlines = $yAxis->getMinorGridlines(); if ($id1 !== '0') { $objWriter->startElement('c:axId'); @@ -431,19 +507,49 @@ class Chart extends WriterPart } $objWriter->startElement('c:scaling'); - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); - $objWriter->endElement(); - $objWriter->endElement(); + if ($yAxis->getAxisOptionsProperty('maximum') !== null) { + $objWriter->startElement('c:max'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('maximum')); + $objWriter->endElement(); + } + if ($yAxis->getAxisOptionsProperty('minimum') !== null) { + $objWriter->startElement('c:min'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minimum')); + $objWriter->endElement(); + } + if (!empty($yAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); $objWriter->writeAttribute('val', 'b'); $objWriter->endElement(); + if ($majorGridlines !== null) { + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $majorGridlines); + $this->writeEffects($objWriter, $majorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines + } + + if ($minorGridlines !== null && $minorGridlines->getObjectState()) { + $objWriter->startElement('c:minorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $minorGridlines); + $this->writeEffects($objWriter, $minorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end minorGridLines + } + if ($xAxisLabel !== null) { $objWriter->startElement('c:title'); $objWriter->startElement('c:tx'); @@ -456,10 +562,6 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:pPr'); - $objWriter->startElement('a:defRPr'); - $objWriter->endElement(); - $objWriter->endElement(); $caption = $xAxisLabel->getCaption(); if (is_array($caption)) { @@ -475,7 +577,7 @@ class Chart extends WriterPart $this->writeLayout($objWriter, $layout); $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -486,38 +588,73 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } + + $textRotation = $yAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $yAxis); - $this->writeShadow($objWriter, $yAxis); - $this->writeSoftEdge($objWriter, $yAxis); - $objWriter->endElement(); // effectLst + $this->writeColor($objWriter, $yAxis->getFillColorObject()); + $this->writeEffects($objWriter, $yAxis); $objWriter->endElement(); // spPr + if ($yAxis->getAxisOptionsProperty('major_unit') !== null) { + $objWriter->startElement('c:majorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_unit')); + $objWriter->endElement(); + } + + if ($yAxis->getAxisOptionsProperty('minor_unit') !== null) { + $objWriter->startElement('c:minorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_unit')); + $objWriter->endElement(); + } + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('horizontal_crosses'))) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); + $objWriter->endElement(); + } } $objWriter->startElement('c:auto'); - $objWriter->writeAttribute('val', 1); + // LineChart with dateAx wants '0' + $objWriter->writeAttribute('val', ($axisType === Axis::AXIS_TYPE_DATE) ? '0' : '1'); $objWriter->endElement(); $objWriter->startElement('c:lblAlgn'); @@ -525,12 +662,36 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:lblOffset'); - $objWriter->writeAttribute('val', 100); + $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); + if ($axisType === Axis::AXIS_TYPE_DATE) { + $property = 'baseTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'majorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'minorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + } + if ($isMultiLevelSeries) { $objWriter->startElement('c:noMultiLvlLbl'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } $objWriter->endElement(); @@ -544,9 +705,11 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, GridLines $majorGridlines, GridLines $minorGridlines): void + private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis): void { - $objWriter->startElement('c:valAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); + $majorGridlines = $xAxis->getMajorGridlines(); + $minorGridlines = $xAxis->getMinorGridlines(); if ($id2 !== '0') { $objWriter->startElement('c:axId'); @@ -568,127 +731,36 @@ class Chart extends WriterPart $objWriter->endElement(); } - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + if (!empty($xAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); $objWriter->writeAttribute('val', 'l'); $objWriter->endElement(); - $objWriter->startElement('c:majorGridlines'); - $objWriter->startElement('c:spPr'); - - if ($majorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$majorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $majorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($majorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln + if ($majorGridlines !== null) { + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $majorGridlines); + $this->writeEffects($objWriter, $majorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines } - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $majorGridlines); - $this->writeShadow($objWriter, $majorGridlines); - $this->writeSoftEdge($objWriter, $majorGridlines); - $objWriter->endElement(); //end effectLst - $objWriter->endElement(); //end spPr - $objWriter->endElement(); //end majorGridLines - - if ($minorGridlines->getObjectState()) { + if ($minorGridlines !== null && $minorGridlines->getObjectState()) { $objWriter->startElement('c:minorGridlines'); $objWriter->startElement('c:spPr'); - - if ($minorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$minorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $minorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($minorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } - - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $minorGridlines); - $this->writeShadow($objWriter, $minorGridlines); - $this->writeSoftEdge($objWriter, $minorGridlines); - $objWriter->endElement(); //end effectLst - + $this->writeLineStyles($objWriter, $minorGridlines); + $this->writeEffects($objWriter, $minorGridlines); $objWriter->endElement(); //end spPr $objWriter->endElement(); //end minorGridLines } @@ -705,10 +777,6 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:pPr'); - $objWriter->startElement('a:defRPr'); - $objWriter->endElement(); - $objWriter->endElement(); $caption = $yAxisLabel->getCaption(); if (is_array($caption)) { @@ -726,7 +794,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -737,85 +805,45 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } + + $textRotation = $xAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } $objWriter->startElement('c:spPr'); - - if ($xAxis->getFillProperty('value') !== null) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getFillProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('alpha')); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - $objWriter->startElement('a:ln'); - - $objWriter->writeAttribute('w', $xAxis->getLineStyleProperty('width')); - $objWriter->writeAttribute('cap', $xAxis->getLineStyleProperty('cap')); - $objWriter->writeAttribute('cmpd', $xAxis->getLineStyleProperty('compound')); - - if ($xAxis->getLineProperty('value') !== null) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getLineProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('alpha')); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $xAxis->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($xAxis->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('head')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('head')); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('end')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('end')); - $objWriter->endElement(); - } - - $objWriter->endElement(); - - $objWriter->startElement('a:effectLst'); - $this->writeGlow($objWriter, $xAxis); - $this->writeShadow($objWriter, $xAxis); - $this->writeSoftEdge($objWriter, $xAxis); - $objWriter->endElement(); //effectList - + $this->writeColor($objWriter, $xAxis->getFillColorObject()); + $this->writeLineStyles($objWriter, $xAxis); + $this->writeEffects($objWriter, $xAxis); $objWriter->endElement(); //end spPr if ($id1 !== '0') { @@ -828,14 +856,20 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value')); $objWriter->endElement(); } else { - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + $crosses = $xAxis->getAxisOptionsProperty('horizontal_crosses'); + if ($crosses) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $crosses); + $objWriter->endElement(); + } } - $objWriter->startElement('c:crossBetween'); - $objWriter->writeAttribute('val', 'midCat'); - $objWriter->endElement(); + $crossBetween = $xAxis->getCrossBetween(); + if ($crossBetween !== '') { + $objWriter->startElement('c:crossBetween'); + $objWriter->writeAttribute('val', $crossBetween); + $objWriter->endElement(); + } if ($xAxis->getAxisOptionsProperty('major_unit') !== null) { $objWriter->startElement('c:majorUnit'); @@ -853,7 +887,7 @@ class Chart extends WriterPart if ($isMultiLevelSeries) { if ($groupType !== DataSeries::TYPE_BUBBLECHART) { $objWriter->startElement('c:noMultiLvlLbl'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } } @@ -936,29 +970,23 @@ class Chart extends WriterPart /** * Method writing plot series values. - * - * @param int $val value for idx (default: 3) - * @param string $fillColor hex color (default: FF9900) */ - private function writePlotSeriesValuesElement(XMLWriter $objWriter, $val = 3, $fillColor = 'FF9900'): void + private function writePlotSeriesValuesElement(XMLWriter $objWriter, int $val, ?ChartColor $fillColor): void { + if ($fillColor === null || !$fillColor->isUsable()) { + return; + } $objWriter->startElement('c:dPt'); - $objWriter->startElement('c:idx'); - $objWriter->writeAttribute('val', $val); - $objWriter->endElement(); - $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); - $objWriter->endElement(); + $objWriter->startElement('c:idx'); + $objWriter->writeAttribute('val', "$val"); + $objWriter->endElement(); // c:idx $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); + $this->writeColor($objWriter, $fillColor); + $objWriter->endElement(); // c:spPr + + $objWriter->endElement(); // c:dPt } /** @@ -981,8 +1009,8 @@ class Chart extends WriterPart $objWriter->endElement(); } - if ($plotGroup->getPlotGrouping() !== null) { - $plotGroupingType = $plotGroup->getPlotGrouping(); + $plotGroupingType = $plotGroup->getPlotGrouping(); + if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) { $objWriter->startElement('c:grouping'); $objWriter->writeAttribute('val', $plotGroupingType); $objWriter->endElement(); @@ -996,11 +1024,11 @@ class Chart extends WriterPart if ($groupType !== DataSeries::TYPE_LINECHART) { if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) { $objWriter->startElement('c:varyColors'); - $objWriter->writeAttribute('val', 1); + $objWriter->writeAttribute('val', '1'); $objWriter->endElement(); } else { $objWriter->startElement('c:varyColors'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } } @@ -1010,44 +1038,46 @@ class Chart extends WriterPart foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) { $objWriter->startElement('c:ser'); - $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); - if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) { - $fillColor = $plotLabel->getFillColor(); - if ($fillColor !== null && !is_array($fillColor)) { - $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - } - $objWriter->startElement('c:idx'); - $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesIdx); + $objWriter->writeAttribute('val', (string) ($this->seriesIndex + $plotSeriesIdx)); $objWriter->endElement(); $objWriter->startElement('c:order'); - $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesRef); + $objWriter->writeAttribute('val', (string) ($this->seriesIndex + $plotSeriesRef)); $objWriter->endElement(); - // Values - $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesRef); - - if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { - $fillColorValues = $plotSeriesValues->getFillColor(); - if ($fillColorValues !== null && is_array($fillColorValues)) { - foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { - $this->writePlotSeriesValuesElement($objWriter, $dataKey, ($fillColorValues[$dataKey] ?? 'FF9900')); - } - } else { - $this->writePlotSeriesValuesElement($objWriter); + $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); + $labelFill = null; + if ($plotLabel && $groupType === DataSeries::TYPE_LINECHART) { + $labelFill = $plotLabel->getFillColorObject(); + $labelFill = ($labelFill instanceof ChartColor) ? $labelFill : null; + } + if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) { + $fillColor = $plotLabel->getFillColorObject(); + if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) { + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $fillColor); + $objWriter->endElement(); // c:spPr } } + // Values + $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesIdx); + + if ($plotSeriesValues !== false && in_array($groupType, self::CUSTOM_COLOR_TYPES, true)) { + $fillColorValues = $plotSeriesValues->getFillColorObject(); + if ($fillColorValues !== null && is_array($fillColorValues)) { + foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { + $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? null); + } + } + } + if ($plotSeriesValues !== false && $plotSeriesValues->getLabelLayout()) { + $this->writeDataLabels($objWriter, $plotSeriesValues->getLabelLayout()); + } + // Labels - $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesRef); + $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) { $objWriter->startElement('c:tx'); $objWriter->startElement('c:strRef'); @@ -1058,66 +1088,55 @@ class Chart extends WriterPart // Formatting for the points if ( - $groupType == DataSeries::TYPE_LINECHART - || $groupType == DataSeries::TYPE_STOCKCHART - || ($groupType === DataSeries::TYPE_SCATTERCHART && $plotSeriesValues !== false && !$plotSeriesValues->getScatterLines()) - || ($plotSeriesValues !== false && $plotSeriesValues->getSchemeClr()) + $plotSeriesValues !== false ) { - $plotLineWidth = 12700; - if ($plotSeriesValues) { - $plotLineWidth = $plotSeriesValues->getLineWidth(); - } - $objWriter->startElement('c:spPr'); - $schemeClr = $plotLabel ? $plotLabel->getSchemeClr() : null; - if ($schemeClr) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:schemeClr'); - $objWriter->writeAttribute('val', $schemeClr); - $objWriter->endElement(); - $objWriter->endElement(); - } - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $plotLineWidth); - if ($groupType == DataSeries::TYPE_STOCKCHART || $groupType === DataSeries::TYPE_SCATTERCHART) { - $objWriter->startElement('a:noFill'); - $objWriter->endElement(); - } elseif ($plotLabel) { - $fillColor = $plotLabel->getFillColor(); - if (is_string($fillColor)) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); + $fillObject = $labelFill ?? $plotSeriesValues->getFillColorObject(); + $callLineStyles = true; + if ($fillObject instanceof ChartColor && $fillObject->isUsable()) { + if ($groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('a:ln'); + $callLineStyles = false; + } + $this->writeColor($objWriter, $fillObject); + if (!$callLineStyles) { + $objWriter->endElement(); // a:ln } } - $objWriter->endElement(); - $objWriter->endElement(); + $nofill = $groupType === DataSeries::TYPE_STOCKCHART || (($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) && !$plotSeriesValues->getScatterLines()); + if ($callLineStyles) { + $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill); + $this->writeEffects($objWriter, $plotSeriesValues); + } + $objWriter->endElement(); // c:spPr } if ($plotSeriesValues) { $plotSeriesMarker = $plotSeriesValues->getPointMarker(); - if ($plotSeriesMarker) { + $markerFillColor = $plotSeriesValues->getMarkerFillColor(); + $fillUsed = $markerFillColor->IsUsable(); + $markerBorderColor = $plotSeriesValues->getMarkerBorderColor(); + $borderUsed = $markerBorderColor->isUsable(); + if ($plotSeriesMarker || $fillUsed || $borderUsed) { $objWriter->startElement('c:marker'); $objWriter->startElement('c:symbol'); - $objWriter->writeAttribute('val', $plotSeriesMarker); + if ($plotSeriesMarker) { + $objWriter->writeAttribute('val', $plotSeriesMarker); + } $objWriter->endElement(); if ($plotSeriesMarker !== 'none') { $objWriter->startElement('c:size'); $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize()); - $objWriter->endElement(); - $fillColor = $plotSeriesValues->getFillColor(); - if (is_string($fillColor) && $fillColor !== '') { - $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); // srgbClr - $objWriter->endElement(); // solidFill - $objWriter->endElement(); // spPr + $objWriter->endElement(); // c:size + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $markerFillColor); + if ($borderUsed) { + $objWriter->startElement('a:ln'); + $this->writeColor($objWriter, $markerBorderColor); + $objWriter->endElement(); // a:ln } + $objWriter->endElement(); // spPr } $objWriter->endElement(); @@ -1126,23 +1145,103 @@ class Chart extends WriterPart if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) { $objWriter->startElement('c:invertIfNegative'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } + // Trendlines + if ($plotSeriesValues !== false) { + foreach ($plotSeriesValues->getTrendLines() as $trendLine) { + $trendLineType = $trendLine->getTrendLineType(); + $order = $trendLine->getOrder(); + $period = $trendLine->getPeriod(); + $dispRSqr = $trendLine->getDispRSqr(); + $dispEq = $trendLine->getDispEq(); + $forward = $trendLine->getForward(); + $backward = $trendLine->getBackward(); + $intercept = $trendLine->getIntercept(); + $name = $trendLine->getName(); + $trendLineColor = $trendLine->getLineColor(); // ChartColor + + $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' + if ($name !== '') { + $objWriter->startElement('c:name'); + $objWriter->writeRawData($name); + $objWriter->endElement(); // c:name + } + $objWriter->startElement('c:spPr'); + + if (!$trendLineColor->isUsable()) { + // use dataSeriesValues line color as a backup if $trendLineColor is null + $dsvLineColor = $plotSeriesValues->getLineColor(); + if ($dsvLineColor->isUsable()) { + $trendLine + ->getLineColor() + ->setColorProperties($dsvLineColor->getValue(), $dsvLineColor->getAlpha(), $dsvLineColor->getType()); + } + } // otherwise, hope Excel does the right thing + + $this->writeLineStyles($objWriter, $trendLine, false); // suppress noFill + + $objWriter->endElement(); // spPr + + $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell' + $objWriter->writeAttribute('val', $trendLineType); + $objWriter->endElement(); // trendlineType + if ($backward !== 0.0) { + $objWriter->startElement('c:backward'); + $objWriter->writeAttribute('val', "$backward"); + $objWriter->endElement(); // c:backward + } + if ($forward !== 0.0) { + $objWriter->startElement('c:forward'); + $objWriter->writeAttribute('val', "$forward"); + $objWriter->endElement(); // c:forward + } + if ($intercept !== 0.0) { + $objWriter->startElement('c:intercept'); + $objWriter->writeAttribute('val', "$intercept"); + $objWriter->endElement(); // c:intercept + } + if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) { + $objWriter->startElement('c:order'); + $objWriter->writeAttribute('val', $order); + $objWriter->endElement(); // order + } + if ($trendLineType == TrendLine::TRENDLINE_MOVING_AVG) { + $objWriter->startElement('c:period'); + $objWriter->writeAttribute('val', $period); + $objWriter->endElement(); // period + } + $objWriter->startElement('c:dispRSqr'); + $objWriter->writeAttribute('val', $dispRSqr ? '1' : '0'); + $objWriter->endElement(); + $objWriter->startElement('c:dispEq'); + $objWriter->writeAttribute('val', $dispEq ? '1' : '0'); + $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('c:trendlineLbl'); + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', 'General'); + $objWriter->writeAttribute('sourceLinked', '0'); + $objWriter->endElement(); // numFmt + $objWriter->endElement(); // trendlineLbl + } + + $objWriter->endElement(); // trendline + } + } // Category Labels - $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesRef); + $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx); if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) { $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries(); if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { - if ($plotGroup->getPlotStyle() !== null) { - $plotStyle = $plotGroup->getPlotStyle(); - if ($plotStyle) { - $objWriter->startElement('c:explosion'); - $objWriter->writeAttribute('val', 25); - $objWriter->endElement(); - } + $plotStyle = $plotGroup->getPlotStyle(); + if (is_numeric($plotStyle)) { + $objWriter->startElement('c:explosion'); + $objWriter->writeAttribute('val', $plotStyle); + $objWriter->endElement(); } } @@ -1177,7 +1276,7 @@ class Chart extends WriterPart $objWriter->endElement(); if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') { $objWriter->startElement('c:smooth'); - $objWriter->writeAttribute('val', '1'); + $objWriter->writeAttribute('val', $plotSeriesValues->getSmoothLine() ? '1' : '0'); $objWriter->endElement(); } } @@ -1197,7 +1296,7 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); $objWriter->endElement(); } - } else { + } elseif ($plotSeriesValues !== false) { $this->writeBubbles($plotSeriesValues, $objWriter); } } @@ -1223,7 +1322,7 @@ class Chart extends WriterPart $objWriter->startElement('c:strCache'); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesLabel->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesLabel->getPointCount()); $objWriter->endElement(); foreach ($plotSeriesLabel->getDataValues() as $plotLabelKey => $plotLabelValue) { @@ -1262,7 +1361,7 @@ class Chart extends WriterPart $objWriter->startElement('c:multiLvlStrCache'); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); for ($level = 0; $level < $levelCount; ++$level) { @@ -1308,7 +1407,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); $dataValues = $plotSeriesValues->getDataValues(); @@ -1333,6 +1432,14 @@ class Chart extends WriterPart } } + private const CUSTOM_COLOR_TYPES = [ + DataSeries::TYPE_BARCHART, + DataSeries::TYPE_BARCHART_3D, + DataSeries::TYPE_PIECHART, + DataSeries::TYPE_PIECHART_3D, + DataSeries::TYPE_DONUTCHART, + ]; + /** * Write Bubble Chart Details. */ @@ -1350,7 +1457,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); $dataValues = $plotSeriesValues->getDataValues(); @@ -1360,7 +1467,7 @@ class Chart extends WriterPart $objWriter->startElement('c:pt'); $objWriter->writeAttribute('idx', $plotSeriesKey); $objWriter->startElement('c:v'); - $objWriter->writeRawData(1); + $objWriter->writeRawData('1'); $objWriter->endElement(); $objWriter->endElement(); } @@ -1409,28 +1516,28 @@ class Chart extends WriterPart $x = $layout->getXPosition(); if ($x !== null) { $objWriter->startElement('c:x'); - $objWriter->writeAttribute('val', $x); + $objWriter->writeAttribute('val', "$x"); $objWriter->endElement(); } $y = $layout->getYPosition(); if ($y !== null) { $objWriter->startElement('c:y'); - $objWriter->writeAttribute('val', $y); + $objWriter->writeAttribute('val', "$y"); $objWriter->endElement(); } $w = $layout->getWidth(); if ($w !== null) { $objWriter->startElement('c:w'); - $objWriter->writeAttribute('val', $w); + $objWriter->writeAttribute('val', "$w"); $objWriter->endElement(); } $h = $layout->getHeight(); if ($h !== null) { $objWriter->startElement('c:h'); - $objWriter->writeAttribute('val', $h); + $objWriter->writeAttribute('val', "$h"); $objWriter->endElement(); } @@ -1449,8 +1556,8 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); $objWriter->startElement('mc:Choice'); - $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); $objWriter->writeAttribute('Requires', 'c14'); + $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); $objWriter->startElement('c14:style'); $objWriter->writeAttribute('val', '102'); @@ -1477,12 +1584,12 @@ class Chart extends WriterPart $objWriter->endElement(); $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->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(); $objWriter->startElement('c:pageSetup'); @@ -1492,14 +1599,24 @@ class Chart extends WriterPart $objWriter->endElement(); } - /** - * Write shadow properties. - * - * @param Axis|GridLines $xAxis - */ - private function writeShadow(XMLWriter $objWriter, $xAxis): void + private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void { - if ($xAxis->getShadowProperty('effect') === null) { + if ( + !empty($yAxis->getSoftEdgesSize()) + || !empty($yAxis->getShadowProperty('effect')) + || !empty($yAxis->getGlowProperty('size')) + ) { + $objWriter->startElement('a:effectLst'); + $this->writeGlow($objWriter, $yAxis); + $this->writeShadow($objWriter, $yAxis); + $this->writeSoftEdge($objWriter, $yAxis); + $objWriter->endElement(); // effectLst + } + } + + private function writeShadow(XMLWriter $objWriter, Properties $xAxis): void + { + if (empty($xAxis->getShadowProperty('effect'))) { return; } /** @var string */ @@ -1515,8 +1632,9 @@ class Chart extends WriterPart if (is_numeric($xAxis->getShadowProperty('direction'))) { $objWriter->writeAttribute('dir', Properties::angleToXml((float) $xAxis->getShadowProperty('direction'))); } - if ($xAxis->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn')); + $algn = $xAxis->getShadowProperty('algn'); + if (is_string($algn) && $algn !== '') { + $objWriter->writeAttribute('algn', $algn); } foreach (['sx', 'sy'] as $sizeType) { $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]); @@ -1530,29 +1648,17 @@ class Chart extends WriterPart $objWriter->writeAttribute($sizeType, Properties::angleToXml((float) $sizeValue)); } } - if ($xAxis->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape')); + $rotWithShape = $xAxis->getShadowProperty('rotWithShape'); + if (is_numeric($rotWithShape)) { + $objWriter->writeAttribute('rotWithShape', (string) (int) $rotWithShape); } - $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value'])); - $alpha = $xAxis->getShadowProperty(['color', 'alpha']); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); - } - $objWriter->endElement(); + $this->writeColor($objWriter, $xAxis->getShadowColorObject(), false); $objWriter->endElement(); } - /** - * Write glow properties. - * - * @param Axis|GridLines $yAxis - */ - private function writeGlow(XMLWriter $objWriter, $yAxis): void + private function writeGlow(XMLWriter $objWriter, Properties $yAxis): void { $size = $yAxis->getGlowProperty('size'); if (empty($size)) { @@ -1560,24 +1666,11 @@ class Chart extends WriterPart } $objWriter->startElement('a:glow'); $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $size)); - $objWriter->startElement("a:{$yAxis->getGlowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', (string) $yAxis->getGlowProperty(['color', 'value'])); - $alpha = $yAxis->getGlowProperty(['color', 'alpha']); - if (is_numeric($alpha)) { - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', Properties::alphaToXml((int) $alpha)); - $objWriter->endElement(); // alpha - } - $objWriter->endElement(); // color + $this->writeColor($objWriter, $yAxis->getGlowColorObject(), false); $objWriter->endElement(); // glow } - /** - * Write soft edge properties. - * - * @param Axis|GridLines $yAxis - */ - private function writeSoftEdge(XMLWriter $objWriter, $yAxis): void + private function writeSoftEdge(XMLWriter $objWriter, Properties $yAxis): void { $softEdgeSize = $yAxis->getSoftEdgesSize(); if (empty($softEdgeSize)) { @@ -1587,4 +1680,95 @@ class Chart extends WriterPart $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize)); $objWriter->endElement(); //end softEdge } + + private function writeLineStyles(XMLWriter $objWriter, Properties $gridlines, bool $noFill = false): void + { + $objWriter->startElement('a:ln'); + $widthTemp = $gridlines->getLineStyleProperty('width'); + if (is_numeric($widthTemp)) { + $objWriter->writeAttribute('w', Properties::pointsToXml((float) $widthTemp)); + } + $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap')); + $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound')); + if ($noFill) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); + } else { + $this->writeColor($objWriter, $gridlines->getLineColor()); + } + + $dash = $gridlines->getLineStyleProperty('dash'); + if (!empty($dash)) { + $objWriter->startElement('a:prstDash'); + $this->writeNotEmpty($objWriter, 'val', $dash); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty('join') === 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } elseif ($gridlines->getLineStyleProperty('join') === 'bevel') { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('head')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('head')); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('end')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('end')); + $objWriter->endElement(); + } + $objWriter->endElement(); //end ln + } + + private function writeNotEmpty(XMLWriter $objWriter, string $name, ?string $value): void + { + if ($value !== null && $value !== '') { + $objWriter->writeAttribute($name, $value); + } + } + + private function writeColor(XMLWriter $objWriter, ChartColor $chartColor, bool $solidFill = true): void + { + $type = $chartColor->getType(); + $value = $chartColor->getValue(); + if (!empty($type) && !empty($value)) { + if ($solidFill) { + $objWriter->startElement('a:solidFill'); + } + $objWriter->startElement("a:$type"); + $objWriter->writeAttribute('val', $value); + $alpha = $chartColor->getAlpha(); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); + $objWriter->endElement(); // a:alpha + } + $brightness = $chartColor->getBrightness(); + if (is_numeric($brightness)) { + $brightness = (int) $brightness; + $lumOff = 100 - $brightness; + $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 + } + $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr + if ($solidFill) { + $objWriter->endElement(); //a:solidFill + } + } + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php index ea0f1faa..5045e8f3 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php @@ -165,7 +165,7 @@ class Comments extends WriterPart // Metadata [$column, $row] = Coordinate::indexesFromString($cellReference); $id = 1024 + $column + $row; - $id = substr($id, 0, 4); + $id = substr("$id", 0, 4); // v:shape $objWriter->startElement('v:shape'); @@ -223,10 +223,10 @@ class Comments extends WriterPart $objWriter->writeElement('x:AutoFill', 'False'); // x:Row - $objWriter->writeElement('x:Row', ($row - 1)); + $objWriter->writeElement('x:Row', (string) ($row - 1)); // x:Column - $objWriter->writeElement('x:Column', ($column - 1)); + $objWriter->writeElement('x:Column', (string) ($column - 1)); $objWriter->endElement(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index b8285fcb..0ca9f64f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -115,10 +115,12 @@ class DefinedNames [, $range[0]] = Worksheet::extractSheetTitle($range[0], true); $range[0] = Coordinate::absoluteCoordinate($range[0]); - $range[1] = Coordinate::absoluteCoordinate($range[1]); + if (count($range) > 1) { + $range[1] = Coordinate::absoluteCoordinate($range[1]); + } $range = implode(':', $range); - $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle() ?? '') . '\'!' . $range); + $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range); $this->objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 43ce442f..02259c09 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -56,7 +56,7 @@ class DocProps extends WriterPart // Variant $objWriter->startElement('vt:variant'); - $objWriter->writeElement('vt:i4', $spreadsheet->getSheetCount()); + $objWriter->writeElement('vt:i4', (string) $spreadsheet->getSheetCount()); $objWriter->endElement(); $objWriter->endElement(); @@ -68,7 +68,7 @@ class DocProps extends WriterPart // Vector $objWriter->startElement('vt:vector'); - $objWriter->writeAttribute('size', $spreadsheet->getSheetCount()); + $objWriter->writeAttribute('size', (string) $spreadsheet->getSheetCount()); $objWriter->writeAttribute('baseType', 'lpstr'); $sheetCount = $spreadsheet->getSheetCount(); @@ -207,7 +207,7 @@ class DocProps extends WriterPart $objWriter->startElement('property'); $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'); - $objWriter->writeAttribute('pid', $key + 2); + $objWriter->writeAttribute('pid', (string) ($key + 2)); $objWriter->writeAttribute('name', $customProperty); switch ($propertyType) { @@ -216,7 +216,7 @@ class DocProps extends WriterPart break; case Properties::PROPERTY_TYPE_FLOAT: - $objWriter->writeElement('vt:r8', $propertyValue); + $objWriter->writeElement('vt:r8', sprintf('%F', $propertyValue)); break; case Properties::PROPERTY_TYPE_BOOLEAN: diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 238fb5bf..99fa2d34 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -67,12 +67,13 @@ class Rels extends WriterPart 'xl/workbook.xml' ); // a custom UI in workbook ? + $target = $spreadsheet->getRibbonXMLData('target'); if ($spreadsheet->hasRibbon()) { $this->writeRelationShip( $objWriter, 5, 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility', - $spreadsheet->getRibbonXMLData('target') + is_string($target) ? $target : '' ); } @@ -284,7 +285,7 @@ class Rels extends WriterPart return $objWriter->getData(); } - private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, $relationship, $type): void + private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, string $relationship, string $type): void { $unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData(); if (!isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()][$relationship])) { @@ -448,7 +449,7 @@ class Rels extends WriterPart /** * Write Override content type. * - * @param int $id Relationship ID. rId will be prepended! + * @param int|string $id Relationship ID. rId will be prepended! * @param string $type Relationship type * @param string $target Relationship target * @param string $targetMode Relationship target mode diff --git a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index da7d825b..078f940a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -65,7 +66,7 @@ class StringTable extends WriterPart /** * Write string table to XML format. * - * @param string[] $stringTable + * @param (string|RichText)[] $stringTable * * @return string XML Output */ @@ -85,13 +86,13 @@ class StringTable extends WriterPart // String table $objWriter->startElement('sst'); $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); - $objWriter->writeAttribute('uniqueCount', count($stringTable)); + $objWriter->writeAttribute('uniqueCount', (string) count($stringTable)); // Loop through string table foreach ($stringTable as $textElement) { $objWriter->startElement('si'); - if (!$textElement instanceof RichText) { + if (!($textElement instanceof RichText)) { $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement); $objWriter->startElement('t'); if ($textToWrite !== trim($textToWrite)) { @@ -99,7 +100,7 @@ class StringTable extends WriterPart } $objWriter->writeRawData($textToWrite); $objWriter->endElement(); - } elseif ($textElement instanceof RichText) { + } else { $this->writeRichText($objWriter, $textElement); } @@ -129,14 +130,16 @@ class StringTable extends WriterPart $objWriter->startElement($prefix . 'r'); // rPr - if ($element instanceof Run) { + if ($element instanceof Run && $element->getFont() !== null) { // rPr $objWriter->startElement($prefix . 'rPr'); // rFont - $objWriter->startElement($prefix . 'rFont'); - $objWriter->writeAttribute('val', $element->getFont()->getName()); - $objWriter->endElement(); + if ($element->getFont()->getName() !== null) { + $objWriter->startElement($prefix . 'rFont'); + $objWriter->writeAttribute('val', $element->getFont()->getName()); + $objWriter->endElement(); + } // Bold $objWriter->startElement($prefix . 'b'); @@ -165,19 +168,25 @@ class StringTable extends WriterPart $objWriter->endElement(); // Color - $objWriter->startElement($prefix . 'color'); - $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); - $objWriter->endElement(); + if ($element->getFont()->getColor()->getARGB() !== null) { + $objWriter->startElement($prefix . 'color'); + $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); + $objWriter->endElement(); + } // Size - $objWriter->startElement($prefix . 'sz'); - $objWriter->writeAttribute('val', $element->getFont()->getSize()); - $objWriter->endElement(); + if ($element->getFont()->getSize() !== null) { + $objWriter->startElement($prefix . 'sz'); + $objWriter->writeAttribute('val', (string) $element->getFont()->getSize()); + $objWriter->endElement(); + } // Underline - $objWriter->startElement($prefix . 'u'); - $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); - $objWriter->endElement(); + if ($element->getFont()->getUnderline() !== null) { + $objWriter->startElement($prefix . 'u'); + $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); + $objWriter->endElement(); + } $objWriter->endElement(); } @@ -198,16 +207,16 @@ class StringTable extends WriterPart * @param RichText|string $richText text string or Rich text * @param string $prefix Optional Namespace prefix */ - public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = null): void + public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = ''): void { - if (!$richText instanceof RichText) { + if (!($richText instanceof RichText)) { $textRun = $richText; $richText = new RichText(); - $run = $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun ?? ''); $run->setFont(null); } - if ($prefix !== null) { + if ($prefix !== '') { $prefix .= ':'; } @@ -225,9 +234,9 @@ class StringTable extends WriterPart } // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? '1' : '0')); // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? '1' : '0')); // Underline $underlineType = $element->getFont()->getUnderline(); switch ($underlineType) { @@ -240,7 +249,9 @@ class StringTable extends WriterPart break; } - $objWriter->writeAttribute('u', $underlineType); + if ($underlineType !== null) { + $objWriter->writeAttribute('u', $underlineType); + } // Strikethrough $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); // Superscript/subscript @@ -249,22 +260,10 @@ class StringTable extends WriterPart } // Color - $objWriter->startElement($prefix . 'solidFill'); - $objWriter->startElement($prefix . 'srgbClr'); - $objWriter->writeAttribute('val', $element->getFont()->getColor()->getRGB()); - $objWriter->endElement(); // srgbClr - $objWriter->endElement(); // solidFill + $this->writeChartTextColor($objWriter, $element->getFont()->getChartColor(), $prefix); // Underscore Color - if ($element->getFont()->getUSchemeClr()) { - $objWriter->startElement($prefix . 'uFill'); - $objWriter->startElement($prefix . 'solidFill'); - $objWriter->startElement($prefix . 'schemeClr'); - $objWriter->writeAttribute('val', $element->getFont()->getUSchemeClr()); - $objWriter->endElement(); // schemeClr - $objWriter->endElement(); // solidFill - $objWriter->endElement(); // uFill - } + $this->writeChartTextColor($objWriter, $element->getFont()->getUnderlineColor(), $prefix, 'uFill'); // fontName if ($element->getFont()->getLatin()) { @@ -295,6 +294,33 @@ class StringTable extends WriterPart } } + private function writeChartTextColor(XMLWriter $objWriter, ?ChartColor $underlineColor, string $prefix, ?string $openTag = ''): void + { + if ($underlineColor !== null) { + $type = $underlineColor->getType(); + $value = $underlineColor->getValue(); + if (!empty($type) && !empty($value)) { + if ($openTag !== '') { + $objWriter->startElement($prefix . $openTag); + } + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . $type); + $objWriter->writeAttribute('val', $value); + $alpha = $underlineColor->getAlpha(); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); // srgbClr/schemeClr/prstClr + $objWriter->endElement(); // solidFill + if ($openTag !== '') { + $objWriter->endElement(); // uFill + } + } + } + } + /** * Flip string table (for index searching). * diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/src/PhpSpreadsheet/Writer/Xlsx/Style.php index cb2e3850..0442c25d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Conditional; @@ -40,7 +41,7 @@ class Style extends WriterPart // numFmts $objWriter->startElement('numFmts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getNumFmtHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getNumFmtHashTable()->count()); // numFmt for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) { @@ -51,54 +52,63 @@ class Style extends WriterPart // fonts $objWriter->startElement('fonts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFontHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFontHashTable()->count()); // font for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) { - $this->writeFont($objWriter, $this->getParentWriter()->getFontHashTable()->getByIndex($i)); + $thisfont = $this->getParentWriter()->getFontHashTable()->getByIndex($i); + if ($thisfont !== null) { + $this->writeFont($objWriter, $thisfont); + } } $objWriter->endElement(); // fills $objWriter->startElement('fills'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFillHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFillHashTable()->count()); // fill for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) { - $this->writeFill($objWriter, $this->getParentWriter()->getFillHashTable()->getByIndex($i)); + $thisfill = $this->getParentWriter()->getFillHashTable()->getByIndex($i); + if ($thisfill !== null) { + $this->writeFill($objWriter, $thisfill); + } } $objWriter->endElement(); // borders $objWriter->startElement('borders'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getBordersHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getBordersHashTable()->count()); // border for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) { - $this->writeBorder($objWriter, $this->getParentWriter()->getBordersHashTable()->getByIndex($i)); + $thisborder = $this->getParentWriter()->getBordersHashTable()->getByIndex($i); + if ($thisborder !== null) { + $this->writeBorder($objWriter, $thisborder); + } } $objWriter->endElement(); // cellStyleXfs $objWriter->startElement('cellStyleXfs'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('numFmtId', 0); - $objWriter->writeAttribute('fontId', 0); - $objWriter->writeAttribute('fillId', 0); - $objWriter->writeAttribute('borderId', 0); + $objWriter->writeAttribute('numFmtId', '0'); + $objWriter->writeAttribute('fontId', '0'); + $objWriter->writeAttribute('fillId', '0'); + $objWriter->writeAttribute('borderId', '0'); $objWriter->endElement(); $objWriter->endElement(); // cellXfs $objWriter->startElement('cellXfs'); - $objWriter->writeAttribute('count', count($spreadsheet->getCellXfCollection())); + $objWriter->writeAttribute('count', (string) count($spreadsheet->getCellXfCollection())); // xf foreach ($spreadsheet->getCellXfCollection() as $cellXf) { @@ -109,24 +119,27 @@ class Style extends WriterPart // cellStyles $objWriter->startElement('cellStyles'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // cellStyle $objWriter->startElement('cellStyle'); $objWriter->writeAttribute('name', 'Normal'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('builtinId', 0); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('builtinId', '0'); $objWriter->endElement(); $objWriter->endElement(); // dxfs $objWriter->startElement('dxfs'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getStylesConditionalHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getStylesConditionalHashTable()->count()); // dxf for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) { - $this->writeCellStyleDxf($objWriter, $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i)->getStyle()); + $thisstyle = $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i); + if ($thisstyle !== null) { + $this->writeCellStyleDxf($objWriter, $thisstyle->getStyle()); + } } $objWriter->endElement(); @@ -171,17 +184,19 @@ class Style extends WriterPart // gradientFill $objWriter->startElement('gradientFill'); - $objWriter->writeAttribute('type', $fill->getFillType()); - $objWriter->writeAttribute('degree', $fill->getRotation()); + $objWriter->writeAttribute('type', (string) $fill->getFillType()); + $objWriter->writeAttribute('degree', (string) $fill->getRotation()); // stop $objWriter->startElement('stop'); $objWriter->writeAttribute('position', '0'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getStartColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -190,9 +205,11 @@ class Style extends WriterPart $objWriter->writeAttribute('position', '1'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getEndColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -220,7 +237,7 @@ class Style extends WriterPart // patternFill $objWriter->startElement('patternFill'); - $objWriter->writeAttribute('patternType', $fill->getFillType()); + $objWriter->writeAttribute('patternType', (string) $fill->getFillType()); if (self::writePatternColors($fill)) { // fgColor @@ -360,20 +377,20 @@ class Style extends WriterPart { // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('fontId', (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('fontId', (string) (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); if ($style->getQuotePrefix()) { - $objWriter->writeAttribute('quotePrefix', 1); + $objWriter->writeAttribute('quotePrefix', '1'); } if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { - $objWriter->writeAttribute('numFmtId', (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); + $objWriter->writeAttribute('numFmtId', (string) (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); } else { - $objWriter->writeAttribute('numFmtId', (int) $style->getNumberFormat()->getBuiltInFormatCode()); + $objWriter->writeAttribute('numFmtId', (string) (int) $style->getNumberFormat()->getBuiltInFormatCode()); } - $objWriter->writeAttribute('fillId', (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); - $objWriter->writeAttribute('borderId', (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); + $objWriter->writeAttribute('fillId', (string) (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); + $objWriter->writeAttribute('borderId', (string) (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); // Apply styles? $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $style->getFont()->getHashCode()) ? '1' : '0'); @@ -387,25 +404,31 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal !== '') { + $objWriter->writeAttribute('horizontal', $horizontal); + } + if ($vertical !== '') { + $objWriter->writeAttribute('vertical', $vertical); + } $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); $objWriter->writeAttribute('wrapText', ($style->getAlignment()->getWrapText() ? 'true' : 'false')); $objWriter->writeAttribute('shrinkToFit', ($style->getAlignment()->getShrinkToFit() ? 'true' : 'false')); if ($style->getAlignment()->getIndent() > 0) { - $objWriter->writeAttribute('indent', $style->getAlignment()->getIndent()); + $objWriter->writeAttribute('indent', (string) $style->getAlignment()->getIndent()); } if ($style->getAlignment()->getReadOrder() > 0) { - $objWriter->writeAttribute('readingOrder', $style->getAlignment()->getReadOrder()); + $objWriter->writeAttribute('readingOrder', (string) $style->getAlignment()->getReadOrder()); } $objWriter->endElement(); @@ -443,21 +466,23 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - if ($style->getAlignment()->getHorizontal() !== null) { - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal) { + $objWriter->writeAttribute('horizontal', $horizontal); } - if ($style->getAlignment()->getVertical() !== null) { - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + if ($vertical) { + $objWriter->writeAttribute('vertical', $vertical); } if ($style->getAlignment()->getTextRotation() !== null) { $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); } $objWriter->endElement(); @@ -465,7 +490,7 @@ class Style extends WriterPart $this->writeBorder($objWriter, $style->getBorders()); // protection - if (($style->getProtection()->getLocked() !== null) || ($style->getProtection()->getHidden() !== null)) { + if ((!empty($style->getProtection()->getLocked())) || (!empty($style->getProtection()->getHidden()))) { if ( $style->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT || $style->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT @@ -503,11 +528,13 @@ class Style extends WriterPart $objWriter->writeAttribute('style', $border->getBorderStyle()); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); - $objWriter->endElement(); + if ($border->getColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); + $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); + } } } @@ -516,15 +543,15 @@ class Style extends WriterPart * * @param int $id Number Format identifier */ - private function writeNumFmt(XMLWriter $objWriter, NumberFormat $numberFormat, $id = 0): void + private function writeNumFmt(XMLWriter $objWriter, ?NumberFormat $numberFormat, $id = 0): void { // Translate formatcode - $formatCode = $numberFormat->getFormatCode(); + $formatCode = ($numberFormat === null) ? null : $numberFormat->getFormatCode(); // numFmt if ($formatCode !== null) { $objWriter->startElement('numFmt'); - $objWriter->writeAttribute('numFmtId', ($id + 164)); + $objWriter->writeAttribute('numFmtId', (string) ($id + 164)); $objWriter->writeAttribute('formatCode', $formatCode); $objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index f9d7197d..7d08388d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -104,14 +104,14 @@ class Workbook extends WriterPart // workbookView $objWriter->startElement('workbookView'); - $objWriter->writeAttribute('activeTab', $spreadsheet->getActiveSheetIndex()); + $objWriter->writeAttribute('activeTab', (string) $spreadsheet->getActiveSheetIndex()); $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false')); - $objWriter->writeAttribute('firstSheet', $spreadsheet->getFirstSheetIndex()); + $objWriter->writeAttribute('firstSheet', (string) $spreadsheet->getFirstSheetIndex()); $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false')); $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false')); $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false')); $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false')); - $objWriter->writeAttribute('tabRatio', $spreadsheet->getTabRatio()); + $objWriter->writeAttribute('tabRatio', (string) $spreadsheet->getTabRatio()); $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility()); $objWriter->endElement(); @@ -157,9 +157,9 @@ class Workbook extends WriterPart $objWriter->writeAttribute('calcId', '999999'); $objWriter->writeAttribute('calcMode', 'auto'); // fullCalcOnLoad isn't needed if we've recalculating for the save - $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? 1 : 0); - $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? 0 : 1); - $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? 0 : 1); + $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? '1' : '0'); + $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? '0' : '1'); + $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? '0' : '1'); $objWriter->endElement(); } @@ -200,7 +200,7 @@ class Workbook extends WriterPart // Write sheet $objWriter->startElement('sheet'); $objWriter->writeAttribute('name', $worksheetName); - $objWriter->writeAttribute('sheetId', $worksheetId); + $objWriter->writeAttribute('sheetId', (string) $worksheetId); if ($sheetState !== 'visible' && $sheetState != '') { $objWriter->writeAttribute('state', $sheetState); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 660f7801..75b44706 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -25,7 +25,7 @@ class Worksheet extends WriterPart * * @return string XML Output */ - public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = null, $includeCharts = false) + public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false) { // Create XML writer $objWriter = null; @@ -146,7 +146,7 @@ class Worksheet extends WriterPart } $autoFilterRange = $worksheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { - $objWriter->writeAttribute('filterMode', 1); + $objWriter->writeAttribute('filterMode', '1'); if (!$worksheet->getAutoFilter()->getEvaluated()) { $worksheet->getAutoFilter()->showHideRows(); } @@ -163,7 +163,7 @@ class Worksheet extends WriterPart // tabColor if ($worksheet->isTabColorSet()) { $objWriter->startElement('tabColor'); - $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB()); + $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB() ?? ''); $objWriter->endElement(); } @@ -223,7 +223,7 @@ class Worksheet extends WriterPart // Show zeros (Excel also writes this attribute only if set to false) if ($worksheet->getSheetView()->getShowZeros() === false) { - $objWriter->writeAttribute('showZeros', 0); + $objWriter->writeAttribute('showZeros', '0'); } // View Layout Type @@ -257,7 +257,7 @@ class Worksheet extends WriterPart // Pane $pane = ''; if ($worksheet->getFreezePane()) { - [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane() ?? ''); + [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane()); $xSplit = Coordinate::columnIndexFromString($xSplit); --$xSplit; --$ySplit; @@ -266,7 +266,7 @@ class Worksheet extends WriterPart $pane = 'topRight'; $objWriter->startElement('pane'); if ($xSplit > 0) { - $objWriter->writeAttribute('xSplit', $xSplit); + $objWriter->writeAttribute('xSplit', "$xSplit"); } if ($ySplit > 0) { $objWriter->writeAttribute('ySplit', $ySplit); @@ -339,7 +339,7 @@ class Worksheet extends WriterPart $outlineLevelRow = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow); + $objWriter->writeAttribute('outlineLevelRow', (string) (int) $outlineLevelRow); // Outline level - column $outlineLevelCol = 0; @@ -348,7 +348,7 @@ class Worksheet extends WriterPart $outlineLevelCol = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol); + $objWriter->writeAttribute('outlineLevelCol', (string) (int) $outlineLevelCol); $objWriter->endElement(); } @@ -368,8 +368,8 @@ class Worksheet extends WriterPart foreach ($worksheet->getColumnDimensions() as $colDimension) { // col $objWriter->startElement('col'); - $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); - $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('min', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('max', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); if ($colDimension->getWidth() < 0) { // No width set, apply default of 10 @@ -401,11 +401,11 @@ class Worksheet extends WriterPart // Outline level if ($colDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel()); + $objWriter->writeAttribute('outlineLevel', (string) $colDimension->getOutlineLevel()); } // Style - $objWriter->writeAttribute('style', $colDimension->getXfIndex()); + $objWriter->writeAttribute('style', (string) $colDimension->getXfIndex()); $objWriter->endElement(); } @@ -428,7 +428,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm()); $objWriter->writeAttribute('hashValue', $protection->getPassword()); $objWriter->writeAttribute('saltValue', $protection->getSalt()); - $objWriter->writeAttribute('spinCount', $protection->getSpinCount()); + $objWriter->writeAttribute('spinCount', (string) $protection->getSpinCount()); } elseif ($protection->getPassword() !== '') { $objWriter->writeAttribute('password', $protection->getPassword()); } @@ -452,7 +452,7 @@ class Worksheet extends WriterPart $objWriter->endElement(); } - private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeAttribute($attr, $val); @@ -466,7 +466,7 @@ class Worksheet extends WriterPart } } - private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeElementIf(XMLWriter $objWriter, bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeElement($attr, $val); @@ -508,7 +508,7 @@ class Worksheet extends WriterPart private static function writeTimePeriodCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('timePeriod', $txt); if (empty($conditional->getConditions())) { if ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TODAY) { @@ -541,7 +541,7 @@ class Worksheet extends WriterPart private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('text', $txt); if (empty($conditional->getConditions())) { if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) { @@ -573,7 +573,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute($attrKey, $val); } $minCfvo = $dataBar->getMinimumConditionalFormatValueObject(); - if ($minCfvo) { + if ($minCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $minCfvo->getType()); if ($minCfvo->getCellFormula()) { @@ -583,7 +583,7 @@ class Worksheet extends WriterPart } $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject(); - if ($maxCfvo) { + if ($maxCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $maxCfvo->getType()); if ($maxCfvo->getCellFormula()) { @@ -605,9 +605,8 @@ class Worksheet extends WriterPart $objWriter->endElement(); //end conditionalFormatting } - private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void + private static function writeDataBarElements(XMLWriter $objWriter, ?ConditionalDataBar $dataBar): void { - /** @var ConditionalDataBar $dataBar */ if ($dataBar) { $objWriter->startElement('dataBar'); self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0'); @@ -674,7 +673,7 @@ class Worksheet extends WriterPart 'dxfId', (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) ); - $objWriter->writeAttribute('priority', $id++); + $objWriter->writeAttribute('priority', (string) $id++); self::writeAttributeif( $objWriter, @@ -729,7 +728,7 @@ class Worksheet extends WriterPart if (!empty($dataValidationCollection)) { $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); $objWriter->startElement('dataValidations'); - $objWriter->writeAttribute('count', count($dataValidationCollection)); + $objWriter->writeAttribute('count', (string) count($dataValidationCollection)); foreach ($dataValidationCollection as $coordinate => $dv) { $objWriter->startElement('dataValidation'); @@ -936,24 +935,24 @@ class Worksheet extends WriterPart { // pageSetup $objWriter->startElement('pageSetup'); - $objWriter->writeAttribute('paperSize', $worksheet->getPageSetup()->getPaperSize()); + $objWriter->writeAttribute('paperSize', (string) $worksheet->getPageSetup()->getPaperSize()); $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation()); if ($worksheet->getPageSetup()->getScale() !== null) { - $objWriter->writeAttribute('scale', $worksheet->getPageSetup()->getScale()); + $objWriter->writeAttribute('scale', (string) $worksheet->getPageSetup()->getScale()); } if ($worksheet->getPageSetup()->getFitToHeight() !== null) { - $objWriter->writeAttribute('fitToHeight', $worksheet->getPageSetup()->getFitToHeight()); + $objWriter->writeAttribute('fitToHeight', (string) $worksheet->getPageSetup()->getFitToHeight()); } else { $objWriter->writeAttribute('fitToHeight', '0'); } if ($worksheet->getPageSetup()->getFitToWidth() !== null) { - $objWriter->writeAttribute('fitToWidth', $worksheet->getPageSetup()->getFitToWidth()); + $objWriter->writeAttribute('fitToWidth', (string) $worksheet->getPageSetup()->getFitToWidth()); } else { $objWriter->writeAttribute('fitToWidth', '0'); } - if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) { - $objWriter->writeAttribute('firstPageNumber', $worksheet->getPageSetup()->getFirstPageNumber()); + if (!empty($worksheet->getPageSetup()->getFirstPageNumber())) { + $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder()); @@ -1006,8 +1005,8 @@ class Worksheet extends WriterPart // rowBreaks if (!empty($aRowBreaks)) { $objWriter->startElement('rowBreaks'); - $objWriter->writeAttribute('count', count($aRowBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks)); + $objWriter->writeAttribute('count', (string) count($aRowBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aRowBreaks)); foreach ($aRowBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); @@ -1024,14 +1023,14 @@ class Worksheet extends WriterPart // Second, write column breaks if (!empty($aColumnBreaks)) { $objWriter->startElement('colBreaks'); - $objWriter->writeAttribute('count', count($aColumnBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks)); + $objWriter->writeAttribute('count', (string) count($aColumnBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks)); foreach ($aColumnBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); $objWriter->startElement('brk'); - $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1); + $objWriter->writeAttribute('id', (string) (Coordinate::columnIndexFromString($coords[0]) - 1)); $objWriter->writeAttribute('man', '1'); $objWriter->endElement(); } @@ -1072,58 +1071,61 @@ class Worksheet extends WriterPart $currentRow = 0; while ($currentRow++ < $highestRow) { - // Get row dimension - $rowDimension = $worksheet->getRowDimension($currentRow); + $isRowSet = isset($cellsByRow[$currentRow]); + if ($isRowSet || $worksheet->rowDimensionExists($currentRow)) { + // Get row dimension + $rowDimension = $worksheet->getRowDimension($currentRow); - // Write current row? - $writeCurrentRow = isset($cellsByRow[$currentRow]) || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; + // Write current row? + $writeCurrentRow = $isRowSet || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; - if ($writeCurrentRow) { - // Start a new row - $objWriter->startElement('row'); - $objWriter->writeAttribute('r', $currentRow); - $objWriter->writeAttribute('spans', '1:' . $colCount); + if ($writeCurrentRow) { + // Start a new row + $objWriter->startElement('row'); + $objWriter->writeAttribute('r', "$currentRow"); + $objWriter->writeAttribute('spans', '1:' . $colCount); - // Row dimensions - if ($rowDimension->getRowHeight() >= 0) { - $objWriter->writeAttribute('customHeight', '1'); - $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); - } - - // Row visibility - if (!$rowDimension->getVisible() === true) { - $objWriter->writeAttribute('hidden', 'true'); - } - - // Collapsed - if ($rowDimension->getCollapsed() === true) { - $objWriter->writeAttribute('collapsed', 'true'); - } - - // Outline level - if ($rowDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); - } - - // Style - if ($rowDimension->getXfIndex() !== null) { - $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); - $objWriter->writeAttribute('customFormat', '1'); - } - - // Write cells - if (isset($cellsByRow[$currentRow])) { - // We have a comma-separated list of column names (with a trailing entry); split to an array - $columnsInRow = explode(',', $cellsByRow[$currentRow]); - array_pop($columnsInRow); - foreach ($columnsInRow as $column) { - // Write cell - $this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable); + // Row dimensions + if ($rowDimension->getRowHeight() >= 0) { + $objWriter->writeAttribute('customHeight', '1'); + $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); } - } - // End row - $objWriter->endElement(); + // Row visibility + if (!$rowDimension->getVisible() === true) { + $objWriter->writeAttribute('hidden', 'true'); + } + + // Collapsed + if ($rowDimension->getCollapsed() === true) { + $objWriter->writeAttribute('collapsed', 'true'); + } + + // Outline level + if ($rowDimension->getOutlineLevel() > 0) { + $objWriter->writeAttribute('outlineLevel', (string) $rowDimension->getOutlineLevel()); + } + + // Style + if ($rowDimension->getXfIndex() !== null) { + $objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex()); + $objWriter->writeAttribute('customFormat', '1'); + } + + // Write cells + if (isset($cellsByRow[$currentRow])) { + // We have a comma-separated list of column names (with a trailing entry); split to an array + $columnsInRow = explode(',', $cellsByRow[$currentRow]); + array_pop($columnsInRow); + foreach ($columnsInRow as $column) { + // Write cell + $this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable); + } + } + + // End row + $objWriter->endElement(); + } } } @@ -1143,7 +1145,7 @@ class Worksheet extends WriterPart StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags())) ); $objWriter->endElement(); - } elseif ($cellValue instanceof RichText) { + } else { $objWriter->startElement('is'); $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue); $objWriter->endElement(); @@ -1177,7 +1179,7 @@ class Worksheet extends WriterPart $cellValue = $cellValue . '.0'; } } - $objWriter->writeElement('v', $cellValue); + $objWriter->writeElement('v', "$cellValue"); } private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void @@ -1246,7 +1248,7 @@ class Worksheet extends WriterPart // Sheet styles $xfi = $pCell->getXfIndex(); - self::writeAttributeIf($objWriter, $xfi, 's', $xfi); + self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi"); // If cell value is supplied, write cell value $cellValue = $pCell->getValue(); @@ -1363,7 +1365,6 @@ class Worksheet extends WriterPart /** @var Conditional $conditional */ foreach ($conditionalStyles as $conditional) { $dataBar = $conditional->getDataBar(); - // @phpstan-ignore-next-line if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) { $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt(); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php index 6fc0c66a..a1bdf96a 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -144,6 +144,9 @@ class Xlfn . '|call' . '|let' . '|register[.]id' + . '|textafter' + . '|textbefore' + . '|textsplit' . '|valuetotext' . ')(?=\\s*[(])/i'; diff --git a/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php new file mode 100644 index 00000000..60c336cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/ArrayTest.php @@ -0,0 +1,34 @@ + [ + 0 => [ + 32 => [ + 'B' => 'PHP', + ], + ], + ], + 1 => [ + 0 => [ + 32 => [ + 'C' => 'Spreadsheet', + ], + ], + ], + ]; + + $values = Functions::flattenArray($array); + + self::assertIsNotArray($values[0]); + self::assertIsNotArray($values[1]); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php index 23ee5ba8..3ebc9888 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationErrorTest.php @@ -5,50 +5,26 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException; use PHPUnit\Framework\TestCase; -use Throwable; class CalculationErrorTest extends TestCase { - public function testCalculationException(): void + public function testCalculationExceptionSuppressed(): void { - $this->expectException(CalcException::class); - $this->expectExceptionMessage('Formula Error:'); $calculation = Calculation::getInstance(); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $calculation->setSuppressFormulaErrors(true); $result = $calculation->_calculateFormulaValue('=SUM('); + $calculation->setSuppressFormulaErrors(false); self::assertFalse($result); } - public function testCalculationError(): void + public function testCalculationException(): void { $calculation = Calculation::getInstance(); - $calculation->suppressFormulaErrors = true; - $error = false; - - try { - $calculation->_calculateFormulaValue('=SUM('); - } catch (Throwable $e) { - self::assertSame("Formula Error: Expecting ')'", $e->getMessage()); - self::assertSame('PHPUnit\\Framework\\Error\\Error', get_class($e)); - $error = true; - } - self::assertTrue($error); - } - - /** - * @param mixed $args - */ - public static function errhandler2(...$args): bool - { - return $args[0] === E_USER_ERROR; - } - - public function testCalculationErrorTrulySuppressed(): void - { - $calculation = Calculation::getInstance(); - $calculation->suppressFormulaErrors = true; - set_error_handler([self::class, 'errhandler2']); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $this->expectException(CalcException::class); + $this->expectExceptionMessage("Formula Error: Expecting ')'"); $result = $calculation->_calculateFormulaValue('=SUM('); - restore_error_handler(); self::assertFalse($result); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php new file mode 100644 index 00000000..d401f9c9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/FormulaParserTest.php @@ -0,0 +1,151 @@ +expectException(CalcException::class); + $this->expectExceptionMessage('Invalid parameter passed: formula'); + new FormulaParser(null); + } + + public function testInvalidTokenId(): void + { + $this->expectException(CalcException::class); + $this->expectExceptionMessage('Token with id 1 does not exist.'); + $result = new FormulaParser('=2'); + $result->getToken(1); + } + + public function testNoFormula(): void + { + $result = new FormulaParser(''); + self::assertSame(0, $result->getTokenCount()); + } + + /** + * @dataProvider providerFormulaParser + */ + public function testFormulaParser(string $formula, array $expectedResult): void + { + $formula = "=$formula"; + $result = new FormulaParser($formula); + self::assertSame($formula, $result->getFormula()); + self::assertSame(count($expectedResult), $result->getTokenCount()); + $tokens = $result->getTokens(); + $token0 = $result->getToken(0); + self::assertSame($tokens[0], $token0); + $idx = -1; + foreach ($expectedResult as $resultArray) { + ++$idx; + self::assertSame($resultArray[0], $tokens[$idx]->getValue()); + self::assertSame($resultArray[1], $tokens[$idx]->getTokenType()); + self::assertSame($resultArray[2], $tokens[$idx]->getTokenSubType()); + } + } + + public function providerFormulaParser(): array + { + return [ + ['5%*(2+(-3))+A3', + [ + ['5', 'Operand', 'Number'], + ['%', 'OperatorPostfix', 'Nothing'], + ['*', 'OperatorInfix', 'Math'], + ['', 'Subexpression', 'Start'], + ['2', 'Operand', 'Number'], + ['+', 'OperatorInfix', 'Math'], + ['', 'Subexpression', 'Start'], + ['-', 'OperatorPrefix', 'Nothing'], + ['3', 'Operand', 'Number'], + ['', 'Subexpression', 'Stop'], + ['', 'Subexpression', 'Stop'], + ['+', 'OperatorInfix', 'Math'], + ['A3', 'Operand', 'Range'], + ], + ], + ['"hello" & "goodbye"', + [ + ['hello', 'Operand', 'Text'], + ['&', 'OperatorInfix', 'Concatenation'], + ['goodbye', 'Operand', 'Text'], + ], + ], + ['+1.23E5', + [ + ['1.23E5', 'Operand', 'Number'], + ], + ], + ['#DIV/0!', + [ + ['#DIV/0!', 'Operand', 'Error'], + ], + ], + ['"HE""LLO"', + [ + ['HE"LLO', 'Operand', 'Text'], + ], + ], + ['MINVERSE({3,1;4,2})', + [ + ['MINVERSE', 'Function', 'Start'], + ['ARRAY', 'Function', 'Start'], + ['ARRAYROW', 'Function', 'Start'], + ['3', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['1', 'Operand', 'Number'], + ['', 'Function', 'Stop'], + [',', 'Argument', 'Nothing'], + ['ARRAYROW', 'Function', 'Start'], + ['4', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['2', 'Operand', 'Number'], + ['', 'Function', 'Stop'], + ['', 'Function', 'Stop'], + ['', 'Function', 'Stop'], + ], + ], + ['[1,1]*5', + [ + ['[1,1]', 'Operand', 'Range'], + ['*', 'OperatorInfix', 'Math'], + ['5', 'Operand', 'Number'], + ], + ], + ['IF(A1>=0,2,3)', + [ + ['IF', 'Function', 'Start'], + ['A1', 'Operand', 'Range'], + ['>=', 'OperatorInfix', 'Logical'], + ['0', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['2', 'Operand', 'Number'], + [',', 'OperatorInfix', 'Union'], + ['3', 'Operand', 'Number'], + ['', 'Function', 'Stop'], + ], + ], + ["'Worksheet'!A1:A3", + [ + ['Worksheet!A1:A3', 'Operand', 'Range'], + ], + ], + ["'Worksh''eet'!A1:A3", + [ + ['Worksh\'eet!A1:A3', 'Operand', 'Range'], + ], + ], + ['true', + [ + ['true', 'Operand', 'Logical'], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php index 9a448824..f15725cb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ConvertUoMTest.php @@ -4,15 +4,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ConvertUoMTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } + const UOM_PRECISION = 1E-12; public function testGetConversionGroups(): void { @@ -52,7 +48,7 @@ class ConvertUoMTest extends TestCase public function testCONVERTUOM($expectedResult, ...$args): void { $result = Engineering::CONVERTUOM(...$args); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::UOM_PRECISION); } public function providerCONVERTUOM(): array @@ -69,7 +65,7 @@ class ConvertUoMTest extends TestCase $formula = "=CONVERT({$value}, {$fromUoM}, {$toUoM})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::UOM_PRECISION); } public function providerConvertUoMArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php index f0a721c7..88e808ab 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfCTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfCTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERFC * @@ -25,7 +19,6 @@ class ErfCTest extends TestCase public function testERFC($expectedResult, $lower): void { $result = Engineering::ERFC($lower); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -43,7 +36,7 @@ class ErfCTest extends TestCase $formula = "=ERFC({$lower})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfCArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php index dc3ee84c..8a069ff1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfPreciseTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfPreciseTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERFPRECISE * @@ -25,7 +19,6 @@ class ErfPreciseTest extends TestCase public function testERFPRECISE($expectedResult, $limit): void { $result = Engineering::ERFPRECISE($limit); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -43,7 +36,7 @@ class ErfPreciseTest extends TestCase $formula = "=ERF.PRECISE({$limit})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfPreciseArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php index 4d13d47d..26e0381c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ErfTest.php @@ -4,18 +4,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ErfTest extends TestCase { const ERF_PRECISION = 1E-12; - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerERF * @@ -26,7 +20,6 @@ class ErfTest extends TestCase public function testERF($expectedResult, $lower, $upper = null): void { $result = Engineering::ERF($lower, $upper); - self::assertEquals($expectedResult, $result); self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } @@ -44,7 +37,7 @@ class ErfTest extends TestCase $formula = "=ERF({$lower}, {$upper})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::ERF_PRECISION); } public function providerErfArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php index 1022052e..a32d946f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/ParseComplexTest.php @@ -3,21 +3,15 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PHPUnit\Framework\TestCase; class ParseComplexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - public function testParseComplex(): void { [$real, $imaginary, $suffix] = [1.23e-4, 5.67e+8, 'j']; - $result = Engineering::parseComplex('1.23e-4+5.67e+8j'); + $result = /** @scrutinizer ignore-deprecated */ Engineering::parseComplex('1.23e-4+5.67e+8j'); self::assertArrayHasKey('real', $result); self::assertEquals($real, $result['real']); self::assertArrayHasKey('imaginary', $result); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php index 3c4bdb5a..2565f6c2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/IrrTest.php @@ -2,26 +2,45 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PHPUnit\Framework\TestCase; - -class IrrTest extends TestCase +class IrrTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerIRR * * @param mixed $expectedResult + * @param mixed $values */ - public function testIRR($expectedResult, ...$args): void + public function testIRR($expectedResult, $values = null): void { - $result = Financial::IRR(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $formula = '=IRR('; + if ($values !== null) { + if (is_array($values)) { + $row = 0; + foreach ($values as $value) { + if (is_array($value)) { + foreach ($value as $arrayValue) { + ++$row; + $sheet->getCell("A$row")->setValue($arrayValue); + } + } else { + ++$row; + $sheet->getCell("A$row")->setValue($value); + } + } + $formula .= "A1:A$row"; + } else { + $sheet->getCell('A1')->setValue($values); + $formula .= 'A1'; + } + } + $formula .= ')'; + $sheet->getCell('D1')->setValue($formula); + $result = $sheet->getCell('D1')->getCalculatedValue(); + $this->adjustResult($result, $expectedResult); + + self::assertEqualsWithDelta($expectedResult, $result, 0.1E-8); } public function providerIRR(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php new file mode 100644 index 00000000..87e0bb4e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressInternationalTest.php @@ -0,0 +1,79 @@ +locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale, string $r, string $c): void + { + if ($locale !== '') { + Settings::setLocale($locale); + } + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerInternational(): array + { + return [ + 'Default' => ['', 'R', 'C'], + 'English' => ['en', 'R', 'C'], + 'French' => ['fr', 'L', 'C'], + 'German' => ['de', 'Z', 'S'], + 'Made-up' => ['xx', 'R', 'C'], + 'Spanish' => ['es', 'F', 'C'], + 'Bulgarian' => ['bg', 'R', 'C'], + 'Czech' => ['cs', 'R', 'C'], // maybe should be R/S + 'Polish' => ['pl', 'R', 'C'], // maybe should be W/K + 'Turkish' => ['tr', 'R', 'C'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode, string $r, string $c): void + { + Functions::setCompatibilityMode($compatibilityMode); + Settings::setLocale('de'); + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=LEFT(ADDRESS(1,1,1,0),1)'); + $sheet->getCell('A2')->setValue('=MID(ADDRESS(1,1,1,0),3,1)'); + self::assertSame($r, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($c, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL, 'Z', 'S'], + [Functions::COMPATIBILITY_OPENOFFICE, 'R', 'C'], + [Functions::COMPATIBILITY_GNUMERIC, 'R', 'C'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php index 2b92030e..2a6ae883 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php @@ -3,17 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class AddressTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerADDRESS * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php index f4f9bb9e..67135af0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnsTest.php @@ -3,25 +3,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class ColumnsTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOLUMNS * * @param mixed $expectedResult + * @param null|array|string $arg */ - public function testCOLUMNS($expectedResult, ...$args): void + public function testCOLUMNS($expectedResult, $arg): void { - $result = LookupRef::COLUMNS(...$args); + $result = LookupRef::COLUMNS($arg); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php index ccd689b1..6c91f3fc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/FormulaTextTest.php @@ -11,6 +11,7 @@ class FormulaTextTest extends AllSetupTeardown { /** * @param mixed $value + * * @dataProvider providerFormulaText */ public function testFormulaText(string $expectedResult, $value): void diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php index 0ce3513b..084e3d9f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/HLookupTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -91,6 +92,44 @@ class HLookupTest extends TestCase self::assertSame($expectedResult, $result); } + /** + * @dataProvider providerHLookupNamedRange + */ + public function testHLookupNamedRange(string $expectedResult, string $cellAddress): void + { + $lookupData = [ + ['Rating', 1, 2, 3, 4], + ['Level', 'Poor', 'Average', 'Good', 'Excellent'], + ]; + $formData = [ + ['Category', 'Rating', 'Level'], + ['Service', 2, '=HLOOKUP(C5,Lookup_Table,2,FALSE)'], + ['Quality', 3, '=HLOOKUP(C6,Lookup_Table,2,FALSE)'], + ['Value', 4, '=HLOOKUP(C7,Lookup_Table,2,FALSE)'], + ['Cleanliness', 3, '=HLOOKUP(C8,Lookup_Table,2,FALSE)'], + ]; + + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($lookupData, null, 'F4'); + $worksheet->fromArray($formData, null, 'B4'); + + $spreadsheet->addNamedRange(new NamedRange('Lookup_Table', $worksheet, '=$G$4:$J$5')); + + $result = $worksheet->getCell($cellAddress)->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerHLookupNamedRange(): array + { + return [ + ['Average', 'D5'], + ['Good', 'D6'], + ['Excellent', 'D7'], + ['Good', 'D8'], + ]; + } + /** * @dataProvider providerHLookupArray */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php new file mode 100644 index 00000000..de3832b9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexOnSpreadsheetTest.php @@ -0,0 +1,39 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray($matrix); + $maxRow = $sheet->getHighestRow(); + $maxColumn = $sheet->getHighestColumn(); + $formulaArray = "A1:$maxColumn$maxRow"; + if ($rowNum === null) { + $formula = "=INDEX($formulaArray)"; + } elseif ($colNum === null) { + $formula = "=INDEX($formulaArray, $rowNum)"; + } else { + $formula = "=INDEX($formulaArray, $rowNum, $colNum)"; + } + $sheet->getCell('ZZ98')->setValue('x'); + $sheet->getCell('ZZ99')->setValue($formula); + $result = $sheet->getCell('ZZ99')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerINDEXonSpreadsheet(): array + { + return require 'tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php index 03c87b50..3fe5d041 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php @@ -3,25 +3,28 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix; use PHPUnit\Framework\TestCase; class IndexTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerINDEX * * @param mixed $expectedResult + * @param mixed $matrix + * @param mixed $rowNum + * @param mixed $colNum */ - public function testINDEX($expectedResult, ...$args): void + public function testINDEX($expectedResult, $matrix, $rowNum = null, $colNum = null): void { - $result = LookupRef::INDEX(...$args); + if ($rowNum === null) { + $result = Matrix::index($matrix); + } elseif ($colNum === null) { + $result = Matrix::index($matrix, $rowNum); + } else { + $result = Matrix::index($matrix, $rowNum, $colNum); + } self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php new file mode 100644 index 00000000..7d6d0a65 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectInternationalTest.php @@ -0,0 +1,132 @@ +locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + Settings::setLocale($this->locale); + // CompatibilityMode is restored in parent + parent::tearDown(); + } + + /** + * @dataProvider providerInternational + */ + public function testR1C1International(string $locale): void + { + Settings::setLocale($locale); + $sameAsEnglish = ['en', 'xx', 'ru', 'tr', 'cs', 'pl']; + $sheet = $this->getSheet(); + $sheet->getCell('C1')->setValue('text'); + $sheet->getCell('A2')->setValue('en'); + $sheet->getCell('B2')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A3')->setValue('fr'); + $sheet->getCell('B3')->setValue('=INDIRECT("L1C3", false)'); + $sheet->getCell('A4')->setValue('de'); + $sheet->getCell('B4')->setValue('=INDIRECT("Z1S3", false)'); + $sheet->getCell('A5')->setValue('es'); + $sheet->getCell('B5')->setValue('=INDIRECT("F1C3", false)'); + $sheet->getCell('A6')->setValue('xx'); + $sheet->getCell('B6')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A7')->setValue('ru'); + $sheet->getCell('B7')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A8')->setValue('cs'); + $sheet->getCell('B8')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A9')->setValue('tr'); + $sheet->getCell('B9')->setValue('=INDIRECT("R1C3", false)'); + $sheet->getCell('A10')->setValue('pl'); + $sheet->getCell('B10')->setValue('=INDIRECT("R1C3", false)'); + $maxRow = $sheet->getHighestRow(); + for ($row = 2; $row <= $maxRow; ++$row) { + $rowLocale = $sheet->getCell("A$row")->getValue(); + if (in_array($rowLocale, $sameAsEnglish, true) && in_array($locale, $sameAsEnglish, true)) { + $expectedResult = 'text'; + } else { + $expectedResult = ($locale === $sheet->getCell("A$row")->getValue()) ? 'text' : '#REF!'; + } + self::assertSame($expectedResult, $sheet->getCell("B$row")->getCalculatedValue(), "Locale $locale error in cell B$row $rowLocale"); + } + } + + public function providerInternational(): array + { + return [ + 'English' => ['en'], + 'French' => ['fr'], + 'German' => ['de'], + 'Made-up' => ['xx'], + 'Spanish' => ['es'], + 'Russian' => ['ru'], + 'Czech' => ['cs'], + 'Polish' => ['pl'], + 'Turkish' => ['tr'], + ]; + } + + /** + * @dataProvider providerRelativeInternational + */ + public function testRelativeInternational(string $locale, string $cell, string $relative): void + { + Settings::setLocale($locale); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell($cell)->setValue("=INDIRECT(\"$relative\", false)"); + self::assertSame('text', $sheet->getCell($cell)->getCalculatedValue()); + } + + public function providerRelativeInternational(): array + { + return [ + 'English A3' => ['en', 'A3', 'R[]C[+2]'], + 'French B4' => ['fr', 'B4', 'L[-1]C[+1]'], + 'German C5' => ['de', 'C5', 'Z[-2]S[]'], + 'Spanish E1' => ['es', 'E1', 'F[+2]C[-2]'], + ]; + } + + /** + * @dataProvider providerCompatibility + */ + public function testCompatibilityInternational(string $compatibilityMode): void + { + Functions::setCompatibilityMode($compatibilityMode); + if ($compatibilityMode === Functions::COMPATIBILITY_EXCEL) { + $expected1 = '#REF!'; + $expected2 = 'text'; + } else { + $expected2 = '#REF!'; + $expected1 = 'text'; + } + Settings::setLocale('fr'); + $sheet = $this->getSheet(); + $sheet->getCell('C3')->setValue('text'); + $sheet->getCell('A1')->setValue('=INDIRECT("R3C3", false)'); + $sheet->getCell('A2')->setValue('=INDIRECT("L3C3", false)'); + self::assertSame($expected1, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame($expected2, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerCompatibility(): array + { + return [ + [Functions::COMPATIBILITY_EXCEL], + [Functions::COMPATIBILITY_OPENOFFICE], + [Functions::COMPATIBILITY_GNUMERIC], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php index 7601e336..accfc058 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php @@ -132,4 +132,48 @@ class IndirectTest extends AllSetupTeardown $result = \PhpOffice\PhpSpreadsheet\Calculation\Functions::flattenSingleValue($result); self::assertSame('This is it', $result); } + + /** + * @param null|int|string $expectedResult + * + * @dataProvider providerRelative + */ + public function testR1C1Relative($expectedResult, string $address): void + { + $sheet = $this->getSheet(); + $sheet->fromArray([ + ['a1', 'b1', 'c1'], + ['a2', 'b2', 'c2'], + ['a3', 'b3', 'c3'], + ['a4', 'b4', 'c4'], + ]); + $sheet->getCell('B2')->setValue('=INDIRECT("' . $address . '", false)'); + self::assertSame($expectedResult, $sheet->getCell('B2')->getCalculatedValue()); + } + + public function providerRelative(): array + { + return [ + 'same row with bracket next column' => ['c2', 'R[]C[+1]'], + 'same row without bracket next column' => ['c2', 'RC[+1]'], + 'same row without bracket next column no plus sign' => ['c2', 'RC[1]'], + 'same row previous column' => ['a2', 'RC[-1]'], + 'previous row previous column' => ['a1', 'R[-1]C[-1]'], + 'previous row same column with bracket' => ['b1', 'R[-1]C[]'], + 'previous row same column without bracket' => ['b1', 'R[-1]C'], + 'previous row next column' => ['c1', 'R[-1]C[+1]'], + 'next row no plus sign previous column' => ['a3', 'R[1]C[-1]'], + 'next row previous column' => ['a3', 'R[+1]C[-1]'], + 'next row same column' => ['b3', 'R[+1]C'], + 'next row next column' => ['c3', 'R[+1]C[+1]'], + 'two rows down same column' => ['b4', 'R[+2]C'], + 'invalid row' => ['#REF!', 'R[-2]C'], + 'invalid column' => ['#REF!', 'RC[-2]'], + 'circular reference' => [0, 'RC'], // matches Excel's treatment + 'absolute row absolute column' => ['c2', 'R2C3'], + 'absolute row relative column' => ['a2', 'R2C[-1]'], + 'relative row absolute column lowercase' => ['a2', 'rc1'], + 'uninitialized cell' => [null, 'RC[+2]'], // Excel result is 0 + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php index e0786505..22787e25 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\NamedRange; class OffsetTest extends AllSetupTeardown { @@ -46,4 +47,18 @@ class OffsetTest extends AllSetupTeardown $sheet->getCell('A5')->setValue('=OFFSET(C1, 0, 0)'); self::assertSame(5, $sheet->getCell('A5')->getCalculatedValue()); } + + public function testOffsetNamedRange(): void + { + $workSheet = $this->getSheet(); + $workSheet->setCellValue('A1', 1); + $workSheet->setCellValue('A2', 2); + + $this->getSpreadsheet()->addNamedRange(new NamedRange('demo', $workSheet, '=$A$1')); + + $workSheet->setCellValue('B1', '=demo'); + $workSheet->setCellValue('B2', '=OFFSET(demo, 1, 0)'); + + self::assertSame(2, $workSheet->getCell('B2')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php index 2155bdf1..33c9dc45 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowsTest.php @@ -3,25 +3,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PHPUnit\Framework\TestCase; class RowsTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerROWS * * @param mixed $expectedResult + * @param null|array|string $arg */ - public function testROWS($expectedResult, ...$args): void + public function testROWS($expectedResult, $arg): void { - $result = LookupRef::ROWS(...$args); + $result = LookupRef::ROWS($arg); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php index 4e05ea8a..5b7647be 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VLookupTest.php @@ -3,26 +3,42 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class VLookupTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerVLOOKUP * * @param mixed $expectedResult + * @param mixed $value + * @param mixed $table + * @param mixed $index */ - public function testVLOOKUP($expectedResult, ...$args): void + public function testVLOOKUP($expectedResult, $value, $table, $index, ?bool $lookup = null): void { - $result = LookupRef::VLOOKUP(...$args); + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + if (is_array($table)) { + $sheet->fromArray($table); + $dimension = $sheet->calculateWorksheetDimension(); + } else { + $sheet->getCell('A1')->setValue($table); + $dimension = 'A1'; + } + if ($lookup === null) { + $lastarg = ''; + } else { + $lastarg = $lookup ? ',TRUE' : ',FALSE'; + } + $sheet->getCell('Z98')->setValue($value); + $sheet->getCell('Z97')->setValue($index); + + $sheet->getCell('Z99')->setValue("=VLOOKUP(Z98,$dimension,Z97$lastarg)"); + $result = $sheet->getCell('Z99')->getCalculatedValue(); self::assertEquals($expectedResult, $result); + $spreadsheet->disconnectWorksheets(); } public function providerVLOOKUP(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php index 328773e0..6bdfa570 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class FactTest extends AllSetupTeardown { + const FACT_PRECISION = 1E-12; + /** * @dataProvider providerFACT * @@ -53,7 +55,7 @@ class FactTest extends AllSetupTeardown $sheet->getCell('B1')->setValue('=FACT(A1)'); } $result = $sheet->getCell('B1')->getCalculatedValue(); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FACT_PRECISION); } public function providerFACTGnumeric(): array @@ -70,7 +72,7 @@ class FactTest extends AllSetupTeardown $formula = "=FACT({$array})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::FACT_PRECISION); } public function providerFactArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php index 96ebc45b..9ac68ee5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\MatrixFunctions; class MUnitTest extends AllSetupTeardown { + const MU_PRECISION = 1.0E-12; + public function testMUNIT(): void { $identity = MatrixFunctions::identity(3); @@ -15,7 +17,7 @@ class MUnitTest extends AllSetupTeardown self::assertEquals($startArray, $resultArray); $inverseArray = MatrixFunctions::inverse($startArray); $resultArray = MatrixFunctions::multiply($startArray, $inverseArray); - self::assertEquals($identity, $resultArray); + self::assertEqualsWithDelta($identity, $resultArray, self::MU_PRECISION); self::assertEquals('#VALUE!', MatrixFunctions::identity(0)); self::assertEquals('#VALUE!', MatrixFunctions::identity(-1)); self::assertEquals('#VALUE!', MatrixFunctions::identity('X')); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php index 99cc1fa8..50b7117c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php @@ -60,7 +60,7 @@ class RandBetweenTest extends AllSetupTeardown $formula = "=RandBetween({$argument1}, {$argument2})"; $result = $calculation->_calculateFormulaValue($formula); self::assertIsArray($result); - self::assertCount($expectedRows, $result); + self::assertCount($expectedRows, /** @scrutinizer ignore-type */ $result); self::assertIsArray($result[0]); self::assertCount($expectedColumns, /** @scrutinizer ignore-type */ $result[0]); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php index cf79ac0b..43156182 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php @@ -127,4 +127,32 @@ class SubTotalTest extends AllSetupTeardown $sheet->getCell('H1')->setValue("=SUBTOTAL(9, A1:$maxCol$maxRow)"); self::assertEquals(362, $sheet->getCell('H1')->getCalculatedValue()); } + + public function testRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, #REF!)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } + + public function testSecondaryRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, B1:B9,#REF!,C1:C9)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } + + public function testNonStringSingleCellRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, 1, C1, Sheet99!A11)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } + + public function testNonStringCellRangeRefError(): void + { + $sheet = $this->getSheet(); + $sheet->getCell('A1')->setValue('=SUBTOTAL(9, Sheet99!A1)'); + self::assertEquals('#REF!', $sheet->getCell('A1')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php index 3f80b8ec..780b9623 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; +use PhpOffice\PhpSpreadsheet\Spreadsheet; + class SumTest extends AllSetupTeardown { /** @@ -44,4 +46,37 @@ class SumTest extends AllSetupTeardown { return require 'tests/data/Calculation/MathTrig/SUMLITERALS.php'; } + + /** + * @dataProvider providerSUMWITHINDEXMATCH + * + * @param mixed $expectedResult + */ + public function testSumWithIndexMatch($expectedResult, string $formula): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->fromArray( + [ + ['Number', 'Formula'], + [83, $formula], + ] + ); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->fromArray( + [ + ['Lookup', 'Match'], + [83, 16], + ] + ); + self::assertSame($expectedResult, $sheet1->getCell('B2')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function providerSUMWITHINDEXMATCH(): array + { + return require 'tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php new file mode 100644 index 00000000..81fa5a53 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ArrayToTextTest.php @@ -0,0 +1,24 @@ +getSheet(); + $worksheet->fromArray($testData, null, 'A1', true); + $worksheet->getCell('H1')->setValue("=ARRAYTOTEXT(A1:C5, {$mode})"); + + $result = $worksheet->getCell('H1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerARRAYTOTEXT(): array + { + return require 'tests/data/Calculation/TextData/ARRAYTOTEXT.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php index 6c9a871d..53ce2435 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ConcatenateTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; +use PhpOffice\PhpSpreadsheet\Spreadsheet; + class ConcatenateTest extends AllSetupTeardown { /** @@ -30,4 +32,27 @@ class ConcatenateTest extends AllSetupTeardown { return require 'tests/data/Calculation/TextData/CONCATENATE.php'; } + + public function testConcatenateWithIndexMatch(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->fromArray( + [ + ['Number', 'Formula'], + [52101293, '=CONCAT(INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)))'], + ] + ); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->fromArray( + [ + ['Lookup', 'Match'], + [52101293, 'PHP'], + ] + ); + self::assertSame('PHP', $sheet1->getCell('B2')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php index a100695f..1a909932 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/NumberValueTest.php @@ -6,6 +6,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation; class NumberValueTest extends AllSetupTeardown { + const NV_PRECISION = 1.0E-8; + /** * @dataProvider providerNUMBERVALUE * @@ -34,7 +36,7 @@ class NumberValueTest extends AllSetupTeardown $sheet->getCell('B1')->setValue('=NUMBERVALUE(A1, A2, A3)'); } $result = $sheet->getCell('B1')->getCalculatedValue(); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::NV_PRECISION); } public function providerNUMBERVALUE(): array @@ -51,7 +53,7 @@ class NumberValueTest extends AllSetupTeardown $formula = "=NumberValue({$argument1}, {$argument2}, {$argument3})"; $result = $calculation->_calculateFormulaValue($formula); - self::assertEqualsWithDelta($expectedResult, $result, 1.0e-14); + self::assertEqualsWithDelta($expectedResult, $result, self::NV_PRECISION); } public function providerNumberValueArray(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php new file mode 100644 index 00000000..00483260 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextAfterTest.php @@ -0,0 +1,36 @@ +getSheet(); + $worksheet->getCell('A1')->setValue($text); + $worksheet->getCell('A2')->setValue((is_array($delimiter)) ? $delimiter[0] : $delimiter); + if (is_array($delimiter)) { + $worksheet->getCell('A3')->setValue($delimiter[1]); + } + $worksheet->getCell('B1')->setValue("=TEXTAFTER({$args})"); + + $result = $worksheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerTEXTAFTER(): array + { + return require 'tests/data/Calculation/TextData/TEXTAFTER.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php new file mode 100644 index 00000000..37e46636 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextBeforeTest.php @@ -0,0 +1,36 @@ +getSheet(); + $worksheet->getCell('A1')->setValue($text); + $worksheet->getCell('A2')->setValue((is_array($delimiter)) ? $delimiter[0] : $delimiter); + if (is_array($delimiter)) { + $worksheet->getCell('A3')->setValue($delimiter[1]); + } + $worksheet->getCell('B1')->setValue("=TEXTBEFORE({$args})"); + + $result = $worksheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerTEXTBEFORE(): array + { + return require 'tests/data/Calculation/TextData/TEXTBEFORE.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php new file mode 100644 index 00000000..e0fec8b9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TextSplitTest.php @@ -0,0 +1,60 @@ + $value) { + ++$index; + $worksheet->getCell("{$column}{$index}")->setValue($value); + } + } else { + $worksheet->getCell("{$column}1")->setValue($argument); + } + } + + /** + * @dataProvider providerTEXTSPLIT + */ + public function testTextSplit(array $expectedResult, array $arguments): void + { + $text = $arguments[0]; + $columnDelimiter = $arguments[1]; + $rowDelimiter = $arguments[2]; + + $args = 'A1'; + $args .= (is_array($columnDelimiter)) ? ', ' . $this->setDelimiterArgument($columnDelimiter, 'B') : ', B1'; + $args .= (is_array($rowDelimiter)) ? ', ' . $this->setDelimiterArgument($rowDelimiter, 'C') : ', C1'; + $args .= (isset($arguments[3])) ? ", {$arguments[3]}" : ','; + $args .= (isset($arguments[4])) ? ", {$arguments[4]}" : ','; + $args .= (isset($arguments[5])) ? ", {$arguments[5]}" : ','; + + $worksheet = $this->getSheet(); + $worksheet->getCell('A1')->setValue($text); + $this->setDelimiterValues($worksheet, 'B', $columnDelimiter); + $this->setDelimiterValues($worksheet, 'C', $rowDelimiter); + $worksheet->getCell('H1')->setValue("=TEXTSPLIT({$args})"); + + $result = Calculation::getInstance($this->getSpreadsheet())->calculateCellValue($worksheet->getCell('H1')); + self::assertSame($expectedResult, $result); + } + + public function providerTEXTSPLIT(): array + { + return require 'tests/data/Calculation/TextData/TEXTSPLIT.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php new file mode 100644 index 00000000..574d4f56 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueToTextTest.php @@ -0,0 +1,28 @@ +getSheet(); + $this->setCell('A1', $value); + $sheet->getCell('B1')->setValue("=VALUETOTEXT(A1, {$format})"); + + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public function providerVALUE(): array + { + return require 'tests/data/Calculation/TextData/VALUETOTEXT.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php index 92330b7d..0ecb3f8b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/ParseFormulaTest.php @@ -162,6 +162,78 @@ class ParseFormulaTest extends TestCase ], '=B:C', ], + 'Combined Cell Reference and Column Range' => [ + [ + ['type' => 'Column Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'sheet1'!A1048576", 'reference' => "'sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "=MIN('sheet1'!A:A) + 'sheet1'!A1", + ], + 'Combined Cell Reference and Column Range with quote' => [ + [ + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "=MIN('Mark''s sheet1'!A:A) + 'Mark''s sheet1'!A1", + ], + 'Combined Cell Reference and Column Range with unescaped quote' => [ + [ + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "=MIN('Mark's sheet1'!A:A) + 'Mark's sheet1'!A1", + ], + 'Combined Column Range and Cell Reference' => [ + [ + ['type' => 'Cell Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'sheet1'!A1", 'reference' => "'sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'sheet1'!A1048576", 'reference' => "'sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "='sheet1'!A1 + MIN('sheet1'!A:A)", + ], + 'Combined Column Range and Cell Reference with quote' => [ + [ + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "='Mark''s sheet1'!A1 + MIN('Mark''s sheet1'!A:A)", + ], + 'Combined Column Range and Cell Reference with unescaped quote' => [ + [ + ['type' => 'Cell Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1", 'reference' => "'Mark's sheet1'!A1"], + ['type' => 'Column Reference', 'value' => "'Mark's sheet1'!A1048576", 'reference' => "'Mark's sheet1'!A1048576"], + ['type' => 'Binary Operator', 'value' => ':', 'reference' => null], + ['type' => 'Operand Count for Function MIN()', 'value' => 1, 'reference' => null], + ['type' => 'Function', 'value' => 'MIN(', 'reference' => null], + ['type' => 'Binary Operator', 'value' => '+', 'reference' => null], + ], + "='Mark's sheet1'!A1 + MIN('Mark's sheet1'!A:A)", + ], 'Range with Defined Names' => [ [ ['type' => 'Defined Name', 'value' => 'GROUP1', 'reference' => 'GROUP1'], diff --git a/tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php b/tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php new file mode 100644 index 00000000..ffdfc2fa --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/RefErrorTest.php @@ -0,0 +1,58 @@ +getActiveSheet(); + $sheet1->setTitle('Sheet1'); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Sheet2'); + $sheet2->getCell('A1')->setValue(5); + $sheet1->getCell('A1')->setValue(9); + $sheet1->getCell('A2')->setValue(2); + $sheet1->getCell('A3')->setValue(4); + $sheet1->getCell('A4')->setValue(6); + $sheet1->getCell('A5')->setValue(7); + $sheet1->getRowDimension(5)->setVisible(false); + $sheet1->getCell('B1')->setValue('=1/0'); + $sheet1->getCell('C1')->setValue('=Sheet99!A1'); + $sheet1->getCell('C2')->setValue('=Sheet2!A1'); + $sheet1->getCell('C3')->setValue('=Sheet2!A2'); + $sheet1->getCell('H1')->setValue($formula); + self::assertSame($expected, $sheet1->getCell('H1')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } + + public function providerRefError(): array + { + return [ + 'Subtotal9 Ok' => [12, '=SUBTOTAL(A1,A2:A4)'], + 'Subtotal9 REF' => ['#REF!', '=SUBTOTAL(A1,A2:A4,C1)'], + 'Subtotal9 with literal and cells' => [111, '=SUBTOTAL(A1,A2:A4,99)'], + 'Subtotal9 with literal no rows hidden' => [111, '=SUBTOTAL(109,A2:A4,99)'], + 'Subtotal9 with literal ignoring hidden row' => [111, '=SUBTOTAL(109,A2:A5,99)'], + 'Subtotal9 with literal using hidden row' => [118, '=SUBTOTAL(9,A2:A5,99)'], + 'Subtotal9 with Null same sheet' => [12, '=SUBTOTAL(A1,A2:A4,A99)'], + 'Subtotal9 with Null Different sheet' => [12, '=SUBTOTAL(A1,A2:A4,C3)'], + 'Subtotal9 with NonNull Different sheet' => [17, '=SUBTOTAL(A1,A2:A4,C2)'], + 'Product DIV0' => ['#DIV/0!', '=PRODUCT(2, 3, B1)'], + 'Sqrt REF' => ['#REF!', '=SQRT(C1)'], + 'Sum NUM' => ['#NUM!', '=SUM(SQRT(-1), A2:A4)'], + 'Sum with literal and cells' => [111, '=SUM(A2:A4, 99)'], + 'Sum REF' => ['#REF!', '=SUM(A2:A4, C1)'], + 'Tan DIV0' => ['#DIV/0!', '=TAN(B1)'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php b/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php new file mode 100644 index 00000000..b1152f3f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php @@ -0,0 +1,34 @@ +getActiveSheet(); + // Note use Armenian character below to make sure chars, not bytes + $longstring = str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5); + $sheet->getCell('C1')->setValue($longstring); + self::assertSame($longstring, $sheet->getCell('C1')->getValue()); + $sheet->getCell('C2')->setValue($longstring . 'abcdef'); + self::assertSame($longstring . 'abcde', $sheet->getCell('C2')->getValue()); + $sheet->getCell('C3')->setValue('abcdef'); + $sheet->getCell('C4')->setValue('=C1 & C3'); + self::assertSame($longstring . 'abcde', $sheet->getCell('C4')->getCalculatedValue(), 'truncate cell concat with cell'); + $sheet->getCell('C5')->setValue('=C1 & "A"'); + self::assertSame($longstring . 'A', $sheet->getCell('C5')->getCalculatedValue(), 'okay cell concat with literal'); + $sheet->getCell('C6')->setValue('=C1 & "ABCDEF"'); + self::assertSame($longstring . 'ABCDE', $sheet->getCell('C6')->getCalculatedValue(), 'truncate cell concat with literal'); + $sheet->getCell('C7')->setValue('="ABCDEF" & C1'); + self::assertSame('ABCDEF' . str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 6), $sheet->getCell('C7')->getCalculatedValue(), 'truncate literal concat with cell'); + $sheet->getCell('C8')->setValue('="ABCDE" & C1'); + self::assertSame('ABCDE' . $longstring, $sheet->getCell('C8')->getCalculatedValue(), 'okay literal concat with cell'); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php index e2384460..ac1f15a1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php @@ -19,18 +19,23 @@ class TranslationTest extends TestCase */ private $returnDate; + /** @var string */ + private $locale; + protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); $this->returnDate = Functions::getReturnDateType(); Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); Functions::setReturnDateType($this->returnDate); + Settings::setLocale($this->locale); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php index f8f02f0e..3edd22c8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/XlfnFunctionsTest.php @@ -1,6 +1,6 @@ getActiveSheet(); $sheet->getCell('A1')->setValue($value); - self::assertEquals($valueBinded, $sheet->getCell('A1')->getValue()); + self::assertEqualsWithDelta($valueBinded, $sheet->getCell('A1')->getValue(), self::AVB_PRECISION); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php index ad7fc776..b14d63fb 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisGlowTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -107,7 +107,7 @@ class AxisGlowTest extends AbstractFunctional $yAxis = $chart->getChartAxisY(); $xAxis = $chart->getChartAxisX(); $yGlowSize = 10.0; - $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $yAxis->setGlowProperties($yGlowSize, 'FFFF00', 30, ChartColor::EXCEL_COLOR_TYPE_RGB); $expectedGlowColor = [ 'type' => 'srgbClr', 'value' => 'FFFF00', @@ -119,6 +119,7 @@ class AxisGlowTest extends AbstractFunctional $xAxis->setSoftEdges($softEdgesX); self::assertEquals($yGlowSize, $yAxis->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $yAxis->getGlowProperty('color')); + self::assertSame($expectedGlowColor['value'], $yAxis->getGlowProperty(['color', 'value'])); self::assertEquals($softEdgesY, $yAxis->getSoftEdgesSize()); self::assertEquals($softEdgesX, $xAxis->getSoftEdgesSize()); @@ -230,7 +231,7 @@ class AxisGlowTest extends AbstractFunctional ); $yAxis = $chart->getChartAxisX(); // deliberate $yGlowSize = 20.0; - $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, Properties::EXCEL_COLOR_TYPE_SCHEME); + $yAxis->setGlowProperties($yGlowSize, 'accent1', 20, ChartColor::EXCEL_COLOR_TYPE_SCHEME); $expectedGlowColor = [ 'type' => 'schemeClr', 'value' => 'accent1', diff --git a/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php new file mode 100644 index 00000000..0bfc2966 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/AxisPropertiesTest.php @@ -0,0 +1,234 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testAxisProperties(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $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_LINECHART, // 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]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $xAxis = new Axis(); + $xAxis->setFillParameters('FF0000', null, 'srgbClr'); + self::assertSame('FF0000', $xAxis->getFillProperty('value')); + self::assertSame('', $xAxis->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis->getFillProperty('type')); + + $xAxis->setAxisOptionsProperties( + Properties::AXIS_LABELS_HIGH, // axisLabels, + null, // $horizontalCrossesValue, + Properties::HORIZONTAL_CROSSES_MAXIMUM, //horizontalCrosses + Properties::ORIENTATION_REVERSED, //axisOrientation + Properties::TICK_MARK_INSIDE, //majorTmt + Properties::TICK_MARK_OUTSIDE, //minorTmt + '8', //minimum + '68', //maximum + '20', //majorUnit + '5', //minorUnit + '6', //textRotation + '0', //hidden + ); + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis->getAxisOptionsProperty('axis_labels')); + self::assertNull($xAxis->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis->getAxisOptionsProperty('orientation')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis->getAxisOptionsProperty('minor_tick_mark')); + self::assertSame('8', $xAxis->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis->getAxisOptionsProperty('hidden')); + + $yAxis = new Axis(); + $yAxis->setFillParameters('accent1', 30, 'schemeClr'); + self::assertSame('accent1', $yAxis->getFillProperty('value')); + self::assertSame('30', $yAxis->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis->getFillProperty('type')); + + // 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 + $xAxis, // xAxis + $yAxis, // yAxis + null, //majorGridlines, + null // minorGridlines + ); + $xAxis2 = $chart->getChartAxisX(); + self::assertSame('FF0000', $xAxis2->getFillProperty('value')); + self::assertSame('', $xAxis2->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis2->getFillProperty('type')); + + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis2->getAxisOptionsProperty('axis_labels')); + self::assertNull($xAxis2->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis2->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis2->getAxisOptionsProperty('orientation')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis2->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis2->getAxisOptionsProperty('minor_tick_mark')); + self::assertSame('8', $xAxis2->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis2->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis2->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis2->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis2->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis2->getAxisOptionsProperty('hidden')); + + $yAxis2 = $chart->getChartAxisY(); + self::assertSame('accent1', $yAxis2->getFillProperty('value')); + self::assertSame('30', $yAxis2->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis2->getFillProperty('type')); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $xAxis3 = $chart2->getChartAxisX(); + self::assertSame('FF0000', $xAxis3->getFillProperty('value')); + self::assertSame('', $xAxis3->getFillProperty('alpha')); + self::assertSame('srgbClr', $xAxis3->getFillProperty('type')); + + self::assertSame(Properties::AXIS_LABELS_HIGH, $xAxis3->getAxisOptionsProperty('axis_labels')); + self::assertSame(Properties::TICK_MARK_INSIDE, $xAxis3->getAxisOptionsProperty('major_tick_mark')); + self::assertSame(Properties::TICK_MARK_OUTSIDE, $xAxis3->getAxisOptionsProperty('minor_tick_mark')); + self::assertNull($xAxis3->getAxisOptionsProperty('horizontal_crosses_value')); + self::assertSame(Properties::HORIZONTAL_CROSSES_MAXIMUM, $xAxis3->getAxisOptionsProperty('horizontal_crosses')); + self::assertSame(Properties::ORIENTATION_REVERSED, $xAxis3->getAxisOptionsProperty('orientation')); + self::assertSame('8', $xAxis3->getAxisOptionsProperty('minimum')); + self::assertSame('68', $xAxis3->getAxisOptionsProperty('maximum')); + self::assertSame('20', $xAxis3->getAxisOptionsProperty('major_unit')); + self::assertSame('5', $xAxis3->getAxisOptionsProperty('minor_unit')); + self::assertSame('6', $xAxis3->getAxisOptionsProperty('textRotation')); + self::assertSame('0', $xAxis3->getAxisOptionsProperty('hidden')); + + $yAxis3 = $chart2->getChartAxisY(); + self::assertSame('accent1', $yAxis3->getFillProperty('value')); + self::assertSame('30', $yAxis3->getFillProperty('alpha')); + self::assertSame('schemeClr', $yAxis3->getFillProperty('type')); + + $xAxis3->setAxisOrientation(Properties::ORIENTATION_NORMAL); + self::assertSame(Properties::ORIENTATION_NORMAL, $xAxis3->getAxisOptionsProperty('orientation')); + $xAxis3->setAxisOptionsProperties( + Properties::AXIS_LABELS_HIGH, // axisLabels, + '5' // $horizontalCrossesValue, + ); + self::assertSame('5', $xAxis3->getAxisOptionsProperty('horizontal_crosses_value')); + + $yAxis3->setLineColorProperties('0000FF', null, 'srgbClr'); + self::assertSame('0000FF', $yAxis3->getLineProperty('value')); + self::assertNull($yAxis3->getLineProperty('alpha')); + self::assertSame('srgbClr', $yAxis3->getLineProperty('type')); + $yAxis3->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL); + self::assertFalse($yAxis3->getAxisIsNumericFormat()); + $yAxis3->setAxisNumberProperties(Properties::FORMAT_CODE_NUMBER); + self::assertTrue($yAxis3->getAxisIsNumericFormat()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php index d6f122ef..78deece4 100644 --- a/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/AxisShadowTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -113,7 +113,7 @@ class AxisShadowTest extends AbstractFunctional 'distance' => 3, 'rotWithShape' => 0, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -139,7 +139,7 @@ class AxisShadowTest extends AbstractFunctional 'ky' => null, ], 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'type' => ChartColor::EXCEL_COLOR_TYPE_RGB, 'value' => 'FF0000', 'alpha' => 20, ], diff --git a/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php b/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php new file mode 100644 index 00000000..431ad670 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/BarChartCustomColorsTest.php @@ -0,0 +1,163 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testBarchartColor(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + // Custom colors for dataSeries (gray, blue, red, orange) + $colors = [ + 'cccccc', + '*accent1', // use schemeClr, was '00abb8', + '/green', // use prstClr, was 'b8292f', + 'eb8500', + ]; + + // 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 + $dataSeriesLabels1 = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + 'Worksheet!$C$1', + null, + 1 + ), // 2011 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues1 = [ + 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 + // Custom Colors + $dataSeriesValues1Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4); + $dataSeriesValues1Element->setFillColor($colors); + $dataSeriesValues1 = [$dataSeriesValues1Element]; + + // Build the dataseries + $series1 = new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ); + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + $layout1->setShowPercent(true); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, [$series1]); + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // no Y-Axis for Pie Chart + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $dataSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $dataSeries2); + $plotValues = $dataSeries2[0]->getPlotValues(); + self::assertCount(1, $plotValues); + $fillColors = $plotValues[0]->getFillColor(); + self::assertSame($colors, $fillColors); + + $writer = new XlsxWriter($reloadedSpreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart2); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(4, substr_count($data, '')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php b/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php new file mode 100644 index 00000000..5c3622ad --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ChartMethodTest.php @@ -0,0 +1,143 @@ +getActiveSheet(); + $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_LINECHART, // 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]); + $title = new Title('Method vs Constructor test'); + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + $xAxis = new Axis(); + $yAxis = new Axis(); + $xAxisLabel = new Title('X-Axis label'); + $yAxisLabel = new Title('Y-Axis label'); + $chart1 = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + $xAxisLabel, // xAxisLabel + $yAxisLabel, // yAxisLabel + $xAxis, // xAxis + $yAxis // yAxis + ); + $chart2 = new Chart('xyz'); + $chart2 + ->setName('chart1') + ->setLegend($legend) + ->setPlotArea($plotArea) + ->setPlotVisibleOnly(true) + ->setDisplayBlanksAs(DataSeries::EMPTY_AS_GAP) + ->setChartAxisX($xAxis) + ->setChartAxisY($yAxis) + ->setXAxisLabel($xAxisLabel) + ->setYAxisLabel($yAxisLabel) + ->setTitle($title); + self::assertEquals($chart1, $chart2); + $spreadsheet->disconnectWorksheets(); + } + + public function testPositions(): void + { + $chart = new Chart('chart1'); + $chart->setTopLeftPosition('B3', 2, 4); + self::assertSame('B3', $chart->getTopLeftCell()); + self::assertEquals(['X' => 2, 'Y' => 4], $chart->getTopLeftOffset()); + self::assertEquals(2, $chart->getTopLeftXOffset()); + self::assertEquals(4, $chart->getTopLeftYOffset()); + $chart->setTopLeftCell('B5'); + self::assertSame('B5', $chart->getTopLeftCell()); + self::assertEquals(2, $chart->getTopLeftXOffset()); + self::assertEquals(4, $chart->getTopLeftYOffset()); + $chart->setTopLeftOffset(6, 8); + self::assertSame('B5', $chart->getTopLeftCell()); + self::assertEquals(6, $chart->getTopLeftXOffset()); + self::assertEquals(8, $chart->getTopLeftYOffset()); + + $chart->setbottomRightPosition('H9', 3, 5); + self::assertSame('H9', $chart->getBottomRightCell()); + self::assertEquals(['X' => 3, 'Y' => 5], $chart->getBottomRightOffset()); + self::assertEquals(3, $chart->getBottomRightXOffset()); + self::assertEquals(5, $chart->getBottomRightYOffset()); + $chart->setbottomRightCell('H11'); + self::assertSame('H11', $chart->getBottomRightCell()); + self::assertEquals(3, $chart->getBottomRightXOffset()); + self::assertEquals(5, $chart->getBottomRightYOffset()); + $chart->setbottomRightOffset(7, 9); + self::assertSame('H11', $chart->getBottomRightCell()); + self::assertEquals(7, $chart->getBottomRightXOffset()); + self::assertEquals(9, $chart->getBottomRightYOffset()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php index 268ee094..1f046af9 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32CatAxValAxTest.php @@ -23,6 +23,8 @@ class Charts32CatAxValAxTest extends TestCase /** @var string */ private $outputFileName = ''; + private const FORMAT_CODE_DATE_ISO8601_SLASH = 'yyyy/mm/dd'; // not automatically treated as numeric + protected function tearDown(): void { if ($this->outputFileName !== '') { @@ -48,7 +50,7 @@ class Charts32CatAxValAxTest extends TestCase ['=DATEVALUE("2021-01-10")', 30.2, 32.2, 0.2], ] ); - $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(Properties::FORMAT_CODE_DATE_ISO8601); + $worksheet->getStyle('A2:A5')->getNumberFormat()->setFormatCode(self::FORMAT_CODE_DATE_ISO8601_SLASH); $worksheet->getColumnDimension('A')->setAutoSize(true); $worksheet->setSelectedCells('A1'); @@ -91,9 +93,9 @@ class Charts32CatAxValAxTest extends TestCase $xAxis = new Axis(); //$xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE ); if (is_bool($numeric)) { - $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601, $numeric); + $xAxis->setAxisNumberProperties(self::FORMAT_CODE_DATE_ISO8601_SLASH, $numeric); } else { - $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_DATE_ISO8601); + $xAxis->setAxisNumberProperties(self::FORMAT_CODE_DATE_ISO8601_SLASH); } // Build the dataseries diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php index 71cda504..f0c84bf0 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ColoredAxisLabelTest.php @@ -46,6 +46,7 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertNotNull($chart); $xAxisLabel = $chart->getXAxisLabel(); + self::assertNotNull($xAxisLabel); $captionArray = $xAxisLabel->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -58,9 +59,13 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('00B050', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('00B050', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $yAxisLabel = $chart->getYAxisLabel(); + self::assertNotNull($yAxisLabel); $captionArray = $yAxisLabel->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -73,7 +78,10 @@ class Charts32ColoredAxisLabelTest extends AbstractFunctional self::assertInstanceOf(Run::class, $run); $font = $run->getFont(); self::assertInstanceOf(Font::class, $font); - self::assertSame('FF0000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('FF0000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php new file mode 100644 index 00000000..26ef34ee --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Charts32DsvGlowTest.php @@ -0,0 +1,77 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testLine4(): void + { + $file = self::DIRECTORY . '32readwriteLineChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $dataSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $dataSeriesArray); + $dataSeries = $dataSeriesArray[0]; + $dataSeriesValuesArray = $dataSeries->getPlotValues(); + self::assertCount(3, $dataSeriesValuesArray); + $dataSeriesValues = $dataSeriesValuesArray[1]; + self::assertEquals(5, $dataSeriesValues->getGlowSize()); + self::assertSame('schemeClr', $dataSeriesValues->getGlowProperty(['color', 'type'])); + self::assertSame('accent2', $dataSeriesValues->getGlowProperty(['color', 'value'])); + self::assertSame(60, $dataSeriesValues->getGlowProperty(['color', 'alpha'])); + + $yAxis = $chart->getChartAxisY(); + $majorGridlines = $yAxis->getMajorGridlines(); + self::assertNotNull($majorGridlines); + self::assertSame('triangle', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertSame('triangle', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $minorGridlines = $yAxis->getMinorGridlines(); + self::assertNotNull($minorGridlines); + self::assertSame('sysDot', $minorGridlines->getLineStyleProperty('dash')); + self::assertSame('FFC000', $minorGridlines->getLineColor()->getValue()); + + $xAxis = $chart->getChartAxisX(); + $majorGridlines = $xAxis->getMajorGridlines(); + $minorGridlines = $xAxis->getMinorGridlines(); + self::assertNotNull($majorGridlines); + self::assertSame('7030A0', $majorGridlines->getLineColor()->getValue()); + self::assertNotNull($minorGridlines); + self::assertFalse($minorGridlines->getLineColor()->isUsable()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php new file mode 100644 index 00000000..b6490776 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Charts32DsvLabelsTest.php @@ -0,0 +1,74 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testBar4(): void + { + $file = self::DIRECTORY . '32readwriteBarChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $dataSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $dataSeriesArray); + $dataSeries = $dataSeriesArray[0]; + $dataSeriesValuesArray = $dataSeries->getPlotValues(); + self::assertCount(1, $dataSeriesValuesArray); + $dataSeriesValues = $dataSeriesValuesArray[0]; + $layout = $dataSeriesValues->getLabelLayout(); + self::assertNotNull($layout); + self::assertTrue($layout->getShowVal()); + $fillColor = $layout->getLabelFillColor(); + self::assertNotNull($fillColor); + self::assertSame('schemeClr', $fillColor->getType()); + self::assertSame('accent1', $fillColor->getValue()); + $borderColor = $layout->getLabelBorderColor(); + self::assertNotNull($borderColor); + self::assertSame('srgbClr', $borderColor->getType()); + self::assertSame('FFC000', $borderColor->getValue()); + $fontColor = $layout->getLabelFontColor(); + self::assertNotNull($fontColor); + self::assertSame('srgbClr', $fontColor->getType()); + self::assertSame('FFFF00', $fontColor->getValue()); + self::assertEquals( + [15, 73, 61, 32], + $dataSeriesValues->getDataValues() + ); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php index d79cba6f..fcde7ae3 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32ScatterTest.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; @@ -45,6 +47,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -65,9 +68,13 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -75,19 +82,22 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(7, $values->getPointSize()); - self::assertSame('FFFF00', $values->getFillColor()); + // Had been testing for Fill Color, but we actually + // meant to test for marker color, which is now distinct. + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } @@ -114,6 +124,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -135,7 +146,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $run = $elements[1]; self::assertInstanceOf(Run::class, $run); @@ -149,7 +163,10 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('single', $font->getUnderline()); - self::assertSame('00B0F0', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('00B0F0', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $run = $elements[2]; self::assertInstanceOf(Run::class, $run); @@ -163,9 +180,13 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -173,19 +194,22 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(7, $values->getPointSize()); - self::assertSame('FFFF00', $values->getFillColor()); + // Had been testing for Fill Color, but we actually + // meant to test for marker color, which is now distinct. + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); $reloadedSpreadsheet->disconnectWorksheets(); } @@ -212,6 +236,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -232,9 +257,13 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -242,17 +271,19 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertTrue($values->getScatterLines()); - self::assertSame(12700, $values->getLineWidth()); + // the default value of 1 point is no longer written out + // when not explicitly specified. + self::assertNull($values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertTrue($values->getScatterLines()); - self::assertSame(12700, $values->getLineWidth()); + self::assertNull($values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertTrue($values->getScatterLines()); - self::assertSame(12700, $values->getLineWidth()); + self::assertNull($values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); @@ -281,6 +312,7 @@ class Charts32ScatterTest extends AbstractFunctional $chart = $charts[0]; self::assertNotNull($chart); $title = $chart->getTitle(); + self::assertNotNull($title); $captionArray = $title->getCaption(); self::assertIsArray($captionArray); self::assertCount(1, $captionArray); @@ -303,10 +335,14 @@ class Charts32ScatterTest extends AbstractFunctional self::assertFalse($font->getSubscript()); self::assertFalse($font->getStrikethrough()); self::assertSame('none', $font->getUnderline()); - self::assertSame('000000', $font->getColor()->getRGB()); + $chartColor = $font->getChartColor(); + self::assertNotNull($chartColor); + self::assertSame('000000', $chartColor->getValue()); + self::assertSame('srgbClr', $chartColor->getType()); } $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; @@ -314,19 +350,185 @@ class Charts32ScatterTest extends AbstractFunctional self::assertCount(3, $plotValues); $values = $plotValues[0]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[1]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(3, $values->getPointSize()); self::assertSame('', $values->getFillColor()); $values = $plotValues[2]; self::assertFalse($values->getScatterLines()); - self::assertSame(28575, $values->getLineWidth()); + self::assertSame(28575 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); self::assertSame(7, $values->getPointSize()); - self::assertSame('FFFF00', $values->getFillColor()); + // Had been testing for Fill Color, but we actually + // meant to test for marker color, which is now distinct. + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter8(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart8.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $xAxis = $chart->getChartAxisX(); + self::assertEquals(45, $xAxis->getAxisOptionsProperty('textRotation')); + + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeries = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeries); + $dataSeries = $plotSeries[0]; + $plotValues = $dataSeries->getPlotValues(); + self::assertCount(3, $plotValues); + $values = $plotValues[0]; + self::assertSame(31750 / Properties::POINTS_WIDTH_MULTIPLIER, $values->getLineWidth()); + + self::assertSame('sq', $values->getLineStyleProperty('cap')); + self::assertSame('tri', $values->getLineStyleProperty('compound')); + self::assertSame('sysDash', $values->getLineStyleProperty('dash')); + self::assertSame('miter', $values->getLineStyleProperty('join')); + self::assertSame('arrow', $values->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertSame('med', $values->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertSame('sm', $values->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertSame('triangle', $values->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertSame('med', $values->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertSame('lg', $values->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertSame('accent1', $values->getLineColorProperty('value')); + self::assertSame('schemeClr', $values->getLineColorProperty('type')); + self::assertSame(40, $values->getLineColorProperty('alpha')); + self::assertSame('', $values->getFillColor()); + + self::assertSame(7, $values->getPointSize()); + self::assertSame('diamond', $values->getPointMarker()); + self::assertSame('0070C0', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); + self::assertSame('002060', $values->getMarkerBorderColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerBorderColor()->getType()); + + $values = $plotValues[1]; + self::assertSame(7, $values->getPointSize()); + self::assertSame('square', $values->getPointMarker()); + self::assertSame('accent6', $values->getMarkerFillColor()->getValue()); + self::assertSame('schemeClr', $values->getMarkerFillColor()->getType()); + self::assertSame(3, $values->getMarkerFillColor()->getAlpha()); + self::assertSame('0FF000', $values->getMarkerBorderColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerBorderColor()->getType()); + self::assertNull($values->getMarkerBorderColor()->getAlpha()); + + $values = $plotValues[2]; + self::assertSame(7, $values->getPointSize()); + self::assertSame('triangle', $values->getPointMarker()); + self::assertSame('FFFF00', $values->getMarkerFillColor()->getValue()); + self::assertSame('srgbClr', $values->getMarkerFillColor()->getType()); + self::assertNull($values->getMarkerFillColor()->getAlpha()); + self::assertSame('accent4', $values->getMarkerBorderColor()->getValue()); + self::assertSame('schemeClr', $values->getMarkerBorderColor()->getType()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter9(): void + { + // gradient testing + $file = self::DIRECTORY . '32readwriteScatterChart9.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertFalse($plotArea->getNoFill()); + self::assertEquals(315.0, $plotArea->getGradientFillAngle()); + $stops = $plotArea->getGradientFillStops(); + self::assertCount(3, $stops); + self::assertEquals(0.43808, $stops[0][0]); + self::assertEquals(0, $stops[1][0]); + self::assertEquals(0.91, $stops[2][0]); + $color = $stops[0][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('CDDBEC', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(20, $color->getBrightness()); + $color = $stops[1][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('FFC000', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertNull($color->getBrightness()); + $color = $stops[2][1]; + self::assertInstanceOf(ChartColor::class, $color); + self::assertSame('srgbClr', $color->getType()); + self::assertSame('00B050', $color->getValue()); + self::assertNull($color->getAlpha()); + self::assertSame(4, $color->getBrightness()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testScatter10(): void + { + // nofill for Chart and PlotArea, hidden Axis + $file = self::DIRECTORY . '32readwriteScatterChart10.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getNoFill()); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertTrue($plotArea->getNoFill()); $reloadedSpreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php index a193ca7d..4cc62360 100644 --- a/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/Charts32XmlTest.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; -use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; use PHPUnit\Framework\TestCase; @@ -13,17 +12,6 @@ class Charts32XmlTest extends TestCase // These tests can only be performed by examining xml. private const DIRECTORY = 'samples' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR; - /** @var string */ - private $outputFileName = ''; - - protected function tearDown(): void - { - if ($this->outputFileName !== '') { - unlink($this->outputFileName); - $this->outputFileName = ''; - } - } - /** * @dataProvider providerScatterCharts */ @@ -33,25 +21,21 @@ class Charts32XmlTest extends TestCase $reader = new XlsxReader(); $reader->setIncludeCharts(true); $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart2.xml'; - $data = file_get_contents($file); - // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } else { - self::assertSame(1, substr_count($data, '')); - self::assertSame($expectedCount, substr_count($data, '')); - } + self::assertSame(1, substr_count($data, '')); + self::assertSame($expectedCount, substr_count($data, '')); } public function providerScatterCharts(): array @@ -69,23 +53,20 @@ class Charts32XmlTest extends TestCase $reader = new XlsxReader(); $reader->setIncludeCharts(true); $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart1.xml'; - $data = file_get_contents($file); // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } else { - self::assertSame(0, substr_count($data, '')); - } + self::assertSame(0, substr_count($data, '')); } /** @@ -103,12 +84,16 @@ class Charts32XmlTest extends TestCase $chart = $charts[0]; self::assertNotNull($chart); $xAxis = $chart->getChartAxisX(); + $yAxis = $chart->getChartAxisY(); self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); if (is_bool($numeric)) { $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, true); } - $yAxis = $chart->getChartAxisY(); + self::assertSame('valAx', $yAxis->getAxisType()); + self::assertSame('valAx', $xAxis->getAxisType()); self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + $xAxis->setAxisType(''); + $yAxis->setAxisType(''); if (is_bool($numeric)) { $xAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); $yAxis->setAxisNumberProperties(Properties::FORMAT_CODE_GENERAL, $numeric); @@ -116,18 +101,11 @@ class Charts32XmlTest extends TestCase $writer = new XlsxWriter($spreadsheet); $writer->setIncludeCharts(true); - $this->outputFileName = File::temporaryFilename(); - $writer->save($this->outputFileName); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); $spreadsheet->disconnectWorksheets(); - $file = 'zip://'; - $file .= $this->outputFileName; - $file .= '#xl/charts/chart2.xml'; - $data = file_get_contents($file); - // confirm that file contains expected tags - if ($data === false) { - self::fail('Unable to read file'); - } elseif ($numeric === true) { + if ($numeric === true) { self::assertSame(0, substr_count($data, '')); self::assertSame(2, substr_count($data, '')); } else { @@ -144,4 +122,100 @@ class Charts32XmlTest extends TestCase [null], ]; } + + public function testCatAxValAxFromRead(): void + { + $file = self::DIRECTORY . '32readwriteScatterChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + $xAxis = $chart->getChartAxisX(); + $yAxis = $chart->getChartAxisY(); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $xAxis->getAxisNumberFormat()); + self::assertSame('valAx', $yAxis->getAxisType()); + self::assertSame('valAx', $xAxis->getAxisType()); + self::assertSame(Properties::FORMAT_CODE_GENERAL, $yAxis->getAxisNumberFormat()); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame(0, substr_count($data, '')); + self::assertSame(2, substr_count($data, '')); + } + + public function testAreaPrstClr(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart4.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + } + + public function testDateAx(): void + { + $file = self::DIRECTORY . '32readwriteLineDateAxisChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + $charts = $sheet->getChartCollection(); + self::assertCount(2, $charts); + $chart = $charts[1]; + self::assertNotNull($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + $spreadsheet->disconnectWorksheets(); + + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + self::assertSame( + 1, + substr_count( + $data, + '' + ) + ); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php index 7fcc2e90..e6f574fd 100644 --- a/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ChartsOpenpyxlTest.php @@ -27,6 +27,7 @@ class ChartsOpenpyxlTest extends TestCase self::assertTrue($chart->getOneCellAnchor()); $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); $plotSeries = $plotArea->getPlotGroup(); self::assertCount(1, $plotSeries); $dataSeries = $plotSeries[0]; diff --git a/tests/PhpSpreadsheetTests/Chart/ColorTest.php b/tests/PhpSpreadsheetTests/Chart/ColorTest.php new file mode 100644 index 00000000..e8326e52 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/ColorTest.php @@ -0,0 +1,32 @@ +getType()); + self::assertSame('800000', $color->getValue()); + $color->setColorProperties('*accent1'); + self::assertSame('schemeClr', $color->getType()); + self::assertSame('accent1', $color->getValue()); + $color->setColorProperties('/red'); + self::assertSame('prstClr', $color->getType()); + self::assertSame('red', $color->getValue()); + } + + public function testDataSeriesValues(): void + { + $dsv = new DataSeriesValues(); + $dsv->setFillColor([new ChartColor(), new ChartColor()]); + self::assertSame(['', ''], $dsv->getFillColor()); + $dsv->setFillColor('cccccc'); + self::assertSame('cccccc', $dsv->getFillColor()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php b/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php new file mode 100644 index 00000000..bba1c1f1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/DataSeriesValues2Test.php @@ -0,0 +1,181 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testDataSeriesValues(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $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( + null, // plotType + null, // plotGrouping + range(0, count($dataSeriesValues) - 1), // plotOrder + $dataSeriesLabels, // plotLabel + $xAxisTickValues, // plotCategory + $dataSeriesValues // plotValues + ); + self::assertEmpty($series->getPlotType()); + self::assertEmpty($series->getPlotGrouping()); + self::assertFalse($series->getSmoothLine()); + $series->setPlotType(DataSeries::TYPE_AREACHART); + $series->setPlotGrouping(DataSeries::GROUPING_PERCENT_STACKED); + $series->setSmoothLine(true); + self::assertSame(DataSeries::TYPE_AREACHART, $series->getPlotType()); + self::assertSame(DataSeries::GROUPING_PERCENT_STACKED, $series->getPlotGrouping()); + self::assertTrue($series->getSmoothLine()); + + // Set the series in the plot area + $plotArea = new PlotArea(null, [$series]); + // Set the chart legend + $legend = 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('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + self::assertSame(1, $plotArea->getPlotGroupCount()); + $plotValues = $plotArea->getPlotGroup()[0]->getPlotValues(); + self::assertCount(3, $plotValues); + self::assertSame([], $plotValues[1]->getDataValues()); + self::assertNull($plotValues[1]->getDataValue()); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $plotGroup2 = $plotArea2->getPlotGroup()[0]; + self::assertNotNull($plotGroup2); + $plotValues2 = $plotGroup2->getPlotValues(); + self::assertCount(3, $plotValues2); + self::assertSame([15.0, 73.0, 61.0, 32.0], $plotValues2[1]->getDataValues()); + self::assertSame([15.0, 73.0, 61.0, 32.0], $plotValues2[1]->getDataValue()); + $labels2 = $plotGroup2->getPlotLabels(); + self::assertCount(3, $labels2); + self::assertEquals(2010, $labels2[0]->getDataValue()); + $dataSeries = $plotArea2->getPlotGroup()[0]; + self::assertFalse($dataSeries->getPlotValuesByIndex(99)); + self::assertNotFalse($dataSeries->getPlotValuesByIndex(0)); + self::assertEquals([12, 56, 52, 30], $dataSeries->getPlotValuesByIndex(0)->getDataValues()); + self::assertSame(DataSeries::TYPE_AREACHART, $dataSeries->getPlotType()); + self::assertSame(DataSeries::GROUPING_PERCENT_STACKED, $dataSeries->getPlotGrouping()); + // SmoothLine written out for DataSeries only for LineChart. + // Original test was wrong - used $chart rather than $chart2 + // to retrieve data which was read in. + //self::assertTrue($dataSeries->getSmoothLine()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testSomeProperties(): void + { + $dataSeriesValues = new DataSeriesValues(); + self::assertNull($dataSeriesValues->getDataSource()); + self::assertEmpty($dataSeriesValues->getPointMarker()); + self::assertSame(3, $dataSeriesValues->getPointSize()); + $dataSeriesValues->setDataSource('Worksheet!$B$1') + ->setPointMarker('square') + ->setPointSize(6); + self::assertSame('Worksheet!$B$1', $dataSeriesValues->getDataSource()); + self::assertSame('square', $dataSeriesValues->getPointMarker()); + self::assertSame(6, $dataSeriesValues->getPointSize()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php b/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php index c34ca697..47c2fb89 100644 --- a/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php +++ b/tests/PhpSpreadsheetTests/Chart/DataSeriesValuesTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Exception; use PHPUnit\Framework\TestCase; @@ -51,13 +52,14 @@ class DataSeriesValuesTest extends TestCase public function testGetLineWidth(): void { $testInstance = new DataSeriesValues(); - self::assertEquals(12700, $testInstance->getLineWidth(), 'should have default'); + // default has changed to null from 1 point (12700) + self::assertNull($testInstance->getLineWidth(), 'should have default'); - $testInstance->setLineWidth(40000); - self::assertEquals(40000, $testInstance->getLineWidth()); + $testInstance->setLineWidth(40000 / Properties::POINTS_WIDTH_MULTIPLIER); + self::assertEquals(40000 / Properties::POINTS_WIDTH_MULTIPLIER, $testInstance->getLineWidth()); $testInstance->setLineWidth(1); - self::assertEquals(12700, $testInstance->getLineWidth(), 'should enforce minimum width'); + self::assertEquals(12700 / Properties::POINTS_WIDTH_MULTIPLIER, $testInstance->getLineWidth(), 'should enforce minimum width'); } public function testFillColorCorrectInput(): void diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php new file mode 100644 index 00000000..e13a5840 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesLineStyleTest.php @@ -0,0 +1,416 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testLineStyles(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $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_LINECHART, // 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]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $majorGridlines = new GridLines(); + $width = 2; + $compound = Properties::LINE_STYLE_COMPOUND_THICKTHIN; + $dash = Properties::LINE_STYLE_DASH_ROUND_DOT; + $cap = Properties::LINE_STYLE_CAP_ROUND; + $join = Properties::LINE_STYLE_JOIN_MITER; + $headArrowType = Properties::LINE_STYLE_ARROW_TYPE_DIAMOND; + $headArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_2; + $endArrowType = Properties::LINE_STYLE_ARROW_TYPE_OVAL; + $endArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_3; + $majorGridlines->setLineStyleProperties( + $width, + $compound, + $dash, + $cap, + $join, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize + ); + $minorGridlines = new GridLines(); + $minorGridlines->setLineColorProperties('00FF00', 30, 'srgbClr'); + + self::assertEquals($width, $majorGridlines->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleArrowWidth('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowLength('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowParameters('end', 'len')); + + self::assertSame('00FF00', $minorGridlines->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines->getLineColorProperty('type')); + + // Create the chart + $yAxis = new Axis(); + $yAxis->setMajorGridlines($majorGridlines); + $yAxis->setMinorGridlines($minorGridlines); + $chart = new Chart( + 'chart1', // name + $title, // title + $legend, // legend + $plotArea, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + $yAxisLabel, // yAxisLabel + null, // xAxis + $yAxis // yAxis + ); + $yAxis2 = $chart->getChartAxisY(); + $majorGridlines2 = $yAxis2->getMajorGridlines(); + self::assertNotNull($majorGridlines2); + self::assertEquals($width, $majorGridlines2->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines2->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines2->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines2->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines2->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines2 = $yAxis2->getMinorGridlines(); + self::assertNotNull($minorGridlines2); + self::assertSame('00FF00', $minorGridlines2->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines2->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines2->getLineColorProperty('type')); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + self::assertSame('A7', $chart2->getTopLeftCell()); + self::assertSame('H20', $chart2->getBottomRightCell()); + self::assertSame($sheet, $chart2->getWorksheet()); + $yAxis3 = $chart2->getChartAxisY(); + $majorGridlines3 = $yAxis3->getMajorGridlines(); + self::assertNotNull($majorGridlines3); + self::assertEquals($width, $majorGridlines3->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines3->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines3->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines3->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines3->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($endArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines3 = $yAxis3->getMinorGridlines(); + self::assertNotNull($minorGridlines3); + self::assertSame('00FF00', $minorGridlines3->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines3->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines3->getLineColorProperty('type')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testLineStylesDeprecated(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $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_LINECHART, // 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]); + // Set the chart legend + $legend = new ChartLegend(ChartLegend::POSITION_TOPRIGHT, null, false); + + $title = new Title('Test %age-Stacked Area Chart'); + $yAxisLabel = new Title('Value ($k)'); + $majorGridlines = new GridLines(); + $width = 2; + $compound = Properties::LINE_STYLE_COMPOUND_THICKTHIN; + $dash = Properties::LINE_STYLE_DASH_ROUND_DOT; + $cap = Properties::LINE_STYLE_CAP_ROUND; + $join = Properties::LINE_STYLE_JOIN_MITER; + $headArrowType = Properties::LINE_STYLE_ARROW_TYPE_DIAMOND; + $headArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_2; + $endArrowType = Properties::LINE_STYLE_ARROW_TYPE_OVAL; + $endArrowSize = (string) Properties::LINE_STYLE_ARROW_SIZE_3; + $majorGridlines->setLineStyleProperties( + $width, + $compound, + $dash, + $cap, + $join, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize + ); + $minorGridlines = new GridLines(); + $minorGridlines->setLineColorProperties('00FF00', 30, 'srgbClr'); + + self::assertEquals($width, $majorGridlines->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'len'])); + self::assertEquals('sm', $majorGridlines->getLineStyleArrowWidth('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowLength('end')); + self::assertEquals('lg', $majorGridlines->getLineStyleArrowParameters('end', 'len')); + + self::assertSame('00FF00', $minorGridlines->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines->getLineColorProperty('type')); + + // 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 + null, // xAxis + null, // yAxis + $majorGridlines, + $minorGridlines // minorGridlines + ); + $majorGridlines2 = $chart->getMajorGridlines(); + self::assertNotNull($majorGridlines2); + self::assertEquals($width, $majorGridlines2->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines2->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines2->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines2->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines2->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($headArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'size'])); + self::assertEquals($endArrowType, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals($endArrowSize, $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'size'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines2->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines2->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines2 = $chart->getMinorGridlines(); + self::assertNotNull($minorGridlines2); + self::assertSame('00FF00', $minorGridlines2->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines2->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines2->getLineColorProperty('type')); + + // Set the position where the chart should appear in the worksheet + $chart->setTopLeftPosition('A7'); + $chart->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $majorGridlines3 = $chart2->getMajorGridlines(); + self::assertNotNull($majorGridlines3); + self::assertEquals($width, $majorGridlines3->getLineStyleProperty('width')); + self::assertEquals($compound, $majorGridlines3->getLineStyleProperty('compound')); + self::assertEquals($dash, $majorGridlines3->getLineStyleProperty('dash')); + self::assertEquals($cap, $majorGridlines3->getLineStyleProperty('cap')); + self::assertEquals($join, $majorGridlines3->getLineStyleProperty('join')); + self::assertEquals($headArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals($endArrowType, $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'type'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'w'])); + self::assertEquals('med', $majorGridlines3->getLineStyleProperty(['arrow', 'head', 'len'])); + self::assertEquals('sm', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'w'])); + self::assertEquals('lg', $majorGridlines3->getLineStyleProperty(['arrow', 'end', 'len'])); + + $minorGridlines3 = $chart2->getMinorGridlines(); + self::assertNotNull($minorGridlines3); + self::assertSame('00FF00', $minorGridlines3->getLineColorProperty('value')); + self::assertSame(30, $minorGridlines3->getLineColorProperty('alpha')); + self::assertSame('srgbClr', $minorGridlines3->getLineColorProperty('type')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php index 04ca7c31..e1215441 100644 --- a/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php +++ b/tests/PhpSpreadsheetTests/Chart/GridlinesShadowGlowTest.php @@ -2,13 +2,14 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\Axis; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -93,9 +94,11 @@ class GridlinesShadowGlowTest extends AbstractFunctional $title = new Title('Test %age-Stacked Area Chart'); $yAxisLabel = new Title('Value ($k)'); + $yAxis = new Axis(); $majorGridlines = new GridLines(); + $yAxis->setMajorGridlines($majorGridlines); $majorGlowSize = 10.0; - $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, Properties::EXCEL_COLOR_TYPE_ARGB); + $majorGridlines->setGlowProperties($majorGlowSize, 'FFFF00', 30, ChartColor::EXCEL_COLOR_TYPE_RGB); $softEdgeSize = 2.5; $majorGridlines->setSoftEdges($softEdgeSize); $expectedGlowColor = [ @@ -110,6 +113,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional self::assertEquals($softEdgeSize, $majorGridlines->getSoftEdgesSize()); $minorGridlines = new GridLines(); + $yAxis->setMinorGridlines($minorGridlines); $expectedShadow = [ 'effect' => 'outerShdw', 'algn' => 'tl', @@ -118,7 +122,7 @@ class GridlinesShadowGlowTest extends AbstractFunctional 'distance' => 3, 'rotWithShape' => 0, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -129,6 +133,11 @@ class GridlinesShadowGlowTest extends AbstractFunctional foreach ($expectedShadow as $key => $value) { self::assertEquals($value, $minorGridlines->getShadowProperty($key), $key); } + $testShadow2 = $minorGridlines->getShadowArray(); + self::assertNull($testShadow2['presets']); + self::assertEquals(['sx' => null, 'sy' => null, 'kx' => null, 'ky' => null], $testShadow2['size']); + unset($testShadow2['presets'], $testShadow2['size']); + self::assertEquals($expectedShadow, $testShadow2); // Create the chart $chart = new Chart( @@ -141,15 +150,16 @@ class GridlinesShadowGlowTest extends AbstractFunctional null, // xAxisLabel $yAxisLabel, // yAxisLabel null, // xAxis - null, // yAxis - $majorGridlines, - $minorGridlines + $yAxis // yAxis ); - $majorGridlines2 = $chart->getMajorGridlines(); + $yAxis2 = $chart->getChartAxisY(); + $majorGridlines2 = $yAxis2->getMajorGridlines(); + self::assertNotNull($majorGridlines2); self::assertEquals($majorGlowSize, $majorGridlines2->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $majorGridlines2->getGlowProperty('color')); self::assertEquals($softEdgeSize, $majorGridlines2->getSoftEdgesSize()); - $minorGridlines2 = $chart->getMinorGridlines(); + $minorGridlines2 = $yAxis2->getMinorGridlines(); + self::assertNotNull($minorGridlines2); foreach ($expectedShadow as $key => $value) { self::assertEquals($value, $minorGridlines2->getShadowProperty($key), $key); } @@ -173,11 +183,14 @@ class GridlinesShadowGlowTest extends AbstractFunctional self::assertCount(1, $charts2); $chart2 = $charts2[0]; self::assertNotNull($chart2); - $majorGridlines3 = $chart2->getMajorGridlines(); + $yAxis3 = $chart2->getChartAxisY(); + $majorGridlines3 = $yAxis3->getMajorGridlines(); + self::assertNotNull($majorGridlines3); self::assertEquals($majorGlowSize, $majorGridlines3->getGlowProperty('size')); self::assertEquals($expectedGlowColor, $majorGridlines3->getGlowProperty('color')); self::assertEquals($softEdgeSize, $majorGridlines3->getSoftEdgesSize()); - $minorGridlines3 = $chart->getMinorGridlines(); + $minorGridlines3 = $yAxis3->getMinorGridlines(); + self::assertNotNull($minorGridlines3); foreach ($expectedShadow as $key => $value) { self::assertEquals($value, $minorGridlines3->getShadowProperty($key), $key); } diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2077Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2077Test.php new file mode 100644 index 00000000..2d1688c6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2077Test.php @@ -0,0 +1,108 @@ +getActiveSheet(); + $worksheet->fromArray( + [ + ['', '2010', '2011', '2012'], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 60], + ] + ); + + // Set the Labels for each data series we want to plot + $dataSeriesLabels1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$B$1', null, 1), // 2011 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$C$1', null, 1), // 2012 + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, 'Worksheet!$D$1', null, 1), // 2013 + ]; + + // Set the X-Axis Labels + $xAxisTickValues1 = [ + 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 + // TODO I think the third parameter can be set,but I didn't succeed + $dataSeriesValues1 = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$B$2:$B$5', null, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', NumberFormat::FORMAT_NUMBER_00, 4), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$D$2:$D$5', NumberFormat::FORMAT_PERCENTAGE_00, 4), + ]; + + // Build the dataseries + $series1 = [ + new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ), + ]; + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + // Set the layout to show percentage with 2 decimal points + $layout1->setShowPercent(true); + $layout1->setNumFmtCode(NumberFormat::FORMAT_PERCENTAGE_00); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, $series1); + + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + $yAxisLabel = new Title('Value ($k)'); + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + 'gap', // displayBlanksAs + null, // xAxisLabel + $yAxisLabel + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart1); + self::assertStringContainsString('', $data); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php new file mode 100644 index 00000000..e1c30770 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2506Test.php @@ -0,0 +1,73 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testDataSeriesValues(): void + { + $reader = new XlsxReader(); + $this->readCharts($reader); + $spreadsheet = $reader->load(self::DIRECTORY . 'issue.2506.xlsx'); + $worksheet = $spreadsheet->getActiveSheet(); + $charts = $worksheet->getChartCollection(); + self::assertCount(4, $charts); + $originalChart1 = $charts[0]; + self::assertNotNull($originalChart1); + $originalPlotArea1 = $originalChart1->getPlotArea(); + self::assertNotNull($originalPlotArea1); + $originalPlotSeries1 = $originalPlotArea1->getPlotGroup(); + self::assertCount(1, $originalPlotSeries1); + self::assertSame('0', $originalPlotSeries1[0]->getPlotStyle()); + $originalChart2 = $charts[1]; + self::assertNotNull($originalChart2); + $originalPlotArea2 = $originalChart2->getPlotArea(); + self::assertNotNull($originalPlotArea2); + $originalPlotSeries2 = $originalPlotArea2->getPlotGroup(); + self::assertCount(1, $originalPlotSeries2); + self::assertSame('5', $originalPlotSeries2[0]->getPlotStyle()); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(4, $charts2); + $chart1 = $charts[0]; + self::assertNotNull($chart1); + $plotArea1 = $chart1->getPlotArea(); + self::assertNotNull($plotArea1); + $plotSeries1 = $plotArea1->getPlotGroup(); + self::assertCount(1, $plotSeries1); + self::assertSame('0', $plotSeries1[0]->getPlotStyle()); + $chart2 = $charts[1]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $plotSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $plotSeries2); + self::assertSame('5', $plotSeries2[0]->getPlotStyle()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php new file mode 100644 index 00000000..960eb04e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2931Test.php @@ -0,0 +1,111 @@ +getActiveSheet(); + + $dataSeriesLabels = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['5-6']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['6-7']), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, null, null, 1, ['7-8']), + ]; + + $xAxisTickValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [1, 2, 3, 4, 5, 6, 7, 8, 9]), + ]; + + $dataSeriesValues = [ + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6, 6, 6, 5.9, 6, 6]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 6.5, 7, 7, 7, 7, 7]), + new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, null, null, 9, [6, 6, 6, 7, 8, 8, 8, 8, 7.9]), + ]; + + $series = new DataSeries( + DataSeries::TYPE_SURFACECHART, + DataSeries::GROUPING_STANDARD, // grouping should not be written for surface chart + range(0, count($dataSeriesValues) - 1), + $dataSeriesLabels, + $xAxisTickValues, + $dataSeriesValues, + null, // plotDirection + false, // smooth line + DataSeries::STYLE_LINEMARKER // plotStyle + ); + + $plotArea = new PlotArea(null, [$series]); + $legend = new ChartLegend(ChartLegend::POSITION_BOTTOM, null, false); + + $title = new Title('График распредления температур в пределах кр'); + + $chart = new Chart( + 'chart2', + $title, + $legend, + $plotArea, + true, + DataSeries::EMPTY_AS_GAP, + ); + + $chart->setTopLeftPosition('$A$1'); + $chart->setBottomRightPosition('$P$20'); + + $sheet->addChart($chart); + + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writer = new XlsxWriter($spreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + + // rotX etc. should be generated for surfaceChart 2D + // even when unspecified. + $expectedXml2D = [ + '', + ]; + $expectedXml3D = [ + '', + ]; + $expectedXmlNoX = [ + 'c:grouping', + ]; + + // confirm that file contains expected tags + foreach ($expectedXml2D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $series->setPlotType(DataSeries::TYPE_SURFACECHART_3D); + $plotArea = new PlotArea(null, [$series]); + $chart->setPlotArea($plotArea); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart); + // confirm that file contains expected tags + foreach ($expectedXml3D as $expected) { + self::assertSame(1, substr_count($data, $expected), $expected); + } + foreach ($expectedXmlNoX as $expected) { + self::assertSame(0, substr_count($data, $expected), $expected); + } + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/Issue2965Test.php b/tests/PhpSpreadsheetTests/Chart/Issue2965Test.php new file mode 100644 index 00000000..8294d39b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/Issue2965Test.php @@ -0,0 +1,42 @@ +Sheet1!$A$1NewTitle', $data); + } + } + + public function testChartTitleFormula(): void + { + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load(self::DIRECTORY . 'issue.2965.xlsx'); + $worksheet = $spreadsheet->getActiveSheet(); + $charts = $worksheet->getChartCollection(); + self::assertCount(1, $charts); + $originalChart1 = $charts[0]; + self::assertNotNull($originalChart1); + $originalTitle1 = $originalChart1->getTitle(); + self::assertNotNull($originalTitle1); + self::assertSame('NewTitle', $originalTitle1->getCaptionText()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue589Test.php b/tests/PhpSpreadsheetTests/Chart/Issue589Test.php similarity index 96% rename from tests/PhpSpreadsheetTests/Writer/Xlsx/Issue589Test.php rename to tests/PhpSpreadsheetTests/Chart/Issue589Test.php index 82478fb9..747cba74 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue589Test.php +++ b/tests/PhpSpreadsheetTests/Chart/Issue589Test.php @@ -1,6 +1,6 @@ ', $actualXml); + self::assertXmlStringEqualsXmlString('', $actualXml); } } } @@ -153,7 +153,7 @@ class Issue589Test extends TestCase if ($actualXml === false) { self::fail('Failure saving the spPr element as xml string!'); } else { - self::assertXmlStringEqualsXmlString('', $actualXml); + self::assertXmlStringEqualsXmlString('', $actualXml); } } } diff --git a/tests/PhpSpreadsheetTests/Chart/LayoutTest.php b/tests/PhpSpreadsheetTests/Chart/LayoutTest.php index 8e927985..5df139e3 100644 --- a/tests/PhpSpreadsheetTests/Chart/LayoutTest.php +++ b/tests/PhpSpreadsheetTests/Chart/LayoutTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PHPUnit\Framework\TestCase; @@ -27,4 +28,43 @@ class LayoutTest extends TestCase $result = $testInstance->getLayoutTarget(); self::assertEquals($LayoutTargetValue, $result); } + + public function testConstructorVsMethods(): void + { + $fillColor = new ChartColor('FF0000', 20, 'srgbClr'); + $borderColor = new ChartColor('accent1', 20, 'schemeClr'); + $fontColor = new ChartColor('red', 20, 'prstClr'); + $array = [ + 'xMode' => 'factor', + 'yMode' => 'edge', + 'x' => 1.0, + 'y' => 2.0, + 'w' => 3.0, + 'h' => 4.0, + 'showVal' => true, + 'dLblPos' => 't', + 'numFmtCode' => '0.00%', + 'numFmtLinked' => true, + 'labelFillColor' => $fillColor, + 'labelBorderColor' => $borderColor, + 'labelFontColor' => $fontColor, + ]; + $layout1 = new Layout($array); + $layout2 = new Layout(); + $layout2 + ->setXMode('factor') + ->setYMode('edge') + ->setXposition(1.0) + ->setYposition(2.0) + ->setWidth(3.0) + ->setHeight(4.0) + ->setShowVal(true) + ->setDLblPos('t') + ->setNumFmtCode('0.00%') + ->setNumFmtLinked(true) + ->setLabelFillColor($fillColor) + ->setLabelBorderColor($borderColor) + ->setLabelFontColor($fontColor); + self::assertEquals($layout1, $layout2); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/LineStylesTest.php b/tests/PhpSpreadsheetTests/Chart/LineStylesTest.php new file mode 100644 index 00000000..ef940c99 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/LineStylesTest.php @@ -0,0 +1,42 @@ +getLineStyleArray(); + $gridlines1->setLineStyleProperties( + 3, // lineWidth + Properties::LINE_STYLE_COMPOUND_DOUBLE, // compoundType + '', // dashType + Properties::LINE_STYLE_CAP_SQUARE, // capType + '', // jointType + '', // headArrowType + '', // headArrowSize + '', // endArrowType + '', // endArrowSize + 'lg', // headArrowWidth + 'med', // headArrowLength + '', // endArrowWidth + '' // endArrowLength + ); + $gridlines2 = new GridLines(); + $lineStyleProperties = [ + 'width' => 3, + 'compound' => Properties::LINE_STYLE_COMPOUND_DOUBLE, + 'cap' => Properties::LINE_STYLE_CAP_SQUARE, + 'arrow' => ['head' => ['w' => 'lg', 'len' => 'med']], + ]; + $gridlines2->setLineStyleArray($lineStyleProperties); + self::assertSame($gridlines1->getLineStyleArray(), $gridlines2->getLineStyleArray()); + $gridlines2->setLineStyleArray(); // resets line styles + self::assertSame($originalLineStyle, $gridlines2->getLineStyleArray()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php index 35161ff7..db602e8a 100644 --- a/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php +++ b/tests/PhpSpreadsheetTests/Chart/MultiplierTest.php @@ -3,11 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Chart; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; use PhpOffice\PhpSpreadsheet\Chart\Legend as ChartLegend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; -use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -109,7 +109,7 @@ class MultiplierTest extends TestCase 'ky' => null, ], 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_ARGB, + 'type' => ChartColor::EXCEL_COLOR_TYPE_RGB, 'value' => 'FF0000', 'alpha' => 20, ], diff --git a/tests/PhpSpreadsheetTests/Chart/PieFillTest.php b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php new file mode 100644 index 00000000..5b35ca3d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/PieFillTest.php @@ -0,0 +1,161 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testPieFill(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray( + [ + ['', 2010, 2011, 2012], + ['Q1', 12, 15, 21], + ['Q2', 56, 73, 86], + ['Q3', 52, 61, 69], + ['Q4', 30, 32, 0], + ] + ); + // Custom colors for dataSeries (gray, blue, red, orange) + $colors = [ + 'cccccc', + '*accent1', // use schemeClr, was '00abb8', + '/green', // use prstClr, was 'b8292f', + 'eb8500', + ]; + + // 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 + $dataSeriesLabels1 = [ + new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + 'Worksheet!$C$1', + null, + 1 + ), // 2011 + ]; + // Set the X-Axis Labels + // Datatype + // Cell reference for data + // Format Code + // Number of datapoints in series + // Data values + // Data Marker + $xAxisTickValues1 = [ + 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 + // Custom Colors + $dataSeriesValues1Element = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, 'Worksheet!$C$2:$C$5', null, 4); + $dataSeriesValues1Element->setFillColor($colors); + $dataSeriesValues1 = [$dataSeriesValues1Element]; + + // Build the dataseries + $series1 = new DataSeries( + DataSeries::TYPE_PIECHART, // plotType + null, // plotGrouping (Pie charts don't have any grouping) + range(0, count($dataSeriesValues1) - 1), // plotOrder + $dataSeriesLabels1, // plotLabel + $xAxisTickValues1, // plotCategory + $dataSeriesValues1 // plotValues + ); + + // Set up a layout object for the Pie chart + $layout1 = new Layout(); + $layout1->setShowVal(true); + $layout1->setShowPercent(true); + + // Set the series in the plot area + $plotArea1 = new PlotArea($layout1, [$series1]); + // Set the chart legend + $legend1 = new ChartLegend(ChartLegend::POSITION_RIGHT, null, false); + + $title1 = new Title('Test Pie Chart'); + + // Create the chart + $chart1 = new Chart( + 'chart1', // name + $title1, // title + $legend1, // legend + $plotArea1, // plotArea + true, // plotVisibleOnly + DataSeries::EMPTY_AS_GAP, // displayBlanksAs + null, // xAxisLabel + null // no Y-Axis for Pie Chart + ); + + // Set the position where the chart should appear in the worksheet + $chart1->setTopLeftPosition('A7'); + $chart1->setBottomRightPosition('H20'); + + // Add the chart to the worksheet + $worksheet->addChart($chart1); + + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + $charts2 = $sheet->getChartCollection(); + self::assertCount(1, $charts2); + $chart2 = $charts2[0]; + self::assertNotNull($chart2); + $plotArea2 = $chart2->getPlotArea(); + self::assertNotNull($plotArea2); + $dataSeries2 = $plotArea2->getPlotGroup(); + self::assertCount(1, $dataSeries2); + $plotValues = $dataSeries2[0]->getPlotValues(); + self::assertCount(1, $plotValues); + $fillColors = $plotValues[0]->getFillColor(); + self::assertSame($colors, $fillColors); + + $writer = new XlsxWriter($reloadedSpreadsheet); + $writer->setIncludeCharts(true); + $writerChart = new XlsxWriter\Chart($writer); + $data = $writerChart->writeChart($chart2); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(1, substr_count($data, '')); + self::assertSame(4, substr_count($data, '')); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/RenderTest.php b/tests/PhpSpreadsheetTests/Chart/RenderTest.php new file mode 100644 index 00000000..b10e854f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/RenderTest.php @@ -0,0 +1,15 @@ +render()); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php b/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php new file mode 100644 index 00000000..93bbaa23 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/RoundedCornersTest.php @@ -0,0 +1,74 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testRounded(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Data', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertTrue($chart->getRoundedCorners()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } + + public function testNotRounded(): void + { + $file = self::DIRECTORY . '32readwriteAreaChart2.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame(1, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getActiveSheet(); + self::assertSame('Data', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(1, $charts); + $chart = $charts[0]; + self::assertNotNull($chart); + self::assertFalse($chart->getRoundedCorners()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php index 58c024c1..20cacbc1 100644 --- a/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ShadowPresetsTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Chart; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Properties; use PHPUnit\Framework\TestCase; @@ -123,6 +124,35 @@ class ShadowPresetsTest extends TestCase } } + public function testPreset0(): void + { + $axis = new Axis(); + $axis->setShadowProperties(0); + $expectedShadow = [ + 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + 'color' => [ + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, + 'value' => 'black', + 'alpha' => 40, + ], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ]; + foreach ($expectedShadow as $key => $value) { + self::assertEquals($value, $axis->getShadowProperty($key), $key); + } + } + public function testOutOfRangePresets(): void { $axis = new Axis(); @@ -131,7 +161,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], @@ -160,7 +190,7 @@ class ShadowPresetsTest extends TestCase 'presets' => Properties::SHADOW_PRESETS_NOSHADOW, 'effect' => null, 'color' => [ - 'type' => Properties::EXCEL_COLOR_TYPE_STANDARD, + 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, 'value' => 'black', 'alpha' => 40, ], diff --git a/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php new file mode 100644 index 00000000..954a7336 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Chart/TrendLineTest.php @@ -0,0 +1,109 @@ +setIncludeCharts(true); + } + + public function writeCharts(XlsxWriter $writer): void + { + $writer->setIncludeCharts(true); + } + + public function testTrendLine(): void + { + $file = self::DIRECTORY . '32readwriteScatterChartTrendlines1.xlsx'; + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getSheet(1); + self::assertSame(2, $sheet->getChartCount()); + /** @var callable */ + $callableReader = [$this, 'readCharts']; + /** @var callable */ + $callableWriter = [$this, 'writeCharts']; + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx', $callableReader, $callableWriter); + $spreadsheet->disconnectWorksheets(); + + $sheet = $reloadedSpreadsheet->getSheet(1); + self::assertSame('Scatter Chart', $sheet->getTitle()); + $charts = $sheet->getChartCollection(); + self::assertCount(2, $charts); + + $chart = $charts[0]; + self::assertNotNull($chart); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeriesArray); + $plotSeries = $plotSeriesArray[0]; + $valuesArray = $plotSeries->getPlotValues(); + self::assertCount(3, $valuesArray); + self::assertEmpty($valuesArray[0]->getTrendLines()); + self::assertEmpty($valuesArray[1]->getTrendLines()); + self::assertEmpty($valuesArray[2]->getTrendLines()); + + $chart = $charts[1]; + self::assertNotNull($chart); + $plotArea = $chart->getPlotArea(); + self::assertNotNull($plotArea); + $plotSeriesArray = $plotArea->getPlotGroup(); + self::assertCount(1, $plotSeriesArray); + $plotSeries = $plotSeriesArray[0]; + $valuesArray = $plotSeries->getPlotValues(); + self::assertCount(1, $valuesArray); + $trendLines = $valuesArray[0]->getTrendLines(); + self::assertCount(3, $trendLines); + + $trendLine = $trendLines[0]; + self::assertSame('linear', $trendLine->getTrendLineType()); + self::assertFalse($trendLine->getDispRSqr()); + self::assertFalse($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent4', $lineColor->getValue()); + self::assertSame('stealth', $trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(0.5, $trendLine->getLineStyleProperty('width')); + self::assertSame('', $trendLine->getName()); + self::assertSame(0.0, $trendLine->getBackward()); + self::assertSame(0.0, $trendLine->getForward()); + self::assertSame(0.0, $trendLine->getIntercept()); + + $trendLine = $trendLines[1]; + self::assertSame('poly', $trendLine->getTrendLineType()); + self::assertTrue($trendLine->getDispRSqr()); + self::assertTrue($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent3', $lineColor->getValue()); + self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(1.25, $trendLine->getLineStyleProperty('width')); + self::assertSame('metric3 polynomial', $trendLine->getName()); + self::assertSame(20.0, $trendLine->getBackward()); + self::assertSame(28.0, $trendLine->getForward()); + self::assertSame(14400.5, $trendLine->getIntercept()); + + $trendLine = $trendLines[2]; + self::assertSame('movingAvg', $trendLine->getTrendLineType()); + self::assertTrue($trendLine->getDispRSqr()); + self::assertFalse($trendLine->getDispEq()); + $lineColor = $trendLine->getLineColor(); + self::assertSame('accent2', $lineColor->getValue()); + self::assertNull($trendLine->getLineStyleProperty(['arrow', 'head', 'type'])); + self::assertEquals(1.5, $trendLine->getLineStyleProperty('width')); + self::assertSame('', $trendLine->getName()); + self::assertSame(0.0, $trendLine->getBackward()); + self::assertSame(0.0, $trendLine->getForward()); + self::assertSame(0.0, $trendLine->getIntercept()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Collection/CellsTest.php b/tests/PhpSpreadsheetTests/Collection/CellsTest.php index 5731d581..9e662b92 100644 --- a/tests/PhpSpreadsheetTests/Collection/CellsTest.php +++ b/tests/PhpSpreadsheetTests/Collection/CellsTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Collection; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Collection\Memory; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -107,7 +108,10 @@ class CellsTest extends TestCase $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); $collection = $this->getMockBuilder(Cells::class) - ->setConstructorArgs([new Worksheet(), new Memory()]) + ->setConstructorArgs([ + new Worksheet(), + Settings::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(), + ]) ->onlyMethods(['has']) ->getMock(); @@ -121,7 +125,9 @@ class CellsTest extends TestCase { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $cache = $this->createMock(Memory::class); + $cache = $this->createMock( + Settings::useSimpleCacheVersion3() ? Memory\SimpleCache3::class : Memory\SimpleCache1::class + ); $cell = $this->createMock(Cell::class); $cache->method('set') ->willReturn(false); diff --git a/tests/PhpSpreadsheetTests/CommentTest.php b/tests/PhpSpreadsheetTests/CommentTest.php index e58afad4..aacd538f 100644 --- a/tests/PhpSpreadsheetTests/CommentTest.php +++ b/tests/PhpSpreadsheetTests/CommentTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Comment; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\TextElement; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Color; use PHPUnit\Framework\TestCase; @@ -83,4 +84,14 @@ class CommentTest extends TestCase $comment->setText($test); self::assertEquals('This is a test comment', (string) $comment); } + + public function testRemoveComment(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getComment('A2')->getText()->createText('Comment to delete'); + self::assertArrayHasKey('A2', $sheet->getComments()); + $sheet->removeComment('A2'); + self::assertEmpty($sheet->getComments()); + } } diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 43eddc8a..c001f204 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -58,7 +58,7 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); self::assertCount(2, $this->spreadsheet->getDefinedNames()); @@ -66,7 +66,7 @@ class DefinedNameTest extends TestCase self::assertNotNull($definedName1); self::assertSame('=A1', $definedName1->getValue()); - $definedName2 = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $definedName2 = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($definedName2); self::assertSame('=B1', $definedName2->getValue()); } @@ -85,19 +85,31 @@ class DefinedNameTest extends TestCase self::assertCount(1, $this->spreadsheet->getDefinedNames()); } + public function testRemoveGlobalDefinedName(): void + { + $this->spreadsheet->addDefinedName( + DefinedName::createInstance('Any', $this->spreadsheet->getActiveSheet(), '=A1') + ); + self::assertCount(1, $this->spreadsheet->getDefinedNames()); + + $this->spreadsheet->removeDefinedName('Any'); + self::assertCount(0, $this->spreadsheet->getDefinedNames()); + $this->spreadsheet->removeDefinedName('Other'); + } + public function testRemoveGlobalDefinedNameWhenDuplicateNames(): void { $this->spreadsheet->addDefinedName( DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getDefinedNames()); - $definedName = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $definedName = $this->spreadsheet->getDefinedName('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($definedName); self::assertSame('=B1', $definedName->getValue()); } @@ -108,10 +120,10 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addDefinedName( - DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + DefinedName::createInstance('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); - $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeDefinedName('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getDefinedNames()); $definedName = $this->spreadsheet->getDefinedName('foo'); @@ -142,10 +154,8 @@ class DefinedNameTest extends TestCase public function testChangeWorksheet(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -160,10 +170,8 @@ class DefinedNameTest extends TestCase public function testLocalOnly(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -178,10 +186,8 @@ class DefinedNameTest extends TestCase public function testScope(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); @@ -196,10 +202,8 @@ class DefinedNameTest extends TestCase public function testClone(): void { - $sheet1 = $this->spreadsheet->getSheetByName('Sheet #1'); - $sheet2 = $this->spreadsheet->getSheetByName('Sheet #2'); - self::assertNotNull($sheet1); - self::assertNotNull($sheet2); + $sheet1 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #1'); + $sheet2 = $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'); $sheet1->getCell('A1')->setValue(1); $sheet2->getCell('A1')->setValue(2); diff --git a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php index 700c7c7f..921c59f5 100644 --- a/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php +++ b/tests/PhpSpreadsheetTests/Functional/PrintAreaTest.php @@ -58,8 +58,7 @@ class PrintAreaTest extends AbstractFunctional private static function getPrintArea(Spreadsheet $spreadsheet, string $name): string { - $sheet = $spreadsheet->getSheetByName($name); - self::assertNotNull($sheet, "Unable to get sheet $name"); + $sheet = $spreadsheet->getSheetByNameOrThrow($name); return $sheet->getPageSetup()->getPrintArea(); } diff --git a/tests/PhpSpreadsheetTests/Functional/StreamTest.php b/tests/PhpSpreadsheetTests/Functional/StreamTest.php index 3911aaa6..05b87ab9 100644 --- a/tests/PhpSpreadsheetTests/Functional/StreamTest.php +++ b/tests/PhpSpreadsheetTests/Functional/StreamTest.php @@ -17,15 +17,10 @@ class StreamTest extends TestCase ['Csv'], ['Html'], ['Mpdf'], + ['Dompdf'], + ['Tcpdf'], ]; - if (\PHP_VERSION_ID < 80000) { - $providerFormats = array_merge( - $providerFormats, - [['Tcpdf'], ['Dompdf']] - ); - } - return $providerFormats; } diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index 50a650f8..384011e8 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -9,7 +9,9 @@ class SampleTest extends TestCase { /** * @runInSeparateProcess + * * @preserveGlobalState disabled + * * @dataProvider providerSample */ public function testSample(string $sample): void @@ -26,9 +28,6 @@ class SampleTest extends TestCase public function providerSample(): array { $skipped = [ - 'Chart/32_Chart_read_write_PDF.php', // Unfortunately JpGraph is not up to date for latest PHP and raise many warnings - 'Chart/32_Chart_read_write_HTML.php', // idem - 'Chart/35_Chart_render.php', // idem ]; // Unfortunately some tests are too long to run with code-coverage diff --git a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php index e7429a25..5acee2a0 100644 --- a/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php +++ b/tests/PhpSpreadsheetTests/LocaleGeneratorTest.php @@ -5,37 +5,61 @@ namespace PhpOffice\PhpSpreadsheetTests; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheetInfra\LocaleGenerator; use PHPUnit\Framework\TestCase; -use ReflectionClass; class LocaleGeneratorTest extends TestCase { public function testLocaleGenerator(): void { - $phpSpreadsheetFunctionsProperty = (new ReflectionClass(Calculation::class)) - ->getProperty('phpSpreadsheetFunctions'); - $phpSpreadsheetFunctionsProperty->setAccessible(true); - $phpSpreadsheetFunctions = $phpSpreadsheetFunctionsProperty->getValue(); + $directory = realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/') ?: ''; + self::assertNotEquals('', $directory); + $phpSpreadsheetFunctions = Calculation::getFunctions(); $localeGenerator = new LocaleGenerator( - (string) realpath(__DIR__ . '/../../src/PhpSpreadsheet/Calculation/locale/'), + $directory . DIRECTORY_SEPARATOR, 'Translations.xlsx', $phpSpreadsheetFunctions ); $localeGenerator->generateLocales(); $testLocales = [ + 'bg', + 'cs', + 'da', + 'de', + 'en', + 'es', + 'fi', 'fr', + 'hu', + 'it', + 'nb', 'nl', + 'pl', 'pt', - 'pt_br', 'ru', + 'sv', + 'tr', ]; - foreach ($testLocales as $locale) { - $locale = str_replace('_', '/', $locale); - $path = realpath(__DIR__ . "/../../src/PhpSpreadsheet/Calculation/locale/{$locale}"); - self::assertFileExists("{$path}/config"); - self::assertFileExists("{$path}/functions"); + $count = count(glob($directory . DIRECTORY_SEPARATOR . '*') ?: []) - 1; // exclude Translations.xlsx + self::assertCount($count, $testLocales); + $testLocales[] = 'pt_br'; + $testLocales[] = 'en_uk'; + $noconfig = ['en']; + $nofunctions = ['en', 'en_uk']; + foreach ($testLocales as $originalLocale) { + $locale = str_replace('_', DIRECTORY_SEPARATOR, $originalLocale); + $path = $directory . DIRECTORY_SEPARATOR . $locale; + if (in_array($originalLocale, $noconfig, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'config'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'config'); + } + if (in_array($originalLocale, $nofunctions, true)) { + self::assertFileDoesNotExist($path . DIRECTORY_SEPARATOR . 'functions'); + } else { + self::assertFileExists($path . DIRECTORY_SEPARATOR . 'functions'); + } } } } diff --git a/tests/PhpSpreadsheetTests/NamedFormulaTest.php b/tests/PhpSpreadsheetTests/NamedFormulaTest.php index 4c4a6b11..28857abe 100644 --- a/tests/PhpSpreadsheetTests/NamedFormulaTest.php +++ b/tests/PhpSpreadsheetTests/NamedFormulaTest.php @@ -62,7 +62,7 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); self::assertCount(2, $this->spreadsheet->getNamedFormulae()); @@ -72,7 +72,7 @@ class NamedFormulaTest extends TestCase '=19%', $formula->getValue() ); - $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($formula); self::assertSame( '=16%', @@ -100,13 +100,13 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getNamedFormulae()); - $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $formula = $this->spreadsheet->getNamedFormula('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($formula); self::assertSame( '=16%', @@ -120,10 +120,10 @@ class NamedFormulaTest extends TestCase new NamedFormula('Foo', $this->spreadsheet->getActiveSheet(), '=19%') ); $this->spreadsheet->addNamedFormula( - new NamedFormula('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=16%', true) + new NamedFormula('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=16%', true) ); - $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeNamedFormula('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getNamedFormulae()); $formula = $this->spreadsheet->getNamedFormula('foo'); @@ -133,4 +133,10 @@ class NamedFormulaTest extends TestCase $formula->getValue() ); } + + public function testRemoveNonExistentNamedFormula(): void + { + self::assertCount(0, $this->spreadsheet->getNamedFormulae()); + $this->spreadsheet->removeNamedFormula('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/NamedRangeTest.php b/tests/PhpSpreadsheetTests/NamedRangeTest.php index c72b7b73..9440ef21 100644 --- a/tests/PhpSpreadsheetTests/NamedRangeTest.php +++ b/tests/PhpSpreadsheetTests/NamedRangeTest.php @@ -62,7 +62,7 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); self::assertCount(2, $this->spreadsheet->getNamedRanges()); @@ -72,7 +72,7 @@ class NamedRangeTest extends TestCase '=A1', $range->getValue() ); - $range = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $range = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($range); self::assertSame( '=B1', @@ -100,13 +100,13 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getActiveSheet()); self::assertCount(1, $this->spreadsheet->getNamedRanges()); - $sheet = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $sheet = $this->spreadsheet->getNamedRange('foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertNotNull($sheet); self::assertSame( '=B1', @@ -120,10 +120,10 @@ class NamedRangeTest extends TestCase new NamedRange('Foo', $this->spreadsheet->getActiveSheet(), '=A1') ); $this->spreadsheet->addNamedRange( - new NamedRange('FOO', $this->spreadsheet->getSheetByName('Sheet #2'), '=B1', true) + new NamedRange('FOO', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2'), '=B1', true) ); - $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getSheetByName('Sheet #2')); + $this->spreadsheet->removeNamedRange('Foo', $this->spreadsheet->getSheetByNameOrThrow('Sheet #2')); self::assertCount(1, $this->spreadsheet->getNamedRanges()); $range = $this->spreadsheet->getNamedRange('foo'); @@ -133,4 +133,10 @@ class NamedRangeTest extends TestCase $range->getValue() ); } + + public function testRemoveNonExistentNamedRange(): void + { + self::assertCount(0, $this->spreadsheet->getNamedRanges()); + $this->spreadsheet->removeNamedRange('Any'); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php index 291a29d0..8c28c1e7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvContiguousTest.php @@ -56,13 +56,11 @@ class CsvContiguousTest extends TestCase private static function getCellValue(Spreadsheet $spreadsheet, string $sheetName, string $cellAddress): string { - $sheet = $spreadsheet->getSheetByName($sheetName); + $sheet = $spreadsheet->getSheetByNameOrThrow($sheetName); $result = ''; - if ($sheet !== null) { - $value = $sheet->getCell($cellAddress)->getValue(); - if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { - $result = (string) $value; - } + $value = $sheet->getCell($cellAddress)->getValue(); + if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { + $result = (string) $value; } return $result; diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php index 448d3d1e..0cd79853 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvEncodingTest.php @@ -66,6 +66,25 @@ class CsvEncodingTest extends TestCase self::assertEquals('sixième', $sheet->getCell('C2')->getValue()); } + public function testSurrogate(): void + { + // Surrogates should occur only in UTF-16, and should + // be properly converted to UTF8 when read. + // FFFE/FFFF are illegal, and should be converted to + // substitution character when read. + // Excel does not handle any of the cells in row 3 well. + // LibreOffice handles A3 fine, and discards B3/C3, + // which is a reasonable action. + $filename = 'tests/data/Reader/CSV/premiere.utf16le.csv'; + $reader = new Csv(); + $reader->setInputEncoding(Csv::guessEncoding($filename)); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertEquals('𐐀', $sheet->getCell('A3')->getValue()); + self::assertEquals('�', $sheet->getCell('B3')->getValue()); + self::assertEquals('�', $sheet->getCell('C3')->getValue()); + } + /** * @dataProvider providerGuessEncoding */ diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php index f9321102..429874dc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvIssue2232Test.php @@ -2,11 +2,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Csv; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\IValueBinder; use PhpOffice\PhpSpreadsheet\Cell\StringValueBinder; use PhpOffice\PhpSpreadsheet\Reader\Csv; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class CsvIssue2232Test extends TestCase @@ -16,14 +16,19 @@ class CsvIssue2232Test extends TestCase */ private $valueBinder; + /** @var string */ + private $locale; + protected function setUp(): void { $this->valueBinder = Cell::getValueBinder(); + $this->locale = Settings::getLocale(); } protected function tearDown(): void { Cell::setValueBinder($this->valueBinder); + Settings::setLocale($this->locale); } /** @@ -78,7 +83,7 @@ class CsvIssue2232Test extends TestCase Cell::setValueBinder($binder); } - Calculation::getInstance()->setLocale('fr'); + Settings::setLocale('fr'); $reader = new Csv(); $filename = 'tests/data/Reader/CSV/issue.2232.csv'; diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php index cf4157e3..fe0117ca 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlImageTest.php @@ -13,7 +13,7 @@ class HtmlImageTest extends TestCase $html = ' - +
test imagetest image voilà
'; $filename = HtmlHelper::createHtml($html); @@ -24,7 +24,7 @@ class HtmlImageTest extends TestCase $drawing = $firstSheet->getDrawingCollection()[0]; self::assertEquals($imagePath, $drawing->getPath()); self::assertEquals('A1', $drawing->getCoordinates()); - self::assertEquals('test image', $drawing->getName()); + self::assertEquals('test image voilà', $drawing->getName()); self::assertEquals('100', $drawing->getWidth()); self::assertEquals('100', $drawing->getHeight()); } diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php index cabac403..ad395ace 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlTest.php @@ -80,6 +80,7 @@ class HtmlTest extends TestCase Blue background Unknown fore/background + Unknown fore/background '; $filename = HtmlHelper::createHtml($html); @@ -93,6 +94,10 @@ class HtmlTest extends TestCase self::assertEquals('000000', $style->getFont()->getColor()->getRGB()); self::assertEquals('000000', $style->getFill()->getEndColor()->getRGB()); self::assertEquals('FFFFFF', $style->getFill()->getstartColor()->getRGB()); + $style = $firstSheet->getCell('C1')->getStyle(); + self::assertEquals('f0f8ff', $style->getFont()->getColor()->getRGB()); + self::assertEquals('eedfcc', $style->getFill()->getEndColor()->getRGB()); + self::assertEquals('eedfcc', $style->getFill()->getstartColor()->getRGB()); } public function testCanApplyInlineFontStyles(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php new file mode 100644 index 00000000..3a41805c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Html/Issue2942Test.php @@ -0,0 +1,36 @@ +éàâèî'; + $reader = new Html(); + $spreadsheet = $reader->loadFromString($content); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + } + + public function testLoadFromFile(): void + { + $file = 'tests/data/Reader/HTML/utf8chars.html'; + $reader = new Html(); + $spreadsheet = $reader->loadSpreadsheetFromFile($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Test Utf-8 characters voilà', $sheet->getTitle()); + self::assertSame('éàâèî', $sheet->getCell('A1')->getValue()); + self::assertSame('αβγδε', $sheet->getCell('B1')->getValue()); + self::assertSame('𐐁𐐂𐐃 & だけち', $sheet->getCell('A2')->getValue()); + self::assertSame('אבגדה', $sheet->getCell('B2')->getValue()); + self::assertSame('𪔀𪔁𪔂', $sheet->getCell('C2')->getValue()); + self::assertSame('᠐᠑᠒', $sheet->getCell('A3')->getValue()); + self::assertSame('അആ', $sheet->getCell('B3')->getValue()); + self::assertSame('กขฃ', $sheet->getCell('C3')->getValue()); + self::assertSame('✀✐✠', $sheet->getCell('D3')->getValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php new file mode 100644 index 00000000..710b292a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php new file mode 100644 index 00000000..977b852b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ColorMapTest.php @@ -0,0 +1,34 @@ + [0x00, '000000', '000000', '000000'], + 'non-default builtin' => [0x02, '000000', '000000', 'FF0000'], + 'system window text color' => [0x40, '000000', '000000', '000000'], + 'system window background color' => [0x41, '000000', '000000', 'FFFFFF'], + 'same biff5/8' => [0x09, 'FFFFFF', 'FFFFFF', '000000'], + 'different biff5/8' => [0x29, '69FFFF', 'CCFFFF', '000000'], + + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php new file mode 100644 index 00000000..e857d84f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/ErrorCodeMapTest.php @@ -0,0 +1,33 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php index a67fbb34..80867892 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/NumberFormatGeneralTest.php @@ -15,20 +15,16 @@ class NumberFormatGeneralTest extends AbstractFunctional $reader = new Xls(); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('Blad1'); - if ($sheet === null) { - self::fail('Expected to find sheet Blad1'); - } else { - $array = $sheet->toArray(); - self::assertSame('€ 2.95', $array[1][3]); - self::assertSame(2.95, $sheet->getCell('D2')->getValue()); - self::assertSame(2.95, $sheet->getCell('D2')->getCalculatedValue()); - self::assertSame('€ 2.95', $sheet->getCell('D2')->getFormattedValue()); - self::assertSame(21, $array[1][4]); - self::assertSame(21, $sheet->getCell('E2')->getValue()); - self::assertSame(21, $sheet->getCell('E2')->getCalculatedValue()); - self::assertSame('21', $sheet->getCell('E2')->getFormattedValue()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('Blad1'); + $array = $sheet->toArray(); + self::assertSame('€ 2.95', $array[1][3]); + self::assertSame(2.95, $sheet->getCell('D2')->getValue()); + self::assertSame(2.95, $sheet->getCell('D2')->getCalculatedValue()); + self::assertSame('€ 2.95', $sheet->getCell('D2')->getFormattedValue()); + self::assertSame(21, $array[1][4]); + self::assertSame(21, $sheet->getCell('E2')->getValue()); + self::assertSame(21, $sheet->getCell('E2')->getCalculatedValue()); + self::assertSame('21', $sheet->getCell('E2')->getFormattedValue()); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php b/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php new file mode 100644 index 00000000..c3056c08 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Rc4Test.php @@ -0,0 +1,21 @@ +RC4($string)); + $expectedResult = '2ac2fecdd8fbb84638e3a4820eb205cc8e29c28b9d5d6b2ef974f311964971c90e8b9ca16467ef2dc6fc3520'; + self::assertSame($expectedResult, $result); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php index 54274e8c..cf45dec1 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/RichTextSizeTest.php @@ -12,8 +12,7 @@ class RichTextSizeTest extends AbstractFunctional $filename = 'tests/data/Reader/XLS/RichTextFontSize.xls'; $reader = new Xls(); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('橱柜门板'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('橱柜门板'); $text = $sheet->getCell('L15')->getValue(); $elements = $text->getRichTextElements(); self::assertEquals(10, $elements[2]->getFont()->getSize()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php index 6d6949d8..7a264394 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/AutoFilter2Test.php @@ -11,12 +11,14 @@ class AutoFilter2Test extends TestCase { private const TESTBOOK = 'tests/data/Reader/XLSX/autofilter2.xlsx'; - public function getVisibleSheet(Worksheet $sheet, int $maxRow): array + public function getVisibleSheet(?Worksheet $sheet, int $maxRow): array { $actualVisible = []; - for ($row = 2; $row <= $maxRow; ++$row) { - if ($sheet->getRowDimension($row)->getVisible()) { - $actualVisible[] = $row; + if ($sheet !== null) { + for ($row = 2; $row <= $maxRow; ++$row) { + if ($sheet->getRowDimension($row)->getVisible()) { + $actualVisible[] = $row; + } } } @@ -26,8 +28,7 @@ class AutoFilter2Test extends TestCase public function testReadDateRange(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('daterange'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('daterange'); $filter = $sheet->getAutoFilter(); $maxRow = 30; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -35,13 +36,14 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; self::assertSame(Rule::AUTOFILTER_RULETYPE_DATEGROUP, $rule->getRuleType()); $value = $rule->getValue(); self::assertIsArray($value); - self::assertCount(6, $value); + self::assertCount(6, /** @scrutinizer ignore-type */ $value); self::assertSame('2002', $value['year']); self::assertSame('', $value['month']); self::assertSame('', $value['day']); @@ -58,8 +60,7 @@ class AutoFilter2Test extends TestCase public function testReadTopTen(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('top10'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('top10'); $filter = $sheet->getAutoFilter(); $maxRow = 65; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -67,6 +68,7 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; @@ -83,8 +85,7 @@ class AutoFilter2Test extends TestCase public function testReadDynamic(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('dynamic'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('dynamic'); $filter = $sheet->getAutoFilter(); $maxRow = 30; self::assertSame("A1:A$maxRow", $filter->getRange()); @@ -92,6 +93,7 @@ class AutoFilter2Test extends TestCase self::assertCount(1, $columns); $column = $columns['A'] ?? null; self::assertNotNull($column); + /** @scrutinizer ignore-call */ $ruleset = $column->getRules(); self::assertCount(1, $ruleset); $rule = $ruleset[0]; diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php index 0f1605ff..e7862697 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ChartSheetTest.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class ChartSheetTest extends TestCase @@ -16,8 +15,7 @@ class ChartSheetTest extends TestCase $spreadsheet = $reader->load($filename); self::assertCount(2, $spreadsheet->getAllSheets()); - $chartSheet = $spreadsheet->getSheetByName('Chart1'); - self::assertInstanceOf(Worksheet::class, $chartSheet); + $chartSheet = $spreadsheet->getSheetByNameOrThrow('Chart1'); self::assertSame(1, $chartSheet->getChartCount()); } @@ -29,7 +27,6 @@ class ChartSheetTest extends TestCase $spreadsheet = $reader->load($filename); self::assertCount(1, $spreadsheet->getAllSheets()); - $chartSheet = $spreadsheet->getSheetByName('Chart1'); - self::assertNull($chartSheet); + self::assertNull($spreadsheet->getSheetByName('Chart1')); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php index 4b051dd0..0ec25d8e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ConditionalFormattingDataBarXlsxTest.php @@ -86,8 +86,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase $dataBar = $conditionalRule->getDataBar(); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals(Color::COLOR_GREEN, $dataBar->getColor()); @@ -109,8 +109,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); @@ -118,6 +118,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar->getConditionalFormattingRuleExt()); //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{72C64AE0-5CD9-164F-83D1-AB720F263E79}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('A3:A23', $rule1ext->getSqref()); @@ -165,8 +166,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('num', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('num', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals('-5', $dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -175,6 +176,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar->getConditionalFormattingRuleExt()); //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{98904F60-57F0-DF47-B480-691B20D325E3}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('B3:B23', $rule1ext->getSqref()); @@ -224,8 +226,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotEmpty($dataBar); self::assertEquals(Conditional::CONDITION_DATABAR, $conditionalRule->getConditionType()); self::assertNotNull($dataBar); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('min', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('max', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEmpty($dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -235,6 +237,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{453C04BA-7ABD-8548-8A17-D9CFD2BDABE9}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('C3:C23', $rule1ext->getSqref()); @@ -286,8 +289,8 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase self::assertNotNull($dataBar); self::assertTrue($dataBar->getShowValue()); - self::assertNotEmpty($dataBar->getMinimumConditionalFormatValueObject()); - self::assertNotEmpty($dataBar->getMaximumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMinimumConditionalFormatValueObject()); + self::assertNotNull($dataBar->getMaximumConditionalFormatValueObject()); self::assertEquals('formula', $dataBar->getMinimumConditionalFormatValueObject()->getType()); self::assertEquals('formula', $dataBar->getMaximumConditionalFormatValueObject()->getType()); self::assertEquals('3+2', $dataBar->getMinimumConditionalFormatValueObject()->getValue()); @@ -297,6 +300,7 @@ class ConditionalFormattingDataBarXlsxTest extends TestCase //ext $rule1ext = $dataBar->getConditionalFormattingRuleExt(); + self::assertNotNull($rule1ext); self::assertEquals('{6C1E066A-E240-3D4A-98F8-8CC218B0DFD2}', $rule1ext->getId()); self::assertEquals('dataBar', $rule1ext->getCfRule()); self::assertEquals('D3:D23', $rule1ext->getSqref()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php new file mode 100644 index 00000000..ddff17d6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DataValidationTest.php @@ -0,0 +1,58 @@ +load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + + self::assertTrue($worksheet->getCell('B3')->hasDataValidation()); + $spreadsheet->disconnectWorksheets(); + } + + /** + * Test for load drop down lists of another sheet. + * Pull #2150, issue #2149. Also issue #2677. + * + * @dataProvider providerExternalSheet + */ + public function testDataValidationOfAnotherSheet(string $expectedB14, string $filename): void + { + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + + // same sheet + $validationCell = $worksheet->getCell('B5'); + self::assertTrue($validationCell->hasDataValidation()); + self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); + self::assertSame('$A$5:$A$7', $validationCell->getDataValidation()->getFormula1()); + + // another sheet + $validationCell = $worksheet->getCell('B14'); + self::assertTrue($validationCell->hasDataValidation()); + self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); + self::assertSame($expectedB14, $validationCell->getDataValidation()->getFormula1()); + $spreadsheet->disconnectWorksheets(); + } + + public function providerExternalSheet(): array + { + return [ + 'standard spreadsheet' => ['Feuil2!$A$3:$A$5', 'tests/data/Reader/XLSX/dataValidation2Test.xlsx'], + 'alternate namespace prefix' => ['Feuil2!$A$3:$A$5', 'tests/data/Reader/XLSX/issue.2677.namespace.xlsx'], + 'missing formula' => ['', 'tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php new file mode 100644 index 00000000..4919cd27 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/HiddenMergeCellsTest.php @@ -0,0 +1,48 @@ +spreadsheet = $reader->load($filename); + } + + public function testHiddenMergeCells(): void + { + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertTrue($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertTrue($a2InMergeRange); + $a2MergeRangeValue = $this->spreadsheet->getActiveSheet()->getCell('A2')->isMergeRangeValueCell(); + self::assertTrue($a2MergeRangeValue); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2'); + self::assertSame([[12, 4, 3]], $cellArray); + } + + public function testUnmergeHiddenMergeCells(): void + { + $this->spreadsheet->getActiveSheet()->unmergeCells('A2:C2'); + + $c2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('C2')->isInMergeRange(); + self::assertFalse($c2InMergeRange); + $a2InMergeRange = $this->spreadsheet->getActiveSheet()->getCell('A2')->isInMergeRange(); + self::assertFalse($a2InMergeRange); + + $cellArray = $this->spreadsheet->getActiveSheet()->rangeToArray('A2:C2', null, false, false, false); + self::assertSame([[12, '=6-B1', '=A2/B2']], $cellArray); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php index 4484b9e0..7a5829f7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2301Test.php @@ -24,5 +24,7 @@ class Issue2301Test extends \PHPUnit\Framework\TestCase self::assertSame('Arial CE', $font->getName()); self::assertSame(9.0, $font->getSize()); self::assertSame('protected', $sheet->getCell('BT10')->getStyle()->getProtection()->getHidden()); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php index 182581e1..f65185e7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2490Test.php @@ -12,6 +12,11 @@ class Issue2490Test extends TestCase */ private static $testbook = 'tests/data/Reader/XLSX/issue.2490.xlsx'; + /** + * @var string + */ + private static $testbook3093 = 'tests/data/Reader/XLSX/issue.3093.xlsx'; + public function testPreliminaries(): void { $file = 'zip://'; @@ -23,6 +28,7 @@ class Issue2490Test extends TestCase self::fail('Unable to read file'); } else { self::assertStringContainsString('', $data); + self::assertSame(64, substr_count($data, 'getCell('B1')->getStyle()->getFill()->getStartColor()->getArgb()); $spreadsheet->disconnectWorksheets(); } + + public function testPreliminaries3093(): void + { + $file = 'zip://'; + $file .= self::$testbook3093; + $file .= '#xl/styles.xml'; + $data = file_get_contents($file); + // confirm that file contains expected color index tag + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString('', $data); + self::assertSame(15, substr_count($data, 'load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('ffc0c0c0', $sheet->getCell('B2')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('ffffff00', $sheet->getCell('D2')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('ffdfa7a6', $sheet->getCell('F2')->getStyle()->getFill()->getStartColor()->getArgb()); + self::assertSame('ff7ba0cd', $sheet->getCell('H2')->getStyle()->getFill()->getStartColor()->getArgb()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php index 6b090fe8..57958245 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2501Test.php @@ -42,28 +42,20 @@ class Issue2501Test extends TestCase $filename = self::$testbook; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); - $sheet = $spreadsheet->getSheetByName('Columns'); + $sheet = $spreadsheet->getSheetByNameOrThrow('Columns'); $expected = [ 'A1:A1048576', 'B1:D1048576', 'E2:E4', ]; - if ($sheet === null) { - self::fail('Unable to find sheet Columns'); - } else { - self::assertSame($expected, array_values($sheet->getMergeCells())); - } - $sheet = $spreadsheet->getSheetByName('Rows'); + self::assertSame($expected, array_values($sheet->getMergeCells())); + $sheet = $spreadsheet->getSheetByNameOrThrow('Rows'); $expected = [ 'A1:XFD1', 'A2:XFD4', 'B5:D5', ]; - if ($sheet === null) { - self::fail('Unable to find sheet Rows'); - } else { - self::assertSame($expected, array_values($sheet->getMergeCells())); - } + self::assertSame($expected, array_values($sheet->getMergeCells())); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php new file mode 100644 index 00000000..82727ef8 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2885Test.php @@ -0,0 +1,30 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('[$-809]0%', $sheet->getStyle('A1')->getNumberFormat()->getFormatCode()); + + $finishColumns = $sheet->getHighestColumn(); + $rowsCount = $sheet->getHighestRow(); + $rows = $sheet->rangeToArray("A1:{$finishColumns}{$rowsCount}"); + self::assertSame('8%', $rows[0][0]); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceIssue2109bTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceIssue2109bTest.php index 4c1628fc..b970f084 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceIssue2109bTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceIssue2109bTest.php @@ -1,6 +1,6 @@ getFreezePane()); self::assertSame('A2', $sheet->getTopLeftCell()); self::assertSame('B3', $sheet->getSelectedCells()); - $sheet = $spreadsheet->getSheetByName('SylkTest'); - if ($sheet === null) { - self::fail('Unable to load expected sheet'); - } else { - self::assertNull($sheet->getFreezePane()); - self::assertNull($sheet->getTopLeftCell()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('SylkTest'); + self::assertNull($sheet->getFreezePane()); + self::assertNull($sheet->getTopLeftCell()); } public function testLoadXlsx(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php index d57dc049..afb36862 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespaceOpenpyxl35Test.php @@ -1,6 +1,6 @@ $array1) { - $sheet = $spreadsheet->getSheetByName($sheetName); - if ($sheet === null) { - self::fail("Unable to find sheet $sheetName"); - } else { - foreach ($array1 as $key => $value) { - self::assertSame($value, self::getCellValue($sheet, $key), "error in sheet $sheetName cell $key"); - } + $sheet = $spreadsheet->getSheetByNameOrThrow($sheetName); + foreach ($array1 as $key => $value) { + self::assertSame($value, self::getCellValue($sheet, $key), "error in sheet $sheetName cell $key"); } } $spreadsheet->disconnectWorksheets(); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespacePurlTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespacePurlTest.php index ddd6ac5d..e8e9eaba 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespacePurlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/NamespacePurlTest.php @@ -1,6 +1,6 @@ getFreezePane()); self::assertSame('A2', $sheet->getTopLeftCell()); self::assertSame('B3', $sheet->getSelectedCells()); - $sheet = $spreadsheet->getSheetByName('SylkTest'); - if ($sheet === null) { - self::fail('Unable to load expected sheet'); - } else { - self::assertNull($sheet->getFreezePane()); - self::assertNull($sheet->getTopLeftCell()); - } + $sheet = $spreadsheet->getSheetByNameOrThrow('SylkTest'); + self::assertNull($sheet->getFreezePane()); + self::assertNull($sheet->getTopLeftCell()); } public function testLoadXlsx(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php index 1bbc88ac..12675b41 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/PageSetup2Test.php @@ -28,8 +28,7 @@ class PageSetup2Test extends TestCase public function testColumnBreak(): void { $spreadsheet = IOFactory::load(self::TESTBOOK); - $sheet = $spreadsheet->getSheetByName('colbreak'); - self::assertNotNull($sheet); + $sheet = $spreadsheet->getSheetByNameOrThrow('colbreak'); $breaks = $sheet->getBreaks(); self::assertCount(1, $breaks); $break = $breaks['D1'] ?? null; diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php new file mode 100644 index 00000000..68b3bb01 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/RibbonTest.php @@ -0,0 +1,77 @@ +load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen(/** @scrutinizer ignore-type */ $data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + self::assertNull($spreadsheet->getRibbonBinObjects()); + foreach (['names', 'data', 'xxxxx'] as $type) { + self::assertNull($spreadsheet->getRibbonBinObjects($type), "Expecting null when type is $type"); + } + self::assertEmpty($spreadsheet->getRibbonBinObjects('types')); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertSame($vbaCode, $reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } + + /** + * Same as above but discard macros. + */ + public function testDiscardMacros(): void + { + $filename = 'tests/data/Reader/XLSX/ribbon.donotopen.zip'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + self::assertTrue($spreadsheet->hasRibbon()); + $target = $spreadsheet->getRibbonXMLData('target'); + self::assertSame('customUI/customUI.xml', $target); + $data = $spreadsheet->getRibbonXMLData('data'); + self::assertIsString($data); + self::assertSame(1522, strlen(/** @scrutinizer ignore-type */ $data)); + $vbaCode = (string) $spreadsheet->getMacrosCode(); + self::assertSame(13312, strlen($vbaCode)); + $spreadsheet->discardMacros(); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + self::assertTrue($reloadedSpreadsheet->hasRibbon()); + $ribbonData = $reloadedSpreadsheet->getRibbonXmlData(); + self::assertIsArray($ribbonData); + self::assertSame($target, $ribbonData['target'] ?? ''); + self::assertSame($data, $ribbonData['data'] ?? ''); + self::assertNull($reloadedSpreadsheet->getMacrosCode()); + self::assertNull($reloadedSpreadsheet->getRibbonBinObjects()); + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php index 0cbd103d..d47ce311 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/SheetsXlsxChartTest.php @@ -23,6 +23,7 @@ class SheetsXlsxChartTest extends TestCase $chart1 = $charts[0]; self::assertNotNull($chart1); $pa1 = $chart1->getPlotArea(); + self::assertNotNull($pa1); self::assertEquals(2, $pa1->getPlotSeriesCount()); $pg1 = $pa1->getPlotGroup()[0]; @@ -35,6 +36,7 @@ class SheetsXlsxChartTest extends TestCase $chart2 = $charts[1]; self::assertNotNull($chart2); $pa1 = $chart2->getPlotArea(); + self::assertNotNull($pa1); self::assertEquals(2, $pa1->getPlotSeriesCount()); $pg1 = $pa1->getPlotGroupByIndex(0); diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php index e1271b9a..3050fa72 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/XlsxTest.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Cell\DataValidation; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\File; @@ -15,6 +14,8 @@ use PHPUnit\Framework\TestCase; class XlsxTest extends TestCase { + const XLSX_PRECISION = 1.0E-8; + public function testLoadXlsxRowColumnAttributes(): void { $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; @@ -133,10 +134,10 @@ class XlsxTest extends TestCase $pageMargins = $worksheet->getPageMargins(); // Convert from inches to cm for testing - self::assertEquals(2.5, $pageMargins->getTop() * 2.54); - self::assertEquals(3.3, $pageMargins->getLeft() * 2.54); - self::assertEquals(3.3, $pageMargins->getRight() * 2.54); - self::assertEquals(1.3, $pageMargins->getHeader() * 2.54); + self::assertEqualsWithDelta(2.5, $pageMargins->getTop() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(3.3, $pageMargins->getLeft() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(3.3, $pageMargins->getRight() * 2.54, self::XLSX_PRECISION); + self::assertEqualsWithDelta(1.3, $pageMargins->getHeader() * 2.54, self::XLSX_PRECISION); self::assertEquals(PageSetup::PAPERSIZE_A4, $worksheet->getPageSetup()->getPaperSize()); self::assertEquals(['A10', 'A20', 'A30', 'A40', 'A50'], array_keys($worksheet->getBreaks())); @@ -161,42 +162,6 @@ class XlsxTest extends TestCase self::assertInstanceOf(Style::class, $conditionalRule->getStyle()); } - public function testLoadXlsxDataValidation(): void - { - $filename = 'tests/data/Reader/XLSX/dataValidationTest.xlsx'; - $reader = new Xlsx(); - $spreadsheet = $reader->load($filename); - - $worksheet = $spreadsheet->getActiveSheet(); - - self::assertTrue($worksheet->getCell('B3')->hasDataValidation()); - } - - /* - * Test for load drop down lists of another sheet. - * Pull #2150, issue #2149 - */ - public function testLoadXlsxDataValidationOfAnotherSheet(): void - { - $filename = 'tests/data/Reader/XLSX/dataValidation2Test.xlsx'; - $reader = new Xlsx(); - $spreadsheet = $reader->load($filename); - - $worksheet = $spreadsheet->getActiveSheet(); - - // same sheet - $validationCell = $worksheet->getCell('B5'); - self::assertTrue($validationCell->hasDataValidation()); - self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); - self::assertSame('$A$5:$A$7', $validationCell->getDataValidation()->getFormula1()); - - // another sheet - $validationCell = $worksheet->getCell('B14'); - self::assertTrue($validationCell->hasDataValidation()); - self::assertSame(DataValidation::TYPE_LIST, $validationCell->getDataValidation()->getType()); - self::assertSame('Feuil2!$A$3:$A$5', $validationCell->getDataValidation()->getFormula1()); - } - /** * Test load Xlsx file without cell reference. * diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php index 97476ed5..ae79dd0e 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/PageSetupTest.php @@ -14,19 +14,25 @@ class PageSetupTest extends TestCase private const MARGIN_UNIT_CONVERSION = 2.54; // Inches to cm /** - * @var Spreadsheet + * @var ?Spreadsheet */ private $spreadsheet; - protected function setup(): void + /** @var string */ + private $filename = 'tests/data/Reader/Xml/PageSetup.xml'; + + protected function tearDown(): void { - $filename = 'tests/data/Reader/Xml/PageSetup.xml'; - $reader = new Xml(); - $this->spreadsheet = $reader->load($filename); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } } public function testPageSetup(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageSetupAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { @@ -49,6 +55,8 @@ class PageSetupTest extends TestCase public function testPageMargins(): void { + $reader = new Xml(); + $this->spreadsheet = $reader->load($this->filename); $assertions = $this->pageMarginAssertions(); foreach ($this->spreadsheet->getAllSheets() as $worksheet) { diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php index 29d81299..9846b861 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php @@ -4,18 +4,51 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xml; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Reader\Xml; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class XmlLoadTest extends TestCase { - public function testLoad(): void + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var string */ + private $locale; + + protected function setUp(): void + { + $this->locale = Settings::getLocale(); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + Settings::setLocale($this->locale); + } + + public function testLoadEnglish(): void + { + $this->xtestLoad(); + } + + public function testLoadFrench(): void + { + Settings::setLocale('fr'); + $this->xtestLoad(); + } + + public function xtestLoad(): void { $filename = __DIR__ . '/../../../..' . '/samples/templates/excel2003.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); @@ -71,7 +104,7 @@ class XmlLoadTest extends TestCase $reader = new Xml(); $filter = new XmlFilter(); $reader->setReadFilter($filter); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(2, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(1); self::assertEquals('Report Data', $sheet->getTitle()); @@ -87,7 +120,7 @@ class XmlLoadTest extends TestCase . '/samples/templates/excel2003.xml'; $reader = new Xml(); $reader->setLoadSheetsOnly(['Unknown Sheet', 'Report Data']); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Report Data', $sheet->getTitle()); @@ -102,7 +135,7 @@ class XmlLoadTest extends TestCase . '/../../../..' . '/samples/templates/excel2003.short.bad.xml'; $reader = new Xml(); - $spreadsheet = $reader->load($filename); + $this->spreadsheet = $spreadsheet = $reader->load($filename); self::assertEquals(1, $spreadsheet->getSheetCount()); $sheet = $spreadsheet->getSheet(0); self::assertEquals('Sample Data', $sheet->getTitle()); diff --git a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php index 2640d80c..ac686a29 100644 --- a/tests/PhpSpreadsheetTests/ReferenceHelperTest.php +++ b/tests/PhpSpreadsheetTests/ReferenceHelperTest.php @@ -2,9 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; use PhpOffice\PhpSpreadsheet\Comment; +use PhpOffice\PhpSpreadsheet\NamedFormula; +use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard; @@ -311,6 +314,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('E7')->hasDataValidation()); + self::assertSame('E7', $sheet->getDataValidation('E7')->getSqref()); } public function testDeleteRowsWithDataValidation(): void @@ -326,6 +330,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('E3')->hasDataValidation()); + self::assertSame('E3', $sheet->getDataValidation('E3')->getSqref()); } public function testDeleteColumnsWithDataValidation(): void @@ -341,6 +346,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('C5')->hasDataValidation()); + self::assertSame('C5', $sheet->getDataValidation('C5')->getSqref()); } public function testInsertColumnsWithDataValidation(): void @@ -356,6 +362,7 @@ class ReferenceHelperTest extends TestCase self::assertFalse($sheet->getCell($cellAddress)->hasDataValidation()); self::assertTrue($sheet->getCell('G5')->hasDataValidation()); + self::assertSame('G5', $sheet->getDataValidation('G5')->getSqref()); } private function setDataValidation(Worksheet $sheet, string $cellAddress): void @@ -534,4 +541,110 @@ class ReferenceHelperTest extends TestCase $printArea = $sheet->getPageSetup()->getPrintArea(); self::assertSame('A1:H10', $printArea); } + + public function testInsertRowsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->insertNewRowBefore(4, 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A8', $firstColumn->getRange()); + self::assertSame('=Data!B$2:B8', $secondColumn->getRange()); + self::assertSame(30, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(25, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(750, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + public function testInsertColumnsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->insertNewColumnBefore('B', 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A6', $firstColumn->getRange()); + self::assertSame('=Data!D$2:D6', $secondColumn->getRange()); + self::assertSame(30, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(25, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(750, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + public function testDeleteRowsWithDefinedNames(): void + { + $spreadsheet = $this->buildDefinedNamesTestWorkbook(); + /** @var Worksheet $dataSheet */ + $dataSheet = $spreadsheet->getSheetByName('Data'); + /** @var Worksheet $totalsSheet */ + $totalsSheet = $spreadsheet->getSheetByName('Totals'); + + $dataSheet->removeRow(3, 2); + Calculation::getInstance($spreadsheet)->flushInstance(); + + /** @var NamedRange $firstColumn */ + $firstColumn = $spreadsheet->getNamedRange('FirstColumn'); + /** @var NamedRange $secondColumn */ + $secondColumn = $spreadsheet->getNamedRange('SecondColumn'); + + self::assertSame('=Data!$A$2:$A4', $firstColumn->getRange()); + self::assertSame('=Data!B$2:B4', $secondColumn->getRange()); + self::assertSame(20, $totalsSheet->getCell('A20')->getCalculatedValue()); + self::assertSame(17, $totalsSheet->getCell('B20')->getCalculatedValue()); + self::assertSame(340, $totalsSheet->getCell('D20')->getCalculatedValue()); + } + + private function buildDefinedNamesTestWorkbook(): Spreadsheet + { + $spreadsheet = new Spreadsheet(); + $dataSheet = $spreadsheet->getActiveSheet(); + $dataSheet->setTitle('Data'); + + $totalsSheet = $spreadsheet->addSheet(new Worksheet()); + $totalsSheet->setTitle('Totals'); + + $spreadsheet->setActiveSheetIndexByName('Data'); + + $dataSheet->fromArray([['Column 1', 'Column 2'], [2, 1], [4, 3], [6, 5], [8, 7], [10, 9]], null, 'A1', true); + + $spreadsheet->addNamedRange( + new NamedRange('FirstColumn', $spreadsheet->getActiveSheet(), '=Data!$A$2:$A6') + ); + $spreadsheet->addNamedFormula( + new NamedFormula('FirstTotal', $spreadsheet->getActiveSheet(), '=SUM(FirstColumn)') + ); + $totalsSheet->setCellValue('A20', '=FirstTotal'); + + $spreadsheet->addNamedRange( + new NamedRange('SecondColumn', $spreadsheet->getActiveSheet(), '=Data!B$2:B6') + ); + $spreadsheet->addNamedFormula( + new NamedFormula('SecondTotal', $spreadsheet->getActiveSheet(), '=SUM(SecondColumn)') + ); + $totalsSheet->setCellValue('B20', '=SecondTotal'); + + $spreadsheet->addNamedFormula( + new NamedFormula('ProductTotal', $spreadsheet->getActiveSheet(), '=FirstTotal*SecondTotal') + ); + $totalsSheet->setCellValue('D20', '=ProductTotal'); + + return $spreadsheet; + } } diff --git a/tests/PhpSpreadsheetTests/RichTextTest.php b/tests/PhpSpreadsheetTests/RichTextTest.php index e2dfcce7..e6f55514 100644 --- a/tests/PhpSpreadsheetTests/RichTextTest.php +++ b/tests/PhpSpreadsheetTests/RichTextTest.php @@ -28,7 +28,6 @@ class RichTextTest extends TestCase public function testTextElements(): void { $element1 = new TextElement('A'); - self::assertNull($element1->getFont()); $element2 = new TextElement('B'); $element3 = new TextElement('C'); $richText = new RichText(); diff --git a/tests/PhpSpreadsheetTests/Shared/FileTest.php b/tests/PhpSpreadsheetTests/Shared/FileTest.php index ddc54b5e..e65ec810 100644 --- a/tests/PhpSpreadsheetTests/Shared/FileTest.php +++ b/tests/PhpSpreadsheetTests/Shared/FileTest.php @@ -87,12 +87,14 @@ class FileTest extends TestCase public function testNotReadable(): void { - if (PHP_OS_FAMILY === 'Windows') { + if (PHP_OS_FAMILY === 'Windows' || stristr(PHP_OS, 'CYGWIN') !== false) { self::markTestSkipped('chmod does not work reliably on Windows'); } $this->tempfile = $temp = File::temporaryFileName(); file_put_contents($temp, ''); - chmod($temp, 0070); + if (chmod($temp, 0070) === false) { + self::markTestSkipped('chmod failed'); + } self::assertFalse(File::testFileNoThrow($temp)); $this->expectException(ReaderException::class); $this->expectExceptionMessage('for reading'); diff --git a/tests/PhpSpreadsheetTests/Shared/Font2Test.php b/tests/PhpSpreadsheetTests/Shared/Font2Test.php new file mode 100644 index 00000000..4bb247cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Font2Test.php @@ -0,0 +1,149 @@ +providerCharsetFromFontName(); + foreach ($tests as $test) { + $thisTest = $test[0]; + if (array_key_exists($thisTest, $covered)) { + $covered[$thisTest] = 1; + } else { + $defaultCovered = true; + } + } + foreach ($covered as $key => $val) { + self::assertEquals(1, $val, "FontName $key not tested"); + } + self::assertTrue($defaultCovered, 'Default key not tested'); + } + + public function providerCharsetFromFontName(): array + { + return [ + ['EucrosiaUPC', Font::CHARSET_ANSI_THAI], + ['Wingdings', Font::CHARSET_SYMBOL], + ['Wingdings 2', Font::CHARSET_SYMBOL], + ['Wingdings 3', Font::CHARSET_SYMBOL], + ['Default', Font::CHARSET_ANSI_LATIN], + ]; + } + + public function testColumnWidths(): void + { + $widths = Font::DEFAULT_COLUMN_WIDTHS; + $fontNames = ['Arial', 'Calibri', 'Verdana']; + $font = new StyleFont(); + foreach ($fontNames as $fontName) { + $font->setName($fontName); + $array = $widths[$fontName]; + foreach ($array as $points => $array2) { + $font->setSize($points); + $px = $array2['px']; + $width = $array2['width']; + self::assertEquals($px, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals($width, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + } + } + $pxCalibri11 = $widths['Calibri'][11]['px']; + $widthCalibri11 = $widths['Calibri'][11]['width']; + $fontName = 'unknown'; + $points = 11; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals($pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals($widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + $points = 22; + $font->setSize($points); + self::assertEquals(2 * $pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals(2 * $widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + $fontName = 'Arial'; + $points = 33; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $pxCalibri11, Font::getDefaultColumnWidthByFont($font, true), "$fontName $points px"); + self::assertEquals(3 * $widthCalibri11, Font::getDefaultColumnWidthByFont($font, false), "$fontName $points ooxml-units"); + } + + public function testRowHeights(): void + { + $heights = Font::DEFAULT_COLUMN_WIDTHS; + $fontNames = ['Arial', 'Calibri', 'Verdana']; + $font = new StyleFont(); + foreach ($fontNames as $fontName) { + $font->setName($fontName); + $array = $heights[$fontName]; + foreach ($array as $points => $array2) { + $font->setSize($points); + $height = $array2['height']; + self::assertEquals($height, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + } + } + $heightArial10 = $heights['Arial'][10]['height']; + $fontName = 'Arial'; + $points = 20; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(2 * $heightArial10, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $heightVerdana10 = $heights['Verdana'][10]['height']; + $fontName = 'Verdana'; + $points = 30; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $heightVerdana10, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $heightCalibri11 = $heights['Calibri'][11]['height']; + $fontName = 'Calibri'; + $points = 22; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(2 * $heightCalibri11, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + $fontName = 'unknown'; + $points = 33; + $font->setName($fontName); + $font->setSize($points); + self::assertEquals(3 * $heightCalibri11, Font::getDefaultRowHeightByFont($font), "$fontName $points points"); + } + + public function testGetTrueTypeFontFileFromFont(): void + { + $fileNames = Font::FONT_FILE_NAMES; + $font = new StyleFont(); + foreach ($fileNames as $fontName => $fontNameArray) { + $font->setName($fontName); + $font->setBold(false); + $font->setItalic(false); + self::assertSame($fileNames[$fontName]['x'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName not bold not italic"); + $font->setBold(true); + $font->setItalic(false); + self::assertSame($fileNames[$fontName]['xb'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName bold not italic"); + $font->setBold(false); + $font->setItalic(true); + self::assertSame($fileNames[$fontName]['xi'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName not bold italic"); + $font->setBold(true); + $font->setItalic(true); + self::assertSame($fileNames[$fontName]['xbi'], Font::getTrueTypeFontFileFromFont($font, false), "$fontName bold italic"); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Font3Test.php b/tests/PhpSpreadsheetTests/Shared/Font3Test.php new file mode 100644 index 00000000..e91e7acf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Font3Test.php @@ -0,0 +1,53 @@ +holdDirectory = Font::getTrueTypeFontPath(); + } + + protected function tearDown(): void + { + Font::setTrueTypeFontPath($this->holdDirectory); + } + + public function testGetTrueTypeException1(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Valid directory to TrueType Font files not specified'); + $font = new StyleFont(); + $font->setName('unknown'); + Font::getTrueTypeFontFileFromFont($font); + } + + public function testGetTrueTypeException2(): void + { + Font::setTrueTypeFontPath(__DIR__); + $this->expectException(SSException::class); + $this->expectExceptionMessage('Unknown font name'); + $font = new StyleFont(); + $font->setName('unknown'); + Font::getTrueTypeFontFileFromFont($font); + } + + public function testGetTrueTypeException3(): void + { + Font::setTrueTypeFontPath(__DIR__); + $this->expectException(SSException::class); + $this->expectExceptionMessage('TrueType Font file not found'); + $font = new StyleFont(); + $font->setName('Calibri'); + Font::getTrueTypeFontFileFromFont($font); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/FontTest.php b/tests/PhpSpreadsheetTests/Shared/FontTest.php index c733f8ee..2d5feb6e 100644 --- a/tests/PhpSpreadsheetTests/Shared/FontTest.php +++ b/tests/PhpSpreadsheetTests/Shared/FontTest.php @@ -8,6 +8,8 @@ use PHPUnit\Framework\TestCase; class FontTest extends TestCase { + const FONT_PRECISION = 1.0E-12; + public function testGetAutoSizeMethod(): void { $expectedResult = Font::AUTOSIZE_METHOD_APPROX; @@ -63,7 +65,7 @@ class FontTest extends TestCase public function testInchSizeToPixels($expectedResult, $size): void { $result = Font::inchSizeToPixels($size); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FONT_PRECISION); } public function providerInchSizeToPixels(): array @@ -80,7 +82,7 @@ class FontTest extends TestCase public function testCentimeterSizeToPixels($expectedResult, $size): void { $result = Font::centimeterSizeToPixels($size); - self::assertEquals($expectedResult, $result); + self::assertEqualsWithDelta($expectedResult, $result, self::FONT_PRECISION); } public function providerCentimeterSizeToPixels(): array diff --git a/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php b/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php index 4b7923d8..c9912b8c 100644 --- a/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php +++ b/tests/PhpSpreadsheetTests/Shared/PasswordHasherTest.php @@ -10,16 +10,27 @@ class PasswordHasherTest extends TestCase { /** * @dataProvider providerHashPassword - * - * @param mixed $expectedResult */ - public function testHashPassword($expectedResult, ...$args): void - { + public function testHashPassword( + string $expectedResult, + string $password, + ?string $algorithm = null, + ?string $salt = null, + ?int $spinCount = null + ): void { if ($expectedResult === 'exception') { $this->expectException(SpException::class); } - $result = PasswordHasher::hashPassword(...$args); - self::assertEquals($expectedResult, $result); + if ($algorithm === null) { + $result = PasswordHasher::hashPassword($password); + } elseif ($salt === null) { + $result = PasswordHasher::hashPassword($password, $algorithm); + } elseif ($spinCount === null) { + $result = PasswordHasher::hashPassword($password, $algorithm, $salt); + } else { + $result = PasswordHasher::hashPassword($password, $algorithm, $salt, $spinCount); + } + self::assertSame($expectedResult, $result); } public function providerHashPassword(): array diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php new file mode 100644 index 00000000..54679499 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperInvalidCharTest.php @@ -0,0 +1,44 @@ +getActiveSheet(); + $substitution = '�'; + $array = [ + ['Normal string', 'Hello', 'Hello'], + ['integer', 2, 2], + ['float', 2.1, 2.1], + ['boolean true', true, true], + ['illegal FFFE/FFFF', "H\xef\xbf\xbe\xef\xbf\xbfello", "H{$substitution}{$substitution}ello"], + ['illegal character', "H\xef\x00\x00ello", "H{$substitution}\x00\x00ello"], + ['overlong character', "H\xc0\xa0ello", "H{$substitution}{$substitution}ello"], + ['Osmanya as single character', "H\xf0\x90\x90\x80ello", 'H𐐀ello'], + ['Osmanya as surrogate pair (x)', "\xed\xa0\x81\xed\xb0\x80", "{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}"], + ['Osmanya as surrogate pair (u)', "\u{d801}\u{dc00}", "{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}{$substitution}"], + ['Half surrogate pair (u)', "\u{d801}", "{$substitution}{$substitution}{$substitution}"], + ['Control character', "\u{7}", "\u{7}"], + ]; + + $sheet->fromArray($array); + $row = 0; + foreach ($array as $value) { + self::assertSame($value[1] === $value[2], StringHelper::isUTF8((string) $value[1])); + ++$row; + $expected = $value[2]; + self::assertSame( + $expected, + $sheet->getCell("B$row")->getValue(), + $sheet->getCell("A$row")->getValue() + ); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php index bcbcd5d2..85a92613 100644 --- a/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperTest.php @@ -119,4 +119,33 @@ class StringHelperTest extends TestCase self::assertEquals($expectedResult, $result); } + + /** + * @dataProvider providerFractions + */ + public function testFraction(string $expected, string $value): void + { + $originalValue = $value; + $result = StringHelper::convertToNumberIfFraction($value); + if ($result === false) { + self::assertSame($expected, $originalValue); + self::assertSame($expected, $value); + } else { + self::assertSame($expected, (string) $value); + self::assertNotEquals($value, $originalValue); + } + } + + public function providerFractions(): array + { + return [ + 'non-fraction' => ['1', '1'], + 'common fraction' => ['1.5', '1 1/2'], + 'fraction between -1 and 0' => ['-0.5', '-1/2'], + 'fraction between -1 and 0 with space' => ['-0.5', ' - 1/2'], + 'fraction between 0 and 1' => ['0.75', '3/4 '], + 'fraction between 0 and 1 with space' => ['0.75', ' 3/4'], + 'improper fraction' => ['1.75', '7/4'], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php index 9ada87a5..34321227 100644 --- a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php +++ b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php @@ -7,6 +7,8 @@ use PHPUnit\Framework\TestCase; class LinearBestFitTest extends TestCase { + const LBF_PRECISION = 1.0E-8; + /** * @dataProvider providerLinearBestFit * @@ -27,13 +29,13 @@ class LinearBestFitTest extends TestCase ): void { $bestFit = new LinearBestFit($yValues, $xValues); $slope = $bestFit->getSlope(1); - self::assertEquals($expectedSlope[0], $slope); + self::assertEqualsWithDelta($expectedSlope[0], $slope, self::LBF_PRECISION); $slope = $bestFit->getSlope(); - self::assertEquals($expectedSlope[1], $slope); + self::assertEqualsWithDelta($expectedSlope[1], $slope, self::LBF_PRECISION); $intersect = $bestFit->getIntersect(1); - self::assertEquals($expectedIntersect[0], $intersect); + self::assertEqualsWithDelta($expectedIntersect[0], $intersect, self::LBF_PRECISION); $intersect = $bestFit->getIntersect(); - self::assertEquals($expectedIntersect[1], $intersect); + self::assertEqualsWithDelta($expectedIntersect[1], $intersect, self::LBF_PRECISION); $equation = $bestFit->getEquation(2); self::assertEquals($expectedEquation, $equation); diff --git a/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php new file mode 100644 index 00000000..a455a7d2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetCoverageTest.php @@ -0,0 +1,208 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->spreadsheet2 !== null) { + $this->spreadsheet2->disconnectWorksheets(); + $this->spreadsheet2 = null; + } + } + + public function testDocumentProperties(): void + { + $this->spreadsheet = new Spreadsheet(); + $properties = $this->spreadsheet->getProperties(); + $properties->setCreator('Anyone'); + $properties->setTitle('Description'); + $this->spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($properties, $this->spreadsheet2->getProperties()); + $properties2 = clone $properties; + $this->spreadsheet2->setProperties($properties2); + self::assertEquals($properties, $this->spreadsheet2->getProperties()); + } + + public function testDocumentSecurity(): void + { + $this->spreadsheet = new Spreadsheet(); + $security = $this->spreadsheet->getSecurity(); + $security->setLockRevision(true); + $revisionsPassword = 'revpasswd'; + $security->setRevisionsPassword($revisionsPassword); + $this->spreadsheet2 = new Spreadsheet(); + self::assertNotEquals($security, $this->spreadsheet2->getSecurity()); + $security2 = clone $security; + $this->spreadsheet2->setSecurity($security2); + self::assertEquals($security, $this->spreadsheet2->getSecurity()); + } + + public function testCellXfCollection(): void + { + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $collection = $this->spreadsheet->getCellXfCollection(); + self::assertCount(4, $collection); + $font1Style = $collection[1]; + self::assertTrue($this->spreadsheet->cellXfExists($font1Style)); + self::assertSame('font1', $this->spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + + $this->spreadsheet->removeCellXfByIndex(1); + self::assertFalse($this->spreadsheet->cellXfExists($font1Style)); + self::assertSame('font2', $this->spreadsheet->getCellXfCollection()[1]->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('Calibri', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + } + + public function testInvalidRemoveCellXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellXf index is out of bounds.'); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $this->spreadsheet->removeCellXfByIndex(5); + } + + public function testInvalidRemoveDefaultStyle(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('No default style found for this workbook'); + // Removing default style probably should be disallowed. + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->removeCellXfByIndex(0); + $this->spreadsheet->getDefaultStyle(); + } + + public function testCellStyleXF(): void + { + $this->spreadsheet = new Spreadsheet(); + $collection = $this->spreadsheet->getCellStyleXfCollection(); + self::assertCount(1, $collection); + $styleXf = $collection[0]; + self::assertSame($styleXf, $this->spreadsheet->getCellStyleXfByIndex(0)); + $hash = $styleXf->getHashCode(); + self::assertSame($styleXf, $this->spreadsheet->getCellStyleXfByHashCode($hash)); + self::assertFalse($this->spreadsheet->getCellStyleXfByHashCode($hash . 'x')); + } + + public function testInvalidRemoveCellStyleXfByIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('CellStyleXf index is out of bounds.'); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->removeCellStyleXfByIndex(5); + } + + public function testInvalidFirstSheetIndex(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('First sheet index must be a positive integer.'); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setFirstSheetIndex(-1); + } + + public function testInvalidVisibility(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Invalid visibility value.'); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setVisibility(Spreadsheet::VISIBILITY_HIDDEN); + self::assertSame(Spreadsheet::VISIBILITY_HIDDEN, $this->spreadsheet->getVisibility()); + $this->spreadsheet->setVisibility(null); + self::assertSame(Spreadsheet::VISIBILITY_VISIBLE, $this->spreadsheet->getVisibility()); + $this->spreadsheet->setVisibility('badvalue'); + } + + public function testInvalidTabRatio(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Tab ratio must be between 0 and 1000.'); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet->setTabRatio(2000); + } + + public function testCopy(): void + { + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getStyle('A1')->getFont()->setName('font1'); + $sheet->getStyle('A2')->getFont()->setName('font2'); + $sheet->getStyle('A3')->getFont()->setName('font3'); + $sheet->getStyle('B1')->getFont()->setName('font1'); + $sheet->getStyle('B2')->getFont()->setName('font2'); + $sheet->getCell('A1')->setValue('this is a1'); + $sheet->getCell('A2')->setValue('this is a2'); + $sheet->getCell('A3')->setValue('this is a3'); + $sheet->getCell('B1')->setValue('this is b1'); + $sheet->getCell('B2')->setValue('this is b2'); + $this->spreadsheet2 = $this->spreadsheet->copy(); + $copysheet = $this->spreadsheet2->getActiveSheet(); + $copysheet->getStyle('A2')->getFont()->setName('font12'); + $copysheet->getCell('A2')->setValue('this was a2'); + + self::assertSame('font1', $sheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $sheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $sheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $sheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $sheet->getCell('A1')->getValue()); + self::assertSame('this is a2', $sheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $sheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $sheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $sheet->getCell('B2')->getValue()); + + self::assertSame('font1', $copysheet->getStyle('A1')->getFont()->getName()); + self::assertSame('font12', $copysheet->getStyle('A2')->getFont()->getName()); + self::assertSame('font3', $copysheet->getStyle('A3')->getFont()->getName()); + self::assertSame('font1', $copysheet->getStyle('B1')->getFont()->getName()); + self::assertSame('font2', $copysheet->getStyle('B2')->getFont()->getName()); + self::assertSame('this is a1', $copysheet->getCell('A1')->getValue()); + self::assertSame('this was a2', $copysheet->getCell('A2')->getValue()); + self::assertSame('this is a3', $copysheet->getCell('A3')->getValue()); + self::assertSame('this is b1', $copysheet->getCell('B1')->getValue()); + self::assertSame('this is b2', $copysheet->getCell('B2')->getValue()); + } + + public function testClone(): void + { + $this->expectException(SSException::class); + $this->expectExceptionMessage('Do not use clone on spreadsheet. Use spreadsheet->copy() instead.'); + $this->spreadsheet = new Spreadsheet(); + $this->spreadsheet2 = clone $this->spreadsheet; + } +} diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index 11fb56e4..76e28b3d 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -2,28 +2,38 @@ namespace PhpOffice\PhpSpreadsheetTests; +use PhpOffice\PhpSpreadsheet\Exception as ssException; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class SpreadsheetTest extends TestCase { - /** @var Spreadsheet */ - private $object; + /** @var ?Spreadsheet */ + private $spreadsheet; - protected function setUp(): void + protected function tearDown(): void { - parent::setUp(); - $this->object = new Spreadsheet(); - $sheet = $this->object->getActiveSheet(); + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + private function getSpreadsheet(): Spreadsheet + { + $this->spreadsheet = $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('someSheet1'); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); $sheet = new Worksheet(); $sheet->setTitle('someSheet 3'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); + + return $spreadsheet; } public function dataProviderForSheetNames(): array @@ -35,6 +45,7 @@ class SpreadsheetTest extends TestCase [1, "'someSheet2'"], [2, 'someSheet 3'], [2, "'someSheet 3'"], + [null, 'someSheet 33'], ]; return $array; @@ -43,135 +54,153 @@ class SpreadsheetTest extends TestCase /** * @dataProvider dataProviderForSheetNames */ - public function testGetSheetByName(int $index, string $sheetName): void + public function testGetSheetByName(?int $index, string $sheetName): void { - self::assertSame($this->object->getSheet($index), $this->object->getSheetByName($sheetName)); + $spreadsheet = $this->getSpreadsheet(); + if ($index === null) { + self::assertNull($spreadsheet->getSheetByName($sheetName)); + } else { + self::assertSame($spreadsheet->getSheet($index), $spreadsheet->getSheetByName($sheetName)); + } } public function testAddSheetDuplicateTitle(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); $sheet = new Worksheet(); $sheet->setTitle('someSheet2'); - $this->object->addSheet($sheet); + $spreadsheet->addSheet($sheet); } public function testAddSheetNoAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->addSheet($sheet); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); } public function testAddSheetAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet0'); - $this->object->addSheet($sheet, 0); - self::assertEquals(3, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet, 0); + self::assertEquals(3, $spreadsheet->getActiveSheetIndex()); } public function testRemoveSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->removeSheetByIndex(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->removeSheetByIndex(4); } public function testRemoveSheetNoAdjustActive(): void { - $this->object->setActiveSheetIndex(1); - self::assertEquals(1, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(2); - self::assertEquals(1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(1); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(2); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); } public function testRemoveSheetAdjustActive(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(1); - self::assertEquals(1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(1); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); } public function testGetSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->getSheet(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->getSheet(4); } public function testGetIndexNonExistent(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->getIndex($sheet); + $spreadsheet->getIndex($sheet); } public function testSetIndexByName(): void { - $this->object->setIndexByName('someSheet1', 1); - self::assertEquals('someSheet2', $this->object->getSheet(0)->getTitle()); - self::assertEquals('someSheet1', $this->object->getSheet(1)->getTitle()); - self::assertEquals('someSheet 3', $this->object->getSheet(2)->getTitle()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setIndexByName('someSheet1', 1); + self::assertEquals('someSheet2', $spreadsheet->getSheet(0)->getTitle()); + self::assertEquals('someSheet1', $spreadsheet->getSheet(1)->getTitle()); + self::assertEquals('someSheet 3', $spreadsheet->getSheet(2)->getTitle()); } public function testRemoveAllSheets(): void { - $this->object->setActiveSheetIndex(2); - self::assertEquals(2, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(1, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(0, $this->object->getActiveSheetIndex()); - $this->object->removeSheetByIndex(0); - self::assertEquals(-1, $this->object->getActiveSheetIndex()); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet->setActiveSheetIndex(2); + self::assertEquals(2, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); + $spreadsheet->removeSheetByIndex(0); + self::assertEquals(-1, $spreadsheet->getActiveSheetIndex()); $sheet = new Worksheet(); $sheet->setTitle('someSheet4'); - $this->object->addSheet($sheet); - self::assertEquals(0, $this->object->getActiveSheetIndex()); + $spreadsheet->addSheet($sheet); + self::assertEquals(0, $spreadsheet->getActiveSheetIndex()); } public function testBug1735(): void { - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $spreadsheet->createSheet()->setTitle('addedsheet'); - $spreadsheet->setActiveSheetIndex(1); - $spreadsheet->removeSheetByIndex(0); - $sheet = $spreadsheet->getActiveSheet(); + $spreadsheet1 = new Spreadsheet(); + $spreadsheet1->createSheet()->setTitle('addedsheet'); + $spreadsheet1->setActiveSheetIndex(1); + $spreadsheet1->removeSheetByIndex(0); + $sheet = $spreadsheet1->getActiveSheet(); self::assertEquals('addedsheet', $sheet->getTitle()); } public function testSetActiveSheetIndexTooHigh(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->setActiveSheetIndex(4); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->setActiveSheetIndex(4); } public function testSetActiveSheetNoSuchName(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $this->object->setActiveSheetIndexByName('unknown'); + $spreadsheet = $this->getSpreadsheet(); + $this->expectException(ssException::class); + $spreadsheet->setActiveSheetIndexByName('unknown'); } public function testAddExternal(): void { - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $sheet = $spreadsheet->createSheet()->setTitle('someSheet19'); + $spreadsheet = $this->getSpreadsheet(); + $spreadsheet1 = new Spreadsheet(); + $sheet = $spreadsheet1->createSheet()->setTitle('someSheet19'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); $sheet->getCell('B1')->getStyle()->getFont()->setSuperscript(true); $sheet->getCell('C1')->getStyle()->getFont()->setSubscript(true); - self::assertCount(4, $spreadsheet->getCellXfCollection()); + self::assertCount(4, $spreadsheet1->getCellXfCollection()); self::assertEquals(1, $sheet->getCell('A1')->getXfIndex()); - $this->object->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); - self::assertCount(2, $this->object->getCellXfCollection()); - $sheet3 = $this->object->addExternalSheet($sheet); - self::assertCount(6, $this->object->getCellXfCollection()); + $spreadsheet->getActiveSheet()->getCell('A1')->getStyle()->getFont()->setBold(true); + self::assertCount(2, $spreadsheet->getCellXfCollection()); + $sheet3 = $spreadsheet->addExternalSheet($sheet); + self::assertCount(6, $spreadsheet->getCellXfCollection()); self::assertEquals('someSheet19', $sheet3->getTitle()); self::assertEquals(1, $sheet3->getCell('A1')->getValue()); self::assertTrue($sheet3->getCell('A1')->getStyle()->getFont()->getBold()); @@ -181,17 +210,17 @@ class SpreadsheetTest extends TestCase public function testAddExternalDuplicateName(): void { - $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $this->expectException(ssException::class); + $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->createSheet()->setTitle('someSheet1'); $sheet->getCell('A1')->setValue(1); $sheet->getCell('A1')->getStyle()->getFont()->setBold(true); - $this->object->addExternalSheet($sheet); + $spreadsheet->addExternalSheet($sheet); } public function testAddExternalColumnDimensionStyles(): void { - $spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet1 = new Spreadsheet(); $sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension'); $sheet1->getCell('A1')->setValue(1); $sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true); @@ -200,7 +229,7 @@ class SpreadsheetTest extends TestCase self::assertEquals(1, $index); self::assertCount(2, $spreadsheet1->getCellXfCollection()); - $spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet2 = new Spreadsheet(); $sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles'); $sheet2->getCell('A1')->setValue(1); $sheet2->getCell('A1')->getStyle()->getFont()->setBold(true); @@ -220,7 +249,7 @@ class SpreadsheetTest extends TestCase public function testAddExternalRowDimensionStyles(): void { - $spreadsheet1 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet1 = new Spreadsheet(); $sheet1 = $spreadsheet1->createSheet()->setTitle('sheetWithColumnDimension'); $sheet1->getCell('A1')->setValue(1); $sheet1->getCell('A1')->getStyle()->getFont()->setItalic(true); @@ -229,7 +258,7 @@ class SpreadsheetTest extends TestCase self::assertEquals(1, $index); self::assertCount(2, $spreadsheet1->getCellXfCollection()); - $spreadsheet2 = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet2 = new Spreadsheet(); $sheet2 = $spreadsheet2->createSheet()->setTitle('sheetWithTwoStyles'); $sheet2->getCell('A1')->setValue(1); $sheet2->getCell('A1')->getStyle()->getFont()->setBold(true); diff --git a/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php b/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php new file mode 100644 index 00000000..1d260a6e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Style/AlignmentMiddleTest.php @@ -0,0 +1,87 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->outputFileName !== '') { + unlink($this->outputFileName); + $this->outputFileName = ''; + } + } + + public function testCenterWriteHtml(): void + { + // Html Writer changes vertical align center to middle + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + $writer = new HTML($this->spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('vertical-align:middle', $html); + self::assertStringNotContainsString('vertical-align:center', $html); + } + + public function testCenterWriteXlsx(): void + { + // Xlsx Writer uses vertical align center unchanged + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical(Alignment::VERTICAL_CENTER); + $this->outputFileName = File::temporaryFilename(); + $writer = new Xlsx($this->spreadsheet); + $writer->save($this->outputFileName); + $zip = new ZipArchive(); + $zip->open($this->outputFileName); + $html = $zip->getFromName('xl/styles.xml'); + $zip->close(); + self::assertStringContainsString('vertical="center"', $html); + self::assertStringNotContainsString('vertical="middle"', $html); + } + + public function testCenterWriteXlsx2(): void + { + // Xlsx Writer changes vertical align middle to center + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue('Cell1'); + $sheet->getStyle('A1') + ->getAlignment() + ->setVertical('middle'); + $this->outputFileName = File::temporaryFilename(); + $writer = new Xlsx($this->spreadsheet); + $writer->save($this->outputFileName); + $zip = new ZipArchive(); + $zip->open($this->outputFileName); + $html = $zip->getFromName('xl/styles.xml'); + $zip->close(); + self::assertStringContainsString('vertical="center"', $html); + self::assertStringNotContainsString('vertical="middle"', $html); + } +} diff --git a/tests/PhpSpreadsheetTests/Style/AlignmentTest.php b/tests/PhpSpreadsheetTests/Style/AlignmentTest.php index d10e3211..6f50ce22 100644 --- a/tests/PhpSpreadsheetTests/Style/AlignmentTest.php +++ b/tests/PhpSpreadsheetTests/Style/AlignmentTest.php @@ -9,10 +9,21 @@ use PHPUnit\Framework\TestCase; class AlignmentTest extends TestCase { + /** @var ?Spreadsheet */ + private $spreadsheet; + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + public function testAlignment(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(45); @@ -31,8 +42,8 @@ class AlignmentTest extends TestCase public function testRotationTooHigh(): void { $this->expectException(PhpSpreadsheetException::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(91); @@ -42,8 +53,8 @@ class AlignmentTest extends TestCase public function testRotationTooLow(): void { $this->expectException(PhpSpreadsheetException::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('Cell1'); $cell1->getStyle()->getAlignment()->setTextRotation(-91); @@ -52,8 +63,8 @@ class AlignmentTest extends TestCase public function testHorizontal(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('X'); $cell1->getStyle()->getAlignment()->setHorizontal(Alignment::HORIZONTAL_LEFT)->setIndent(1); @@ -74,8 +85,8 @@ class AlignmentTest extends TestCase public function testReadOrder(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); $cell1 = $sheet->getCell('A1'); $cell1->setValue('ABC'); $cell1->getStyle()->getAlignment()->setReadOrder(0); diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php index 3c96403b..decbad5b 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/CellMatcherTest.php @@ -2,23 +2,45 @@ namespace PhpOffice\PhpSpreadsheetTests\Style\ConditionalFormatting; +use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Exception as ssException; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellMatcher; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; class CellMatcherTest extends TestCase { /** - * @var Spreadsheet + * @var ?Spreadsheet */ protected $spreadsheet; - protected function setUp(): void + protected function loadSpreadsheet(): Spreadsheet { $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); - $this->spreadsheet = $reader->load($filename); + + return $reader->load($filename); + } + + protected function tearDown(): void + { + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + private function confirmString(Worksheet $worksheet, Cell $cell, string $cellAddress): string + { + $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()) ?? ''; + if ($cfRange === '') { + self::fail("{$cellAddress} is not in a Conditional Format range"); + } + + return $cfRange; } /** @@ -26,16 +48,11 @@ class CellMatcherTest extends TestCase */ public function testBasicCellIsComparison(string $sheetname, string $cellAddress, array $expectedMatches): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -77,21 +94,35 @@ class CellMatcherTest extends TestCase ]; } + public function testNotInRange(): void + { + $this->spreadsheet = $this->loadSpreadsheet(); + $sheetname = 'cellIs Comparison'; + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); + $cell = $worksheet->getCell('J20'); + + $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); + self::assertNull($cfRange); + } + + public function testUnknownSheet(): void + { + $this->expectException(ssException::class); + $this->spreadsheet = $this->loadSpreadsheet(); + $sheetname = 'cellIs Comparisonxxx'; + $this->spreadsheet->getSheetByNameOrThrow($sheetname); + } + /** * @dataProvider rangeCellIsComparisonDataProvider */ public function testRangeCellIsComparison(string $sheetname, string $cellAddress, bool $expectedMatch): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -128,16 +159,11 @@ class CellMatcherTest extends TestCase */ public function testCellIsMultipleExpression(string $sheetname, string $cellAddress, array $expectedMatches): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -167,16 +193,11 @@ class CellMatcherTest extends TestCase */ public function testCellIsExpression(string $sheetname, string $cellAddress, bool $expectedMatch): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -216,16 +237,11 @@ class CellMatcherTest extends TestCase */ public function testTextExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -329,16 +345,11 @@ class CellMatcherTest extends TestCase */ public function testBlankExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -365,16 +376,11 @@ class CellMatcherTest extends TestCase */ public function testErrorExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -400,16 +406,11 @@ class CellMatcherTest extends TestCase */ public function testDateOccurringExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -447,16 +448,11 @@ class CellMatcherTest extends TestCase */ public function testDuplicatesExpressions(string $sheetname, string $cellAddress, array $expectedMatches): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyles = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); @@ -486,16 +482,11 @@ class CellMatcherTest extends TestCase */ public function testCrossWorksheetExpressions(string $sheetname, string $cellAddress, bool $expectedMatch): void { - $worksheet = $this->spreadsheet->getSheetByName($sheetname); - if ($worksheet === null) { - self::markTestSkipped("{$sheetname} not found in test workbook"); - } + $this->spreadsheet = $this->loadSpreadsheet(); + $worksheet = $this->spreadsheet->getSheetByNameOrThrow($sheetname); $cell = $worksheet->getCell($cellAddress); - $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); - if ($cfRange === null) { - self::markTestSkipped("{$cellAddress} is not in a Conditional Format range"); - } + $cfRange = $this->confirmString($worksheet, $cell, $cellAddress); $cfStyle = $worksheet->getConditionalStyles($cell->getCoordinate()); $matcher = new CellMatcher($cell, $cfRange); diff --git a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/DateValueWizardTest.php b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/DateValueWizardTest.php index 06d76bb2..4bbda920 100644 --- a/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/DateValueWizardTest.php +++ b/tests/PhpSpreadsheetTests/Style/ConditionalFormatting/Wizard/DateValueWizardTest.php @@ -1,6 +1,6 @@ $expectedWizard + * @psalm-param class-string $expectedWizard */ - public function testBasicWizardFactory(string $ruleType, $expectedWizard): void + public function testBasicWizardFactory(string $ruleType, string $expectedWizard): void { $wizard = $this->wizardFactory->newRule($ruleType); self::assertInstanceOf($expectedWizard, $wizard); @@ -54,10 +54,7 @@ class WizardFactoryTest extends TestCase $filename = 'tests/data/Style/ConditionalFormatting/CellMatcher.xlsx'; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($filename); - $worksheet = $spreadsheet->getSheetByName($sheetName); - if ($worksheet === null) { - self::markTestSkipped("{$sheetName} not found in test workbook"); - } + $worksheet = $spreadsheet->getSheetByNameOrThrow($sheetName); $cell = $worksheet->getCell($cellAddress); $cfRange = $worksheet->getConditionalRange($cell->getCoordinate()); diff --git a/tests/PhpSpreadsheetTests/Style/FontTest.php b/tests/PhpSpreadsheetTests/Style/FontTest.php index 02814afa..6cd4d950 100644 --- a/tests/PhpSpreadsheetTests/Style/FontTest.php +++ b/tests/PhpSpreadsheetTests/Style/FontTest.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Style; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Font; use PHPUnit\Framework\TestCase; class FontTest extends TestCase @@ -88,4 +89,20 @@ class FontTest extends TestCase self::assertEquals('Calibri', $font->getName(), 'Null string changed to default'); $spreadsheet->disconnectWorksheets(); } + + public function testUnderlineHash(): void + { + $font1 = new Font(); + $font2 = new Font(); + $font2aHash = $font2->getHashCode(); + self::assertSame($font1->getHashCode(), $font2aHash); + $font2->setUnderlineColor( + [ + 'type' => 'srgbClr', + 'value' => 'FF0000', + ] + ); + $font2bHash = $font2->getHashCode(); + self::assertNotEquals($font1->getHashCode(), $font2bHash); + } } diff --git a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php index e386b292..f09c34d7 100644 --- a/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php +++ b/tests/PhpSpreadsheetTests/Style/NumberFormatTest.php @@ -47,7 +47,7 @@ class NumberFormatTest extends TestCase public function testFormatValueWithMask($expectedResult, ...$args): void { $result = NumberFormat::toFormattedString(...$args); - self::assertEquals($expectedResult, $result); + self::assertSame($expectedResult, $result); } public function providerNumberFormat(): array diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php index a80d3d63..ebec3686 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterTest.php @@ -65,6 +65,7 @@ class AutoFilterTest extends SetupTeardown $ranges = [ 'G1:J512' => "$title!G1:J512", 'K1:N20' => 'K1:N20', + 'B10' => 'B10', ]; foreach ($ranges as $actualRange => $fullRange) { @@ -94,11 +95,22 @@ class AutoFilterTest extends SetupTeardown self::assertEquals($expectedResult, $result); } - public function testSetRangeInvalidRange(): void + public function testSetRangeInvalidRowRange(): void { $this->expectException(PhpSpreadsheetException::class); - $expectedResult = 'A1'; + $expectedResult = '999'; + + $sheet = $this->getSheet(); + $autoFilter = $sheet->getAutoFilter(); + $autoFilter->setRange($expectedResult); + } + + public function testSetRangeInvalidColumnRange(): void + { + $this->expectException(PhpSpreadsheetException::class); + + $expectedResult = 'ABC'; $sheet = $this->getSheet(); $autoFilter = $sheet->getAutoFilter(); @@ -498,4 +510,26 @@ class AutoFilterTest extends SetupTeardown self::assertArrayHasKey('K', $columns); self::assertArrayHasKey('M', $columns); } + + public function testAutoExtendRange(): void + { + $spreadsheet = $this->getSpreadsheet(); + $worksheet = $spreadsheet->addSheet(new Worksheet($spreadsheet, 'Autosized AutoFilter')); + + $worksheet->getCell('A1')->setValue('Col 1'); + $worksheet->getCell('B1')->setValue('Col 2'); + + $worksheet->setAutoFilter('A1:B1'); + $lastRow = $worksheet->getAutoFilter()->autoExtendRange(1, 1); + self::assertSame(1, $lastRow, 'No data below AutoFilter, so there should ne no resize'); + + $lastRow = $worksheet->getAutoFilter()->autoExtendRange(1, 999); + self::assertSame(999, $lastRow, 'Filter range is already correctly sized'); + + $data = [['A', 'A'], ['B', 'A'], ['A', 'B'], ['C', 'B'], ['B', null], [null, null], ['D', 'D'], ['E', 'E']]; + $worksheet->fromArray($data, null, 'A2', true); + + $lastRow = $worksheet->getAutoFilter()->autoExtendRange(1, 1); + self::assertSame(6, $lastRow, 'Filter range has been re-sized incorrectly'); + } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php index 1a7ff675..02b96426 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnCellIteratorTest.php @@ -40,6 +40,7 @@ class ColumnCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $ColumnCell) { self::assertNotNull($ColumnCell); + /** @scrutinizer ignore-call */ $values[] = $ColumnCell->getValue(); self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); @@ -60,6 +61,7 @@ class ColumnCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $ColumnCell) { self::assertNotNull($ColumnCell); + /** @scrutinizer ignore-call */ $values[] = $ColumnCell->getValue(); self::assertEquals($ColumnCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $ColumnCell); @@ -81,6 +83,7 @@ class ColumnCellIteratorTest extends TestCase while ($iterator->valid()) { $current = $iterator->current(); self::assertNotNull($current); + /** @scrutinizer ignore-call */ $cell = $current->getCoordinate(); $values[] = $sheet->getCell($cell)->getValue(); $iterator->prev(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php b/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php new file mode 100644 index 00000000..beb87028 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/ColumnIteratorEmptyTest.php @@ -0,0 +1,154 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('D2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('E2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('E3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('F2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('F3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('H3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H4', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyColumnBasic + */ + public function testIteratorEmptyColumn(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnBasic(): array + { + return [ + ['A', false], + ['B', true], + ['C', false], + ['D', false], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnNullAsEmpty + */ + public function testIteratorEmptyColumnWithNull(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnNullAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', false], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnEmptyStringAsEmpty + */ + public function testIteratorEmptyColumnWithEmptyString(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnEmptyStringAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', false], + ['D', true], + ['E', false], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } + + /** + * @dataProvider emptyColumnNullAndEmptyStringAsEmpty + */ + public function testIteratorEmptyColumnWithNullAndEmptyString(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new ColumnIterator($sheet, 'A', 'I'); + $iterator->seek($columnId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty( + CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + ); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnNullAndEmptyStringAsEmpty(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', true], + ['E', true], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php new file mode 100644 index 00000000..68c9d87a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/MergeBehaviourTest.php @@ -0,0 +1,166 @@ +getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsDefaultBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviour(): void + { + $expectedResult = [ + [1.1, 2.2, 3.3], + [4.4, 5.5, 9.9], + [5.5, 7.7, 13.2], + ]; + + $mergeRange = 'A1:C3'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataRaw, null, 'A1', true); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function testMergeCellsHideBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19', '2022-09-15'], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_HIDE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + /** + * @dataProvider mergeCellsMergeBehaviourProvider + */ + public function testMergeCellsMergeBehaviour(array $testData, string $mergeRange, array $expectedResult): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($testData, null, 'A1', true); + // Force a precalculation to populate the calculation cache, so that we can verify that it is being cleared + $worksheet->toArray(); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } + + public function mergeCellsMergeBehaviourProvider(): array + { + return [ + 'With Calculated Values' => [ + $this->testDataRaw, + 'A1:C3', + [ + ['1.1 2.2 1.1 4.4 5.5 0 1.1 0 0', null, null], + [null, null, null], + [null, null, null], + ], + ], + 'With Empty Cells' => [ + [ + [1, '', 2], + [null, 3, null], + [4, null, 5], + ], + 'A1:C3', + [ + ['1 2 3 4 5', null, null], + [null, null, null], + [null, null, null], + ], + ], + [ + [ + [12, '=5+1', '=A1/A2'], + ], + 'A1:C1', + [ + ['12 6 #DIV/0!', null, null], + ], + ], + ]; + } + + public function testMergeCellsMergeBehaviourFormatted(): void + { + $expectedResult = [ + ['1960-12-19 2022-09-15', null], + ]; + + $mergeRange = 'A1:B1'; + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->fromArray($this->testDataFormatted, null, 'A1', true); + $worksheet->getStyle($mergeRange)->getNumberFormat()->setFormatCode('yyyy-mm-dd'); + $worksheet->mergeCells($mergeRange, Worksheet::MERGE_CELL_CONTENT_MERGE); + + $mergeResult = $worksheet->toArray(null, true, true, false); + self::assertSame($expectedResult, $mergeResult); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php index b0606804..9afda3cb 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/RowCellIteratorTest.php @@ -40,6 +40,7 @@ class RowCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $RowCell) { self::assertNotNull($RowCell); + /** @scrutinizer ignore-call */ $values[] = $RowCell->getValue(); self::assertEquals($RowCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $RowCell); @@ -59,6 +60,7 @@ class RowCellIteratorTest extends TestCase $values = []; foreach ($iterator as $key => $RowCell) { self::assertNotNull($RowCell); + /** @scrutinizer ignore-call */ $values[] = $RowCell->getValue(); self::assertEquals($RowCellIndexResult++, $key); self::assertInstanceOf(Cell::class, $RowCell); @@ -80,6 +82,7 @@ class RowCellIteratorTest extends TestCase while ($iterator->valid()) { $current = $iterator->current(); self::assertNotNull($current); + /** @scrutinizer ignore-call */ $cell = $current->getCoordinate(); $values[] = $sheet->getCell($cell)->getValue(); $iterator->prev(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php b/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php new file mode 100644 index 00000000..9bee6b59 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/RowIteratorEmptyTest.php @@ -0,0 +1,154 @@ +getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B3', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('B4', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B5', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C5', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B6', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C6', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B7', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C7', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B8', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C8', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('D8', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyRowBasic + */ + public function testIteratorEmptyRow(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowBasic(): array + { + return [ + [1, false], + [2, true], + [3, false], + [4, false], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowNullAsEmpty + */ + public function testIteratorEmptyRowWithNull(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowNullAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, false], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowEmptyStringAsEmpty + */ + public function testIteratorEmptyRowWithEmptyString(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty(CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowEmptyStringAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, false], + [4, true], + [5, false], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyRowNullAndEmptyStringAsEmpty + */ + public function testIteratorEmptyRowWithNullAndEmptyString(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheet($spreadsheet); + $iterator = new RowIterator($sheet, 1, 9); + $iterator->seek($rowId); + $row = $iterator->current(); + $isEmpty = $row->isEmpty( + CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + ); + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowNullAndEmptyStringAsEmpty(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, true], + [5, true], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php index b367583b..506e753c 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetNamedRangesTest.php @@ -29,6 +29,15 @@ class WorksheetNamedRangesTest extends TestCase self::assertTrue($cellExists); } + public function testCellExistsUtf8(): void + { + $namedCell = 'Χαιρετισμός'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cellExists = $worksheet->cellExists($namedCell); + self::assertTrue($cellExists); + } + public function testCellNotExists(): void { $namedCell = 'GOODBYE'; @@ -67,6 +76,15 @@ class WorksheetNamedRangesTest extends TestCase self::assertSame('Hello', $cell->getValue()); } + public function testGetCellUtf8(): void + { + $namedCell = 'Χαιρετισμός'; + + $worksheet = $this->spreadsheet->getActiveSheet(); + $cell = $worksheet->getCell($namedCell); + self::assertSame('नमस्ते', $cell->getValue()); + } + public function testGetCellNotExists(): void { $namedCell = 'GOODBYE'; diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index 5377444d..30da1d75 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -3,7 +3,9 @@ namespace PhpOffice\PhpSpreadsheetTests\Worksheet; use Exception; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\CellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PHPUnit\Framework\TestCase; @@ -29,6 +31,7 @@ class WorksheetTest extends TestCase /** * @param string $title * @param string $expectMessage + * * @dataProvider setTitleInvalidProvider */ public function testSetTitleInvalid($title, $expectMessage): void @@ -87,6 +90,7 @@ class WorksheetTest extends TestCase /** * @param string $codeName * @param string $expectMessage + * * @dataProvider setCodeNameInvalidProvider */ public function testSetCodeNameInvalid($codeName, $expectMessage): void @@ -151,6 +155,7 @@ class WorksheetTest extends TestCase * @param string $expectTitle * @param string $expectCell * @param string $expectCell2 + * * @dataProvider extractSheetTitleProvider */ public function testExtractSheetTitle($range, $expectTitle, $expectCell, $expectCell2): void @@ -405,4 +410,100 @@ class WorksheetTest extends TestCase self::assertSame($expectedData, $worksheet->toArray()); self::assertSame($expectedHighestRow, $worksheet->getHighestRow()); } + + private static function getPopulatedSheetForEmptyRowTest(Spreadsheet $spreadsheet): Worksheet + { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B3', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('B4', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B5', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C5', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B6', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C6', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B7', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C7', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('B8', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('C8', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('D8', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + private static function getPopulatedSheetForEmptyColumnTest(Spreadsheet $spreadsheet): Worksheet + { + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValueExplicit('A1', 'Hello World', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('C2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('D2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('E2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('E3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('F2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('F3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G2', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('G3', 'PHP', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H2', null, DataType::TYPE_NULL); + $sheet->setCellValueExplicit('H3', '', DataType::TYPE_STRING); + $sheet->setCellValueExplicit('H4', 'PHP', DataType::TYPE_STRING); + + return $sheet; + } + + /** + * @dataProvider emptyRowProvider + */ + public function testIsEmptyRow(int $rowId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheetForEmptyRowTest($spreadsheet); + + $isEmpty = $sheet->isEmptyRow($rowId, CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyRowProvider(): array + { + return [ + [1, false], + [2, true], + [3, true], + [4, true], + [5, true], + [6, false], + [7, false], + [8, false], + [9, true], + ]; + } + + /** + * @dataProvider emptyColumnProvider + */ + public function testIsEmptyColumn(string $columnId, bool $expectedEmpty): void + { + $spreadsheet = new Spreadsheet(); + $sheet = self::getPopulatedSheetForEmptyColumnTest($spreadsheet); + + $isEmpty = $sheet->isEmptyColumn($columnId, CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL | CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + + self::assertSame($expectedEmpty, $isEmpty); + $spreadsheet->disconnectWorksheets(); + } + + public function emptyColumnProvider(): array + { + return [ + ['A', false], + ['B', true], + ['C', true], + ['D', true], + ['E', true], + ['F', false], + ['G', false], + ['H', false], + ['I', true], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php new file mode 100644 index 00000000..815d7e3d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/PaperSizeArrayTest.php @@ -0,0 +1,65 @@ +outfile !== '') { + unlink($this->outfile); + $this->outfile = ''; + } + } + + public function testPaperSizeArray(): void + { + // Issue 1713 - array in PhpSpreadsheet is 2 elements, + // but in Dompdf it is 4 elements, first 2 are zero. + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + // TABLOID is a 2-element array in Writer/Pdf.php $paperSizes + $size = PageSetup::PAPERSIZE_TABLOID; + $sheet->getPageSetup()->setPaperSize($size); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + $contents = file_get_contents($this->outfile); + self::assertNotFalse($contents); + self::assertStringContainsString('/MediaBox [0.000 0.000 792.000 1224.000]', $contents); + } + + public function testPaperSizeNotArray(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + // LETTER is a string in Writer/Pdf.php $paperSizes + $size = PageSetup::PAPERSIZE_LETTER; + $sheet->getPageSetup()->setPaperSize($size); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $this->outfile = File::temporaryFilename(); + $writer->save($this->outfile); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + $contents = file_get_contents($this->outfile); + self::assertNotFalse($contents); + self::assertStringContainsString('/MediaBox [0.000 0.000 612.000 792.000]', $contents); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php new file mode 100644 index 00000000..2aa9d5ed --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Dompdf/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Dompdf($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' transform:rotate(90deg);', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php b/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php new file mode 100644 index 00000000..da928457 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/TextRotationTest.php @@ -0,0 +1,24 @@ +getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Html($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' transform:rotate(90deg);', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php index c5d4da68..1c1ffb06 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/VisibilityTest.php @@ -99,11 +99,11 @@ class VisibilityTest extends Functional\AbstractFunctional self::assertEquals(1, $rowsrch); $rowsrch = preg_match('/^\\s*table[.]sheet0 tr[.]row1 [{] height:25pt [}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style1 [{].*text-decoration:line-through;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style1, th[.]style1 [{].*text-decoration:line-through;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style2 [{].*text-decoration:underline line-through;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style2, th[.]style2 [{].*text-decoration:underline line-through;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); - $rowsrch = preg_match('/^\\s*td[.]style3 [{].*text-decoration:underline;.*[}]\\s*$/m', $html); + $rowsrch = preg_match('/^\\s*td[.]style3, th[.]style3 [{].*text-decoration:underline;.*[}]\\s*$/m', $html); self::assertEquals(1, $rowsrch); $this->writeAndReload($spreadsheet, 'Html'); diff --git a/tests/PhpSpreadsheetTests/Writer/Mpdf/ImageCopyPdfTest.php b/tests/PhpSpreadsheetTests/Writer/Mpdf/ImageCopyPdfTest.php index 111848da..5d31bc01 100644 --- a/tests/PhpSpreadsheetTests/Writer/Mpdf/ImageCopyPdfTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Mpdf/ImageCopyPdfTest.php @@ -1,6 +1,6 @@ getActiveSheet(); + $sheet->setPrintGridlines(true); + $sheet->getStyle('A7')->getAlignment()->setTextRotation(90); + $sheet->setCellValue('A7', 'Lorem Ipsum'); + $writer = new Mpdf($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString(' text-rotate:90;', $html); + $spreadsheet->disconnectWorksheets(); + unset($spreadsheet); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php new file mode 100644 index 00000000..38fd29d3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xls/ParserTest.php @@ -0,0 +1,56 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + public function testNonArray(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unexpected non-array'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(); + } + + public function testMissingIndex(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unexpected non-array'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(['left' => 0]); + } + + public function testParseError(): void + { + $this->expectException(WriterException::class); + $this->expectExceptionMessage('Unknown token +'); + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + $parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => '+']); + } + + public function testGoodParse(): void + { + $this->spreadsheet = new Spreadsheet(); + $parser = new Parser($this->spreadsheet); + self::assertSame('1e01001e02001e0300', bin2hex($parser->toReversePolish(['left' => 1, 'right' => 2, 'value' => 3]))); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php index 7de39328..8c4aa1b9 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xls/VisibilityTest.php @@ -79,7 +79,7 @@ class VisibilityTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xls'); foreach ($visibleSheets as $sheetName => $visibility) { - $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByNameOrThrow($sheetName); self::assertSame($visibility, $reloadedWorksheet->getSheetState()); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php new file mode 100644 index 00000000..d4b4e6ea --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/CalculationErrorTest.php @@ -0,0 +1,64 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->reloadedSpreadsheet !== null) { + $this->reloadedSpreadsheet->disconnectWorksheets(); + $this->reloadedSpreadsheet = null; + } + } + + public function testCalculationExceptionSuppressed(): void + { + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $calculation = Calculation::getInstance($this->spreadsheet); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $calculation->setSuppressFormulaErrors(true); + $sheet->getCell('A1')->setValue('=SUM('); + $sheet->getCell('A2')->setValue('=2+3'); + $this->reloadedSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xlsx'); + $rcalculation = Calculation::getInstance($this->reloadedSpreadsheet); + self::assertFalse($rcalculation->getSuppressFormulaErrors()); + $rcalculation->setSuppressFormulaErrors(true); + $rsheet = $this->reloadedSpreadsheet->getActiveSheet(); + self::assertSame('=SUM(', $rsheet->getCell('A1')->getValue()); + self::assertFalse($rsheet->getCell('A1')->getCalculatedValue()); + self::assertSame('=2+3', $rsheet->getCell('A2')->getValue()); + self::assertSame(5, $rsheet->getCell('A2')->getCalculatedValue()); + $calculation->setSuppressFormulaErrors(false); + $rcalculation->setSuppressFormulaErrors(false); + } + + public function testCalculationException(): void + { + $this->expectException(CalcException::class); + $this->expectExceptionMessage("Formula Error: Expecting ')'"); + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $calculation = Calculation::getInstance($this->spreadsheet); + self::assertFalse($calculation->getSuppressFormulaErrors()); + $sheet->getCell('A1')->setValue('=SUM('); + $sheet->getCell('A2')->setValue('=2+3'); + $this->reloadedSpreadsheet = $this->writeAndReload($this->spreadsheet, 'Xlsx'); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php new file mode 100644 index 00000000..e2cbbff3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsInsertRowsTest.php @@ -0,0 +1,45 @@ +load($inputFilename); + $sheet = $spreadsheet->getActiveSheet(); + $drawingCollection = $sheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection); + $drawing = $drawingCollection[0]; + self::assertNotNull($drawing); + self::assertSame('D10', $drawing->getCoordinates()); + self::assertSame('F11', $drawing->getCoordinates2()); + self::assertSame('oneCell', $drawing->getEditAs()); + + $sheet->insertNewRowBefore(5); + $sheet->insertNewRowBefore(6); + + // Save spreadsheet to file and read it back + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + $rsheet = $reloadedSpreadsheet->getActiveSheet(); + $drawingCollection2 = $rsheet->getDrawingCollection(); + self::assertCount(1, $drawingCollection2); + $drawing2 = $drawingCollection2[0]; + self::assertNotNull($drawing2); + self::assertSame('D12', $drawing2->getCoordinates()); + self::assertSame('F13', $drawing2->getCoordinates2()); + self::assertSame('oneCell', $drawing2->getEditAs()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index f089b9ee..14b816d2 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -554,10 +554,10 @@ class DrawingsTest extends AbstractFunctional { return [ 'absolute' => ['absolute'], - 'onecell' => ['onecell'], - 'twocell' => ['twocell'], - 'unset (will be treated as twocell)' => [''], - 'unknown (will be treated as twocell)' => ['unknown', ''], + 'onecell' => ['oneCell'], + 'twocell' => ['twoCell'], + 'unset (will be treated as twoCell)' => [''], + 'unknown (will be treated as twoCell)' => ['unknown', ''], ]; } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php index 13a13bc9..7a4b9ca2 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/LocaleFloatsTest.php @@ -2,9 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; -class LocaleFloatsTest extends TestCase +class LocaleFloatsTest extends AbstractFunctional { /** * @var bool @@ -16,6 +17,12 @@ class LocaleFloatsTest extends TestCase */ private $currentLocale; + /** @var ?Spreadsheet */ + private $spreadsheet; + + /** @var ?Spreadsheet */ + private $reloadedSpreadsheet; + protected function setUp(): void { $this->currentLocale = setlocale(LC_ALL, '0'); @@ -34,6 +41,14 @@ class LocaleFloatsTest extends TestCase if ($this->localeAdjusted && is_string($this->currentLocale)) { setlocale(LC_ALL, $this->currentLocale); } + if ($this->spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + if ($this->reloadedSpreadsheet !== null) { + $this->reloadedSpreadsheet->disconnectWorksheets(); + $this->reloadedSpreadsheet = null; + } } public function testLocaleFloatsCorrectlyConvertedByWriter(): void @@ -42,18 +57,17 @@ class LocaleFloatsTest extends TestCase self::markTestSkipped('Unable to set locale for testing.'); } - $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $this->spreadsheet = $spreadsheet = new Spreadsheet(); + $properties = $spreadsheet->getProperties(); + $properties->setCustomProperty('Version', 1.2); $spreadsheet->getActiveSheet()->setCellValue('A1', 1.1); - $filename = 'decimalcomma.xlsx'; - $writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet); - $writer->save($filename); + $this->reloadedSpreadsheet = $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); - $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); - $spreadsheet = $reader->load($filename); - unlink($filename); - - $result = $spreadsheet->getActiveSheet()->getCell('A1')->getValue(); + $result = $reloadedSpreadsheet->getActiveSheet()->getCell('A1')->getValue(); + self::assertEqualsWithDelta(1.1, $result, 1.0E-8); + $prop = $reloadedSpreadsheet->getProperties()->getCustomPropertyValue('Version'); + self::assertEqualsWithDelta(1.2, $prop, 1.0E-8); $actual = sprintf('%f', $result); self::assertStringContainsString('1,1', $actual); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php index f9535714..eba7288f 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/UnparsedDataCloneTest.php @@ -77,19 +77,15 @@ class UnparsedDataCloneTest extends TestCase $reader1 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet1 = $reader1->load($resultFilename1); unlink($resultFilename1); - $sheet1c = $spreadsheet1->getSheetByName('Clone'); - self::assertNotNull($sheet1c); - $sheet1o = $spreadsheet1->getSheetByName('Original'); - self::assertNotNull($sheet1o); + $sheet1c = $spreadsheet1->getSheetByNameOrThrow('Clone'); + $sheet1o = $spreadsheet1->getSheetByNameOrThrow('Original'); $writer->save($resultFilename2); $reader2 = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet2 = $reader2->load($resultFilename2); unlink($resultFilename2); - $sheet2c = $spreadsheet2->getSheetByName('Clone'); - self::assertNotNull($sheet2c); - $sheet2o = $spreadsheet2->getSheetByName('Original'); - self::assertNotNull($sheet2o); + $sheet2c = $spreadsheet2->getSheetByNameOrThrow('Clone'); + $sheet2o = $spreadsheet2->getSheetByNameOrThrow('Original'); self::assertEquals($spreadsheet1->getSheetCount(), $spreadsheet2->getSheetCount()); self::assertCount(1, $sheet1c->getDrawingCollection()); diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php index 7e1ca967..ec2534fd 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/VisibilityTest.php @@ -79,7 +79,7 @@ class VisibilityTest extends AbstractFunctional $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); foreach ($visibleSheets as $sheetName => $visibility) { - $reloadedWorksheet = $reloadedSpreadsheet->getSheetByName($sheetName) ?? new Worksheet(); + $reloadedWorksheet = $reloadedSpreadsheet->getSheetByNameOrThrow($sheetName); self::assertSame($visibility, $reloadedWorksheet->getSheetState()); } } diff --git a/tests/data/Calculation/DateTime/DATE.php b/tests/data/Calculation/DateTime/DATE.php index 72816b76..d8b4303a 100644 --- a/tests/data/Calculation/DateTime/DATE.php +++ b/tests/data/Calculation/DateTime/DATE.php @@ -1,7 +1,9 @@ ['exception'], ]; diff --git a/tests/data/Calculation/LookupRef/ADDRESS.php b/tests/data/Calculation/LookupRef/ADDRESS.php index b9e170d5..14a3cf7d 100644 --- a/tests/data/Calculation/LookupRef/ADDRESS.php +++ b/tests/data/Calculation/LookupRef/ADDRESS.php @@ -48,6 +48,22 @@ return [ false, 'EXCEL SHEET', ], + '0 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!R2C3", + 2, + 3, + null, + 0, + 'EXCEL SHEET', + ], + '1 instead of bool for 4th arg' => [ + "'EXCEL SHEET'!\$C\$2", + 2, + 3, + null, + 1, + 'EXCEL SHEET', + ], [ "'EXCEL SHEET'!\$C\$2", 2, diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index 078ed007..ee1b919e 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -186,4 +186,14 @@ return [ 3, true, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, 102], + [null, 'Red'], + ], + 2, + false, + ], ]; diff --git a/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php b/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php new file mode 100644 index 00000000..eff13722 --- /dev/null +++ b/tests/data/Calculation/LookupRef/INDEXonSpreadsheet.php @@ -0,0 +1,234 @@ + [ + 1, // Expected + [ + [1], + ], + 0, + ], + 'Row Number omitted' => [ + 'exception', // Expected + [ + [1], + ], + ], + 'Negative Row' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + -1, + ], + 'Row > matrix rows' => [ + '#REF!', // Expected + [ + [1], + [2], + ], + 10, + ], + 'Row is not a number' => [ + '#NAME?', // Expected + [ + [1], + [2], + ], + 'NaN', + ], + 'Row is reference to non-number' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + 'ZZ98', + ], + 'Row is quoted non-numeric result' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + '"string"', + ], + 'Row is Error' => [ + '#N/A', // Expected + [ + [1], + [2], + ], + '#N/A', + ], + 'Return row 2 only one column' => [ + 'xyz', // Expected + [ + ['abc'], + ['xyz'], + ], + 2, + ], + 'Return row 1 col 2' => [ + 'def', // Expected + [ + ['abc', 'def'], + ['xyz', 'tuv'], + ], + 1, + 2, + ], + 'Column number omitted from 2-column matrix' => [ + '#REF!', // Expected + [ + ['abc', 'def'], + ['xyz', 'tuv'], + ], + 1, + ], + 'Column number omitted from 1-column matrix' => [ + 'xyz', // Expected + [ + ['abc'], + ['xyz'], + ], + 2, + ], + 'Return row 2 from larger matrix (Phpspreadsheet flattens expected [2,4] to single value)' => [ + 2, // Expected + // Input + [ + [1, 3], + [2, 4], + ], + 2, + 0, + ], + 'Negative Column' => [ + '#VALUE!', // Expected + [ + [1, 3], + [2, 4], + ], + 0, + -1, + ], + 'Column > matrix columns' => [ + '#REF!', // Expected + [ + [1, 3], + [2, 4], + ], + 2, + 10, + ], + 'Column is not a number' => [ + '#NAME?', // Expected + [ + [1], + [2], + ], + 1, + 'NaN', + ], + 'Column is reference to non-number' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + 1, + 'ZZ98', + ], + 'Column is quoted non-number' => [ + '#VALUE!', // Expected + [ + [1], + [2], + ], + 1, + '"string"', + ], + 'Column is Error' => [ + '#N/A', // Expected + [ + [1], + [2], + ], + 1, + '#N/A', + ], + 'Row 2 Column 2' => [ + 4, // Expected + [ + [1, 3], + [2, 4], + ], + 2, + 2, + ], + 'Row 2 Column 2 Alphabetic' => [ + 'Pears', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 2, + ], + 'Row 2 Column 1 Alphabetic' => [ + 'Bananas', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 1, + ], + 'Row 2 Column 0 (PhpSpreadsheet flattens result)' => [ + 'Bananas', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 0, + ], + 'Row 5 column 2' => [ + 3, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 5, + 2, + ], + 'Row 5 column 0 (flattened)' => [ + 8, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 5, + 0, + ], + 'Row 0 column 2 (flattened)' => [ + 6, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 0, + 2, + ], +]; diff --git a/tests/data/Calculation/LookupRef/VLOOKUP.php b/tests/data/Calculation/LookupRef/VLOOKUP.php index 2162d49a..21146638 100644 --- a/tests/data/Calculation/LookupRef/VLOOKUP.php +++ b/tests/data/Calculation/LookupRef/VLOOKUP.php @@ -98,7 +98,7 @@ return [ ['10y1', 7.0], ['10y2', 10.0], ], - 'NaN', + -5, ], [ '#REF!', @@ -111,9 +111,9 @@ return [ '#REF!', '10y2', [ - 2.0, - 7.0, - 10.0, + [2.0], + [7.0], + [10.0], ], 2.0, ], @@ -163,4 +163,34 @@ return [ 3, null, ], + 'issue2934' => [ + 'Red', + 102, + [ + [null, null], + [102, 'Red'], + ], + 2, + false, + ], + 'string supplied as index' => [ + '#VALUE!', + 102, + [ + [null, null], + [102, 'Red'], + ], + 'xyz', + false, + ], + 'num error propagated' => [ + '#NUM!', + 102, + [ + [null, null], + [102, 'Red'], + ], + '=SQRT(-1)', + false, + ], ]; diff --git a/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php b/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php new file mode 100644 index 00000000..b62cfd30 --- /dev/null +++ b/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php @@ -0,0 +1,34 @@ + ['exception'], + 'result just fits' => [ + // Note use Armenian character below to make sure chars, not bytes + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5) . 'ABCDE', + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'ABCDE', + ], + 'result too long' => [ + '#CALC!', + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'abc', + '=A2', + ], + 'propagate DIV0' => ['#DIV/0!', '1', '=2/0', '3'], ]; diff --git a/tests/data/Calculation/TextData/REPLACE.php b/tests/data/Calculation/TextData/REPLACE.php index 8f02e2e3..2268bf48 100644 --- a/tests/data/Calculation/TextData/REPLACE.php +++ b/tests/data/Calculation/TextData/REPLACE.php @@ -65,4 +65,20 @@ return [ 'negative length' => ['#VALUE!', 'hello', 3, -1, 'xyz'], 'boolean 1st parm' => ['TRDFGE', true, 3, 1, 'DFG'], 'boolean 4th parm' => ['heFALSElo', 'hello', 3, 1, false], + 'propagate REF' => ['#REF!', '=sheet99!A1', 3, 1, 'x'], + 'propagate DIV0' => ['#DIV/0!', '=1/0', 3, 1, 'x'], + 'string which just sneaks in' => [ + str_repeat('A', 32766) . 'C', + str_repeat('A', 32766) . 'B', + 32767, + '1', + 'C', + ], + 'string which overflows' => [ + '#VALUE!', + str_repeat('A', 32766) . 'B', + 32767, + '1', + 'CC', + ], ]; diff --git a/tests/data/Calculation/TextData/REPT.php b/tests/data/Calculation/TextData/REPT.php index f8aef72c..172d8fae 100644 --- a/tests/data/Calculation/TextData/REPT.php +++ b/tests/data/Calculation/TextData/REPT.php @@ -11,4 +11,8 @@ return [ ['111', 1, 3], ['δύο δύο ', 'δύο ', 2], ['#VALUE!', 'ABC', -1], + 'result too long' => ['#VALUE!', 'A', 32768], + 'result just fits' => [str_repeat('A', 32767), 'A', 32767], + 'propagate NUM' => ['#NUM!', '=SQRT(-1)', 5], + 'propagate REF' => ['#REF!', '=sheet99!A1', 5], ]; diff --git a/tests/data/Calculation/TextData/SUBSTITUTE.php b/tests/data/Calculation/TextData/SUBSTITUTE.php index 9b695a71..baf1c87b 100644 --- a/tests/data/Calculation/TextData/SUBSTITUTE.php +++ b/tests/data/Calculation/TextData/SUBSTITUTE.php @@ -85,4 +85,32 @@ return [ 'bool false instance' => ['#VALUE!', 'abcdefg', 'def', '123', false], 'bool true instance' => ['#VALUE!', 'abcdefg', 'def', '123', true], 'bool text' => ['FA-SE', false, 'L', '-'], + 'propagate REF' => ['#REF!', '=sheet99!A1', 'A', 'x'], + 'propagate DIV0' => ['#DIV/0!', 'hello', '=1/0', 1, 'x'], + 'string which just sneaks in' => [ + str_repeat('A', 32766) . 'C', + str_repeat('A', 32766) . 'B', + 'B', + 'C', + ], + 'string which overflows' => [ + '#VALUE!', + str_repeat('A', 32766) . 'B', + 'B', + 'CC', + ], + 'okay long string instance' => [ + 'AAAAB' . str_repeat('A', 32762), + str_repeat('A', 32767), + 'A', + 'B', + 5, + ], + 'overflow long string instance' => [ + '#VALUE!', + str_repeat('A', 32767), + 'A', + 'BB', + 5, + ], ]; diff --git a/tests/data/Calculation/TextData/TEXTAFTER.php b/tests/data/Calculation/TextData/TEXTAFTER.php new file mode 100644 index 00000000..ebcfecb9 --- /dev/null +++ b/tests/data/Calculation/TextData/TEXTAFTER.php @@ -0,0 +1,251 @@ + [ + "'s red hood", + [ + "Red riding hood's red hood", + 'hood', + ], + ], + 'END Case-sensitive Offset 2' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + 2, + ], + ], + 'END Case-sensitive Offset -1' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + -1, + ], + ], + 'END Case-sensitive Offset -2' => [ + "'s red hood", + [ + "Red riding hood's red hood", + 'hood', + -2, + ], + ], + 'END Case-sensitive Offset 3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + 3, + ], + ], + 'END Case-sensitive Offset -3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + -3, + ], + ], + 'END Case-sensitive Offset 3 with end' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + 3, + 0, + 1, + ], + ], + 'END Case-sensitive Offset -3 with end' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + 'hood', + -3, + 0, + 1, + ], + ], + 'END Case-sensitive - No Match' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'HOOD', + ], + ], + 'END Case-insensitive Offset 1' => [ + "'s red hood", + [ + "Red riding hood's red hood", + 'HOOD', + 1, + 1, + ], + ], + 'END Case-insensitive Offset 2' => [ + '', + [ + "Red riding hood's red hood", + 'HOOD', + 2, + 1, + ], + ], + 'END Offset 0' => [ + ExcelError::VALUE(), + [ + "Red riding hood's red hood", + 'hood', + 0, + ], + ], + 'Empty match positive' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + '', + ], + ], + 'Empty match negative' => [ + '', + [ + "Red riding hood's red hood", + '', + -1, + ], + ], + 'START Case-sensitive Offset 1' => [ + ' riding hood', + [ + "Red Riding Hood's red riding hood", + 'red', + ], + ], + 'START Case-insensitive Offset 1' => [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 1, + ], + ], + 'START Case-sensitive Offset -2' => [ + "Red Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 0, + 1, + ], + ], + 'START Case-insensitive Offset -2' => [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 1, + 1, + ], + ], + [ + ' riding hood', + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 0, + ], + ], + [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 1, + ], + ], + [ + "Red Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 0, + 1, + ], + ], + [ + " Riding Hood's red riding hood", + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 1, + 1, + ], + ], + [ + ExcelError::NA(), + [ + 'Socrates', + ' ', + 1, + 0, + 0, + ], + ], + [ + '', + [ + 'Socrates', + ' ', + 1, + 0, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 1' => [ + " riding hood's red riding hood", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 1, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 2' => [ + "'s red riding hood", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 2, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 3' => [ + ' riding hood', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 3, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset -2' => [ + ' riding hood', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + -2, + 1, + ], + ], +]; diff --git a/tests/data/Calculation/TextData/TEXTBEFORE.php b/tests/data/Calculation/TextData/TEXTBEFORE.php new file mode 100644 index 00000000..1929354c --- /dev/null +++ b/tests/data/Calculation/TextData/TEXTBEFORE.php @@ -0,0 +1,243 @@ + [ + 'Red riding ', + [ + "Red riding hood's red hood", + 'hood', + ], + ], + 'END Case-sensitive Offset 2' => [ + "Red riding hood's red ", + [ + "Red riding hood's red hood", + 'hood', + 2, + ], + ], + 'END Case-sensitive Offset -1' => [ + "Red riding hood's red ", + [ + "Red riding hood's red hood", + 'hood', + -1, + ], + ], + 'END Case-sensitive Offset -2' => [ + 'Red riding ', + [ + "Red riding hood's red hood", + 'hood', + -2, + ], + ], + 'END Case-sensitive Offset 3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + 3, + ], + ], + 'END Case-sensitive Offset -3' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'hood', + -3, + ], + ], + 'END Case-sensitive Offset 3 with end' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + 'hood', + 3, + 0, + 1, + ], + ], + 'END Case-sensitive Offset -3 with end' => [ + '', + [ + "Red riding hood's red hood", + 'hood', + -3, + 0, + 1, + ], + ], + 'END Case-sensitive - No Match' => [ + ExcelError::NA(), + [ + "Red riding hood's red hood", + 'HOOD', + ], + ], + 'END Case-insensitive Offset 1' => [ + 'Red riding ', + [ + "Red riding hood's red hood", + 'HOOD', + 1, + 1, + ], + ], + 'END Case-insensitive Offset 2' => [ + "Red riding hood's red ", + [ + "Red riding hood's red hood", + 'HOOD', + 2, + 1, + ], + ], + 'END Offset 0' => [ + ExcelError::VALUE(), + [ + "Red riding hood's red hood", + 'hood', + 0, + ], + ], + 'Empty match positive' => [ + '', + [ + "Red riding hood's red hood", + '', + ], + ], + 'Empty match negative' => [ + "Red riding hood's red hood", + [ + "Red riding hood's red hood", + '', + -1, + ], + ], + 'START Case-sensitive Offset 1' => [ + "Red Riding Hood's ", + [ + "Red Riding Hood's red riding hood", + 'red', + ], + ], + 'START Case-insensitive Offset 1' => [ + '', + [ + "Red Riding Hood's red riding hood", + 'red', + 1, + 1, + ], + ], + 'START Case-sensitive Offset -2' => [ + '', + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 0, + 1, + ], + ], + 'START Case-insensitive Offset -2' => [ + '', + [ + "Red Riding Hood's red riding hood", + 'red', + -2, + 1, + 1, + ], + ], + [ + ExcelError::NA(), + [ + 'ABACADAEA', + 'A', + 6, + 0, + 0, + ], + ], + [ + 'ABACADAEA', + [ + 'ABACADAEA', + 'A', + 6, + 0, + 1, + ], + ], + [ + ExcelError::NA(), + [ + 'Socrates', + ' ', + 1, + 0, + 0, + ], + ], + [ + 'Socrates', + [ + 'Socrates', + ' ', + 1, + 0, + 1, + ], + ], + [ + 'Immanuel', + [ + 'Immanuel Kant', + ' ', + 1, + 0, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 1' => [ + 'Little ', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 1, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 2' => [ + 'Little Red riding ', + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 2, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset 3' => [ + "Little Red riding hood's ", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + 3, + 1, + ], + ], + 'Multi-delimiter Case-Insensitive Offset -2' => [ + "Little Red riding hood's ", + [ + "Little Red riding hood's red riding hood", + ['HOOD', 'RED'], + -2, + 1, + ], + ], +]; diff --git a/tests/data/Calculation/TextData/TEXTJOIN.php b/tests/data/Calculation/TextData/TEXTJOIN.php index e345f7c1..565358a7 100644 --- a/tests/data/Calculation/TextData/TEXTJOIN.php +++ b/tests/data/Calculation/TextData/TEXTJOIN.php @@ -1,5 +1,7 @@ ['exception', ['-', true]], 'three arguments' => ['a', ['-', true, 'a']], 'boolean as string' => ['TRUE-FALSE-TRUE', ['-', true, true, false, true]], + 'result too long' => [ + '#CALC!', + [ + ',', + true, + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'abcde', + ], + ], + 'result just fits' => [ + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5) . ',abcd', + [ + ',', + true, + str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 5), + 'abcd', + ], + ], + 'propagate REF' => ['#REF!', [',', true, '1', '=sheet99!A1', '3']], + 'propagate NUM' => ['#NUM!', [',', true, '1', '=SQRT(-1)', '3']], ]; diff --git a/tests/data/Calculation/TextData/TEXTSPLIT.php b/tests/data/Calculation/TextData/TEXTSPLIT.php new file mode 100644 index 00000000..64016ca7 --- /dev/null +++ b/tests/data/Calculation/TextData/TEXTSPLIT.php @@ -0,0 +1,107 @@ +createTextRun('Hello'); +$richText1->createText(' World'); + +$richText2 = new RichText(); +$richText2->createTextRun('Hello'); +$richText2->createText("\nWorld"); + +return [ + ['1', 1, 0], + ['1.23', 1.23, 0], + ['-123.456', -123.456, 0], + ['TRUE', true, 0], + ['FALSE', false, 0], + ['Hello World', 'Hello World', 0], + ['HelloWorld', "Hello\nWorld", 0], + ['"Hello World"', 'Hello World', 1], + ['"HelloWorld"', "Hello\nWorld", 1], + ['Hello World', $richText1, 0], + ['HelloWorld', $richText2, 0], + ['"Hello World"', $richText1, 1], + ['"HelloWorld"', $richText2, 1], +]; diff --git a/tests/data/Calculation/Translations.php b/tests/data/Calculation/Translations.php index 604c6721..0965bac1 100644 --- a/tests/data/Calculation/Translations.php +++ b/tests/data/Calculation/Translations.php @@ -80,4 +80,14 @@ return [ 'nb', '=MAX(ABS({2,-3;-4,5}), ABS{-2,3;4,-5})', ], + 'not fooled by *RC' => [ + '=3*RC(B1)', + 'fr', + '=3*RC(B1)', + ], + 'handle * for ROW' => [ + '=3*LIGNE(B1)', + 'fr', + '=3*ROW(B1)', + ], ]; diff --git a/tests/data/CellExtractAllCellReferencesInRange.php b/tests/data/CellExtractAllCellReferencesInRange.php index a2ecbdd2..192500ce 100644 --- a/tests/data/CellExtractAllCellReferencesInRange.php +++ b/tests/data/CellExtractAllCellReferencesInRange.php @@ -130,4 +130,37 @@ return [ ], 'Z2:AA3', ], + [ + [ + 'Sheet1!D3', + ], + 'Sheet1!D3', + ], + [ + [ + 'Sheet1!D3', + 'Sheet1!E3', + 'Sheet1!D4', + 'Sheet1!E4', + ], + 'Sheet1!D3:E4', + ], + [ + [ + "'Sheet 1'!D3", + "'Sheet 1'!E3", + "'Sheet 1'!D4", + "'Sheet 1'!E4", + ], + "'Sheet 1'!D3:E4", + ], + [ + [ + "'Mark''s Sheet'!D3", + "'Mark''s Sheet'!E3", + "'Mark''s Sheet'!D4", + "'Mark''s Sheet'!E4", + ], + "'Mark's Sheet'!D3:E4", + ], ]; diff --git a/tests/data/CellGetRangeBoundaries.php b/tests/data/CellGetRangeBoundaries.php index ff434796..7a56b4c0 100644 --- a/tests/data/CellGetRangeBoundaries.php +++ b/tests/data/CellGetRangeBoundaries.php @@ -2,29 +2,45 @@ return [ [ - [ - [ - 'B', - 4, - ], - [ - 'E', - 9, - ], + 'Cell Range' => [ + ['B', 4], + ['E', 9], ], 'B4:E9', ], - [ + 'Single Cell' => [ [ - [ - 'B', - 4, - ], - [ - 'B', - 4, - ], + ['B', 4], + ['B', 4], ], 'B4', ], + 'Column Range' => [ + [ + ['B', 1], + ['C', 1048576], + ], + 'B:C', + ], + 'Single Column Range' => [ + [ + ['B', 1], + ['B', 1048576], + ], + 'B:B', + ], + 'Row Range' => [ + [ + ['A', 2], + ['XFD', 3], + ], + '2:3', + ], + 'Single Row Range' => [ + [ + ['A', 2], + ['XFD', 2], + ], + '2:2', + ], ]; diff --git a/tests/data/CellRangeBoundaries.php b/tests/data/CellRangeBoundaries.php index 9e856ced..882f437c 100644 --- a/tests/data/CellRangeBoundaries.php +++ b/tests/data/CellRangeBoundaries.php @@ -1,30 +1,46 @@ [ [ - [ - 2, - 4, - ], - [ - 5, - 9, - ], + [2, 4], + [5, 9], ], 'B4:E9', ], - [ + 'Single Cell' => [ [ - [ - 2, - 4, - ], - [ - 2, - 4, - ], + [2, 4], + [2, 4], ], 'B4', ], + 'Column Range' => [ + [ + [2, 1], + [3, 1048576], + ], + 'B:C', + ], + 'Single Column Range' => [ + [ + [2, 1], + [2, 1048576], + ], + 'B:B', + ], + 'Row Range' => [ + [ + [1, 2], + [16384, 3], + ], + '2:3', + ], + 'Single Row Range' => [ + [ + [1, 2], + [16384, 2], + ], + '2:2', + ], ]; diff --git a/tests/data/Reader/CSV/premiere.utf16le.csv b/tests/data/Reader/CSV/premiere.utf16le.csv index a5bb1ff1..4ca3c3e4 100644 Binary files a/tests/data/Reader/CSV/premiere.utf16le.csv and b/tests/data/Reader/CSV/premiere.utf16le.csv differ diff --git a/tests/data/Reader/HTML/utf8chars.html b/tests/data/Reader/HTML/utf8chars.html new file mode 100644 index 00000000..8d58c798 --- /dev/null +++ b/tests/data/Reader/HTML/utf8chars.html @@ -0,0 +1,28 @@ + + + + +Test Utf-8 characters voilà + + + + + + + + + + + + + + + + + + + + +
éàâèîαβγδε
𐐁𐐂𐐃 & だけちאבגדה𪔀𪔁𪔂
᠐᠑᠒അആกขฃ✀✐✠
+ + diff --git a/tests/data/Reader/Ods/HiddenMergeCellsTest.ods b/tests/data/Reader/Ods/HiddenMergeCellsTest.ods new file mode 100644 index 00000000..b8beb972 Binary files /dev/null and b/tests/data/Reader/Ods/HiddenMergeCellsTest.ods differ diff --git a/tests/data/Reader/XLS/HiddenMergeCellsTest.xls b/tests/data/Reader/XLS/HiddenMergeCellsTest.xls new file mode 100644 index 00000000..fefbcecc Binary files /dev/null and b/tests/data/Reader/XLS/HiddenMergeCellsTest.xls differ diff --git a/tests/data/Reader/XLSX/HiddenMergeCellsTest.xlsx b/tests/data/Reader/XLSX/HiddenMergeCellsTest.xlsx new file mode 100644 index 00000000..124f0561 Binary files /dev/null and b/tests/data/Reader/XLSX/HiddenMergeCellsTest.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.2506.xlsx b/tests/data/Reader/XLSX/issue.2506.xlsx new file mode 100644 index 00000000..6b2c7fbc Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2506.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.2677.namespace.xlsx b/tests/data/Reader/XLSX/issue.2677.namespace.xlsx new file mode 100644 index 00000000..8ed2eaab Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2677.namespace.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx b/tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx new file mode 100644 index 00000000..b7f6cc5c Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2677.removeformula1.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.2885.xlsx b/tests/data/Reader/XLSX/issue.2885.xlsx new file mode 100644 index 00000000..d7f4d489 Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2885.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.2965.xlsx b/tests/data/Reader/XLSX/issue.2965.xlsx new file mode 100644 index 00000000..7c0672d3 Binary files /dev/null and b/tests/data/Reader/XLSX/issue.2965.xlsx differ diff --git a/tests/data/Reader/XLSX/issue.3093.xlsx b/tests/data/Reader/XLSX/issue.3093.xlsx new file mode 100644 index 00000000..eb141ec9 Binary files /dev/null and b/tests/data/Reader/XLSX/issue.3093.xlsx differ diff --git a/tests/data/Reader/XLSX/ribbon.donotopen.zip b/tests/data/Reader/XLSX/ribbon.donotopen.zip new file mode 100644 index 00000000..97d58bdc Binary files /dev/null and b/tests/data/Reader/XLSX/ribbon.donotopen.zip differ diff --git a/tests/data/Style/NumberFormat.php b/tests/data/Style/NumberFormat.php index 80f7080b..b307e23d 100644 --- a/tests/data/Style/NumberFormat.php +++ b/tests/data/Style/NumberFormat.php @@ -146,13 +146,13 @@ return [ '#,###', ], [ - 12, + '12', 12000, '#,', ], // Scaling test [ - 12.199999999999999, + '12.2', 12200000, '0.0,,', ], @@ -1486,4 +1486,9 @@ return [ '-1111.119', NumberFormat::FORMAT_ACCOUNTING_EUR, ], + 'issue 1929' => ['(79.3%)', -0.793, '#,##0.0%;(#,##0.0%)'], + 'percent without leading 0' => ['6.2%', 0.062, '##.0%'], + 'percent with leading 0' => ['06.2%', 0.062, '00.0%'], + 'percent lead0 no decimal' => ['06%', 0.062, '00%'], + 'percent nolead0 no decimal' => ['6%', 0.062, '##%'], ]; diff --git a/tests/data/Worksheet/namedRangeTest.xlsx b/tests/data/Worksheet/namedRangeTest.xlsx index a4383bb0..b0936a5a 100644 Binary files a/tests/data/Worksheet/namedRangeTest.xlsx and b/tests/data/Worksheet/namedRangeTest.xlsx differ diff --git a/tests/data/Writer/XLSX/issue.2908.xlsx b/tests/data/Writer/XLSX/issue.2908.xlsx new file mode 100644 index 00000000..3dcd7923 Binary files /dev/null and b/tests/data/Writer/XLSX/issue.2908.xlsx differ