diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8fb4f81978..d640a0b8e8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,12 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install locales + run: sudo apt-get install -y language-pack-fr language-pack-de + + - name: Install single-byte locale + run: sudo sed -i -e 's/# de_DE@euro/de_DE@euro/g' /etc/locale.gen && sudo locale-gen de_DE@euro + - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index f567e658c6..1d78534a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). -## TBD - 3.7.0 +## TBD - 3.8.0 ### Added -- Nothing yet. +- CHOOSECOLS, CHOOSEROWS, DROP, TAKE, and EXPAND. [PR #4286](https://github.com/PHPOffice/PhpSpreadsheet/pull/4286) ### Changed @@ -21,11 +21,28 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Deprecated +- Nothing yet. + +### Fixed + +- Xlsx Reader Shared Formula with Boolean Result. Partial solution for [Issue #4280](https://github.com/PHPOffice/PhpSpreadsheet/issues/4280) [PR #4281](https://github.com/PHPOffice/PhpSpreadsheet/pull/4281) +- Retitling cloned Worksheets. [Issue #641](https://github.com/PHPOffice/PhpSpreadsheet/issues/641) [PR #4302](https://github.com/PHPOffice/PhpSpreadsheet/pull/4302) + +## 2024-12-26 - 3.7.0 + +### Deprecated + - Drawing::setIsUrl is unneeded. The property is set when setPath determines whether path is a url. ### Fixed -- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Security patches for Samples. +- Security patches for Html Writer. +- Avoid unexpected charset in currency symbol. [PR #4279](https://github.com/PHPOffice/PhpSpreadsheet/pull/4279) +- Add forceFullCalc option to Xlsx Writer. [Issue #4269](https://github.com/PHPOffice/PhpSpreadsheet/issues/4269) [PR #4271](https://github.com/PHPOffice/PhpSpreadsheet/pull/4271) +- More context options may be needed for http(s) image. [Php issue 17121](https://github.com/php/php-src/issues/17121) [PR #4276](https://github.com/PHPOffice/PhpSpreadsheet/pull/4276) +- Coverage-related tweaks to Xls Reader. [PR #4277](https://github.com/PHPOffice/PhpSpreadsheet/pull/4277) +- Several fixed to ODS Writer. [Issue #4261](https://github.com/PHPOffice/PhpSpreadsheet/issues/4261) [PR #4263](https://github.com/PHPOffice/PhpSpreadsheet/pull/4263) [PR #4264](https://github.com/PHPOffice/PhpSpreadsheet/pull/4264) [PR #4266](https://github.com/PHPOffice/PhpSpreadsheet/pull/4266) ## 2024-12-08 - 3.6.0 diff --git a/LICENSE b/LICENSE index 3ec5723dde..04a90f083e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 PhpSpreadsheet Authors +Copyright (c) 2019-2025 PhpSpreadsheet Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.lock b/composer.lock index 6533b316ad..cc706e608e 100644 --- a/composer.lock +++ b/composer.lock @@ -788,16 +788,16 @@ }, { "name": "dompdf/dompdf", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "2d622faf9aa1f8f7f24dd094e49b5cf6c0c5d4e6" + "reference": "baf4084b27c7f4b5b7a221b19a94d11327664eb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/2d622faf9aa1f8f7f24dd094e49b5cf6c0c5d4e6", - "reference": "2d622faf9aa1f8f7f24dd094e49b5cf6c0c5d4e6", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/baf4084b27c7f4b5b7a221b19a94d11327664eb8", + "reference": "baf4084b27c7f4b5b7a221b19a94d11327664eb8", "shasum": "" }, "require": { @@ -813,7 +813,7 @@ "ext-json": "*", "ext-zip": "*", "mockery/mockery": "^1.3", - "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", "squizlabs/php_codesniffer": "^3.5", "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" }, @@ -846,22 +846,22 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v3.0.1" + "source": "https://github.com/dompdf/dompdf/tree/v3.0.2" }, - "time": "2024-12-05T14:59:38+00:00" + "time": "2024-12-27T20:27:37+00:00" }, { "name": "dompdf/php-font-lib", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/dompdf/php-font-lib.git", - "reference": "991d6a954f6bbd7e41022198f00586b230731441" + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/991d6a954f6bbd7e41022198f00586b230731441", - "reference": "991d6a954f6bbd7e41022198f00586b230731441", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", "shasum": "" }, "require": { @@ -891,9 +891,9 @@ "homepage": "https://github.com/dompdf/php-font-lib", "support": { "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/1.0.0" + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" }, - "time": "2024-04-29T13:40:38+00:00" + "time": "2024-12-02T14:37:59+00:00" }, { "name": "dompdf/php-svg-lib", @@ -1051,16 +1051,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.65.0", + "version": "v3.66.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "79d4f3e77b250a7d8043d76c6af8f0695e8a469f" + "reference": "5f5f2a142ff36b93c41885bca29cc5f861c013e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/79d4f3e77b250a7d8043d76c6af8f0695e8a469f", - "reference": "79d4f3e77b250a7d8043d76c6af8f0695e8a469f", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/5f5f2a142ff36b93c41885bca29cc5f861c013e6", + "reference": "5f5f2a142ff36b93c41885bca29cc5f861c013e6", "shasum": "" }, "require": { @@ -1086,7 +1086,7 @@ "symfony/polyfill-mbstring": "^1.28", "symfony/polyfill-php80": "^1.28", "symfony/polyfill-php81": "^1.28", - "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/process": "^5.4 || ^6.0 || ^7.0 <7.2", "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { @@ -1142,7 +1142,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.65.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.66.0" }, "funding": [ { @@ -1150,7 +1150,7 @@ "type": "github" } ], - "time": "2024-11-25T00:39:24+00:00" + "time": "2024-12-29T13:46:23+00:00" }, { "name": "masterminds/html5", @@ -1221,16 +1221,16 @@ }, { "name": "mitoteam/jpgraph", - "version": "10.4.3", + "version": "10.4.4", "source": { "type": "git", "url": "https://github.com/mitoteam/jpgraph.git", - "reference": "f0db97108aec23a3bbb34721365931af992b83b3" + "reference": "9ad8e2fcc30f765c788a28543e9705fb541d499f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/f0db97108aec23a3bbb34721365931af992b83b3", - "reference": "f0db97108aec23a3bbb34721365931af992b83b3", + "url": "https://api.github.com/repos/mitoteam/jpgraph/zipball/9ad8e2fcc30f765c788a28543e9705fb541d499f", + "reference": "9ad8e2fcc30f765c788a28543e9705fb541d499f", "shasum": "" }, "require": { @@ -1270,9 +1270,9 @@ ], "support": { "issues": "https://github.com/mitoteam/jpgraph/issues", - "source": "https://github.com/mitoteam/jpgraph/tree/10.4.3" + "source": "https://github.com/mitoteam/jpgraph/tree/10.4.4" }, - "time": "2024-12-01T06:36:31+00:00" + "time": "2025-01-01T05:39:20+00:00" }, { "name": "mpdf/mpdf", @@ -3012,24 +3012,24 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v8.6.0", + "version": "v8.7.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "d2fb94a9641be84d79c7548c6d39bbebba6e9a70" + "reference": "f414ff953002a9b18e3a116f5e462c56f21237cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d2fb94a9641be84d79c7548c6d39bbebba6e9a70", - "reference": "d2fb94a9641be84d79c7548c6d39bbebba6e9a70", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/f414ff953002a9b18e3a116f5e462c56f21237cf", + "reference": "f414ff953002a9b18e3a116f5e462c56f21237cf", "shasum": "" }, "require": { "ext-iconv": "*", - "php": ">=5.6.20" + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.27" + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.40" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -3071,9 +3071,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.6.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.7.0" }, - "time": "2024-07-01T07:33:21+00:00" + "time": "2024-10-27T17:38:32+00:00" }, { "name": "sebastian/cli-parser", @@ -4065,16 +4065,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87" + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", - "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", "shasum": "" }, "require": { @@ -4141,7 +4141,7 @@ "type": "open_collective" } ], - "time": "2024-11-16T12:02:36+00:00" + "time": "2024-12-11T16:04:26+00:00" }, { "name": "symfony/console", @@ -5425,20 +5425,21 @@ }, { "name": "tecnickcom/tcpdf", - "version": "6.7.7", + "version": "6.8.0", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "cfbc0028cc23f057f2baf9e73bdc238153c22086" + "reference": "14ffa0e308f5634aa2489568b4b90b24073b6731" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/cfbc0028cc23f057f2baf9e73bdc238153c22086", - "reference": "cfbc0028cc23f057f2baf9e73bdc238153c22086", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/14ffa0e308f5634aa2489568b4b90b24073b6731", + "reference": "14ffa0e308f5634aa2489568b4b90b24073b6731", "shasum": "" }, "require": { - "php": ">=5.5.0" + "ext-curl": "*", + "php": ">=7.1.0" }, "type": "library", "autoload": { @@ -5485,7 +5486,7 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.7.7" + "source": "https://github.com/tecnickcom/TCPDF/tree/6.8.0" }, "funding": [ { @@ -5493,7 +5494,7 @@ "type": "custom" } ], - "time": "2024-10-26T12:15:02+00:00" + "time": "2024-12-23T13:34:57+00:00" }, { "name": "theseer/tokenizer", diff --git a/docs/references/function-list-by-category.md b/docs/references/function-list-by-category.md index fdd23ae624..e6a702028f 100644 --- a/docs/references/function-list-by-category.md +++ b/docs/references/function-list-by-category.md @@ -240,8 +240,12 @@ Excel Function | PhpSpreadsheet Function ADDRESS | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Address::cell AREAS | **Not yet Implemented** CHOOSE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Selection::CHOOSE +CHOOSECOLS | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::chooseCols +CHOOSEROWS | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::chooseRows COLUMN | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowColumnInformation::COLUMN COLUMNS | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowColumnInformation::COLUMNS +DROP | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::drop +EXPAND | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::expand FILTER | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Filter::filter FORMULATEXT | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Formula::text GETPIVOTDATA | **Not yet Implemented** @@ -258,6 +262,7 @@ ROWS | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowCo RTD | **Not yet Implemented** SORT | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Sort::sort SORTBY | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Sort::sortBy +TAKE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::take TRANSPOSE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix::transpose UNIQUE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Unique::unique VLOOKUP | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup::lookup @@ -284,8 +289,6 @@ BASE | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Base:: CEILING | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Ceiling::ceiling CEILING.MATH | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Ceiling::math CEILING.PRECISE | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Ceiling::precise -CHOOSECOLS | **Not yet Implemented** -CHOOSEROWS | **Not yet Implemented** COMBIN | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations::withoutRepetition COMBINA | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations::withRepetition COS | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Cosine::cos @@ -296,11 +299,9 @@ CSC | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\C CSCH | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Cosecant::csch DECIMAL | **Not yet Implemented** DEGREES | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Angle::toDegrees -DROP | **Not yet Implemented** ECMA.CEILING | **Not yet Implemented** EVEN | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Round::even EXP | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Exp::evaluate -EXPAND | **Not yet Implemented** FACT | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Factorial::fact FACTDOUBLE | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Factorial::factDouble FLOOR | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Floor::floor @@ -354,7 +355,6 @@ SUMSQ | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\SumSqu SUMX2MY2 | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\SumSquares::sumXSquaredMinusYSquared SUMX2PY2 | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\SumSquares::sumXSquaredPlusYSquared SUMXMY2 | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\SumSquares::sumXMinusYSquared -TAKE | **Not yet Implemented** TAN | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Tangent::tan TANH | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Tangent::tanh TOCOL | **Not yet Implemented** diff --git a/docs/references/function-list-by-name.md b/docs/references/function-list-by-name.md index a403beffe8..3696b95b7c 100644 --- a/docs/references/function-list-by-name.md +++ b/docs/references/function-list-by-name.md @@ -79,8 +79,8 @@ CHISQ.INV.RT | CATEGORY_STATISTICAL | \PhpOffice\PhpSpread CHISQ.TEST | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\ChiSquared::test CHITEST | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\ChiSquared::test CHOOSE | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Selection::CHOOSE -CHOOSECOLS | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** -CHOOSEROWS | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** +CHOOSECOLS | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::chooseCols +CHOOSEROWS | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::chooseRows CLEAN | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Trim::nonPrintable CODE | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\CharacterConvert::code COLUMN | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowColumnInformation::COLUMN @@ -158,7 +158,7 @@ DOLLAR | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpread DOLLARDE | CATEGORY_FINANCIAL | \PhpOffice\PhpSpreadsheet\Calculation\Financial\Dollar::decimal DOLLARFR | CATEGORY_FINANCIAL | \PhpOffice\PhpSpreadsheet\Calculation\Financial\Dollar::fractional DPRODUCT | CATEGORY_DATABASE | \PhpOffice\PhpSpreadsheet\Calculation\Database\DProduct::evaluate -DROP | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** +DROP | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::drop DSTDEV | CATEGORY_DATABASE | \PhpOffice\PhpSpreadsheet\Calculation\Database\DStDev::evaluate DSTDEVP | CATEGORY_DATABASE | \PhpOffice\PhpSpreadsheet\Calculation\Database\DStDevP::evaluate DSUM | CATEGORY_DATABASE | \PhpOffice\PhpSpreadsheet\Calculation\Database\DSum::evaluate @@ -183,7 +183,7 @@ ERROR.TYPE | CATEGORY_INFORMATION | \PhpOffice\PhpSpread EVEN | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Round::even EXACT | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Text::exact EXP | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Exp::evaluate -EXPAND | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** +EXPAND | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::expand EXPON.DIST | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\Exponential::distribution EXPONDIST | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\Exponential::distribution @@ -554,7 +554,7 @@ T.DIST.RT | CATEGORY_STATISTICAL | **Not yet Implemente T.INV | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\StudentT::inverse T.INV.2T | CATEGORY_STATISTICAL | **Not yet Implemented** T.TEST | CATEGORY_STATISTICAL | **Not yet Implemented** -TAKE | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** +TAKE | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::take TAN | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Tangent::tan TANH | CATEGORY_MATH_AND_TRIG | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Tangent::tanh TBILLEQ | CATEGORY_FINANCIAL | \PhpOffice\PhpSpreadsheet\Calculation\Financial\TreasuryBill::bondEquivalentYield diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 19fe8a8ed9..28bc3b173e 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -169,6 +169,12 @@ $writer->save("05featuredemo.xlsx"); **Note** Formulas will still be calculated in any column set to be autosized even if pre-calculated is set to false +**Note** Prior to release 3.7.0, the use of this feature will cause Excel to be used in a mode where opening a sheet saved in this manner *might* not automatically recalculate a cell's formula when a cell used it the formula changes. Furthermore, that behavior might be applied to all spreadsheets open at the time. To avoid this behavior, add the following statement after `setPreCalculateFormulas` above: +```php +$writer->setForceFullCalc(false); +``` +In a future release, the property's default may change to `false` and that statement may no longer be required. + #### Office 2003 compatibility pack Because of a bug in the Office2003 compatibility pack, there can be some diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 8b27155d90..c768c49f7e 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -925,6 +925,8 @@ getStyle('A1:M500'), rather than styling the cells individually in a loop. This is much faster compared to looping through cells and styling them individually. +**Tip** If you are styling entire row(s) or column(s), e.g. getStyle('A:A'), it is recommended to use applyFromArray as described below rather than setting the styles individually as described above. + There is also an alternative manner to set styles. The following code sets a cell's style to font bold, alignment right, top border thin and a gradient fill: diff --git a/samples/Engineering/Convert-Online.php b/samples/Engineering/Convert-Online.php index a923956b4d..bae13f778c 100644 --- a/samples/Engineering/Convert-Online.php +++ b/samples/Engineering/Convert-Online.php @@ -78,14 +78,16 @@ $quantity = $_POST['quantity']; $fromUnit = $_POST['fromUnit']; $toUnit = $_POST['toUnit']; - if (isset($units[$_POST['category']][$fromUnit], $units[$_POST['category']][$toUnit])) { + if (!is_numeric($quantity)) { + $helper->log('Quantity is not numeric'); + } elseif (isset($units[$_POST['category']][$fromUnit], $units[$_POST['category']][$toUnit])) { /** @var float|string */ $result = ConvertUOM::CONVERT($quantity, $fromUnit, $toUnit); - echo "{$quantity} {$units[$_POST['category']][$fromUnit]} is {$result} {$units[$_POST['category']][$toUnit]}", PHP_EOL; + $helper->log("{$quantity} {$units[$_POST['category']][$fromUnit]} is {$result} {$units[$_POST['category']][$toUnit]}"); } else { - echo 'Please enter quantity and select From Unit and To Unit', PHP_EOL; + $helper->log('Please enter quantity and select From Unit and To Unit'); } } else { - echo 'Please enter quantity and select From Unit and To Unit', PHP_EOL; + $helper->log('Please enter quantity and select From Unit and To Unit'); } diff --git a/samples/Wizards/NumberFormat/Accounting.php b/samples/Wizards/NumberFormat/Accounting.php index 0b87dd4812..e453a5fd31 100644 --- a/samples/Wizards/NumberFormat/Accounting.php +++ b/samples/Wizards/NumberFormat/Accounting.php @@ -64,13 +64,6 @@ >Trailing -
- -
- >Yes - >No -
-

@@ -85,21 +78,23 @@ $helper->log('The Sample Number Value must be numeric'); } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); + } elseif (!in_array($_POST['currency'], array_keys($currencies), true)) { + $helper->log('Unrecognized currency symbol'); } else { try { - $wizard = new Wizard\Accounting($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $wizard = new Wizard\Accounting($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position']); $mask = $wizard->format(); $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); $helper->log('
Code:
'); $helper->log('use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;'); $helper->log( - "\$mask = Wizard\\Accounting('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" + "\$wizard = new Wizard\\Accounting('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" . (isset($_POST['thousands']) ? 'WITH_THOUSANDS_SEPARATOR' : 'WITHOUT_THOUSANDS_SEPARATOR') . ', Wizard\Currency::' . (((bool) $_POST['position']) ? 'LEADING_SYMBOL' : 'TRAILING_SYMBOL') - . ', Wizard\Currency::' . (((bool) $_POST['spacing']) ? 'SYMBOL_WITH_SPACING' : 'SYMBOL_WITHOUT_SPACING') - . ');
' + . ');' ); - $helper->log('echo (string) $mask;'); + $helper->log('$mask = $wizard->format();'); + $helper->log('
echo (string) $mask;'); $helper->log('
Mask:
'); $helper->log($mask . '
'); $helper->log('
Example:
'); diff --git a/samples/Wizards/NumberFormat/Currency.php b/samples/Wizards/NumberFormat/Currency.php index 2c2b248525..c2a66093b2 100644 --- a/samples/Wizards/NumberFormat/Currency.php +++ b/samples/Wizards/NumberFormat/Currency.php @@ -5,6 +5,8 @@ use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard\CurrencyNegative; +use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter; require __DIR__ . '/../Header.php'; @@ -15,6 +17,19 @@ return; } +$negatives = [ + CurrencyNegative::minus, + CurrencyNegative::redMinus, + CurrencyNegative::parentheses, + CurrencyNegative::redParentheses, +]; +$negativesString = [ + 'CurrencyNegative::minus', + 'CurrencyNegative::redMinus', + 'CurrencyNegative::parentheses', + 'CurrencyNegative::redParentheses', +]; + $currencies = [ '$' => 'US Dollars ($)', '€' => 'Euro (€)', @@ -65,10 +80,12 @@
- +
- >Yes - >No + >Minus Sign + >Red Minus Sign + >Parentheses + >Red Parentheses
@@ -85,21 +102,27 @@ $helper->log('The Sample Number Value must be numeric'); } elseif (!is_numeric($_POST['decimals']) || str_contains((string) $_POST['decimals'], '.') || (int) $_POST['decimals'] < 0) { $helper->log('The Decimal Places value must be positive integer'); + } elseif (!in_array($_POST['currency'], array_keys($currencies), true)) { + $helper->log('Unrecognized currency symbol'); } else { try { - $wizard = new Wizard\Currency($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position'], (bool) $_POST['spacing']); + $negative = $negatives[$_POST['negative']] ?? CurrencyNegative::minus; + $wizard = new Wizard\Currency($_POST['currency'], (int) $_POST['decimals'], isset($_POST['thousands']), (bool) $_POST['position']); + $wizard->setNegative($negative); $mask = $wizard->format(); - $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask); + $example = (string) NumberFormat::toFormattedString((float) $_POST['number'], $mask, [HtmlWriter::class, 'formatColorStatic']); $helper->log('
Code:
'); $helper->log('use PhpOffice\PhpSpreadsheet\Style\NumberFormat\Wizard;'); + $helper->log('use PhpOffice\PhpSpreadsheet\Style\NumberFormat\CurrencyNegative;'); $helper->log( - "\$mask = Wizard\\Currency('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" + "\$wizard = new Wizard\\Currency('{$_POST['currency']}', {$_POST['decimals']}, Wizard\\Number::" . (isset($_POST['thousands']) ? 'WITH_THOUSANDS_SEPARATOR' : 'WITHOUT_THOUSANDS_SEPARATOR') . ', Wizard\Currency::' . (((bool) $_POST['position']) ? 'LEADING_SYMBOL' : 'TRAILING_SYMBOL') - . ', Wizard\Currency::' . (((bool) $_POST['spacing']) ? 'SYMBOL_WITH_SPACING' : 'SYMBOL_WITHOUT_SPACING') - . ');
' + . ');' ); - $helper->log('echo (string) $mask;'); + $helper->log('$wizard->setNegative(' . $negativesString[$_POST['negative']] . ');'); + $helper->log('$mask = $wizard->format();'); + $helper->log('
echo (string) $mask;'); $helper->log('
Mask:
'); $helper->log($mask . '
'); $helper->log('
Example:
'); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 704332b59e..b256aad3b3 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -553,13 +553,13 @@ public static function getExcelConstants(string $key): bool|null 'argumentCount' => '2+', ], 'CHOOSECOLS' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'chooseCols'], 'argumentCount' => '2+', ], 'CHOOSEROWS' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'chooseRows'], 'argumentCount' => '2+', ], 'CLEAN' => [ @@ -925,8 +925,8 @@ public static function getExcelConstants(string $key): bool|null 'argumentCount' => '3', ], 'DROP' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'drop'], 'argumentCount' => '2-3', ], 'DSTDEV' => [ @@ -1025,8 +1025,8 @@ public static function getExcelConstants(string $key): bool|null 'argumentCount' => '1', ], 'EXPAND' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'expand'], 'argumentCount' => '2-4', ], 'EXPONDIST' => [ @@ -2490,8 +2490,8 @@ public static function getExcelConstants(string $key): bool|null 'argumentCount' => '1', ], 'TAKE' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\ChooseRowsEtc::class, 'take'], 'argumentCount' => '2-3', ], 'TAN' => [ diff --git a/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php b/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php index 0107f40453..aa2019e2bb 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php +++ b/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php @@ -145,11 +145,9 @@ private function rows(array $arguments): array private function columns(array $arguments): array { return array_map( - function (mixed $argument): int { - return is_array($argument) && is_array($argument[array_keys($argument)[0]]) + fn (mixed $argument): int => is_array($argument) && is_array($argument[array_keys($argument)[0]]) ? count($argument[array_keys($argument)[0]]) - : 1; - }, + : 1, $arguments ); } diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 9a094966a3..5ee2dfd7a4 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -230,6 +230,32 @@ public static function flattenArray(mixed $array): array return $flattened; } + /** + * Convert a multi-dimensional array to a simple 1-dimensional array. + * Same as above but argument is specified in ... format. + * + * @param mixed $array Array to be flattened + * + * @return array Flattened array + */ + public static function flattenArray2(mixed ...$array): array + { + $flattened = []; + $stack = array_values($array); + + while (!empty($stack)) { + $value = array_shift($stack); + + if (is_array($value)) { + array_unshift($stack, ...array_values($value)); + } else { + $flattened[] = $value; + } + } + + return $flattened; + } + public static function scalar(mixed $value): mixed { if (!is_array($value)) { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php b/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php new file mode 100644 index 0000000000..97303e7533 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/ChooseRowsEtc.php @@ -0,0 +1,241 @@ + [$x]) : null, ...$array)); // @phpstan-ignore-line + } + + /** @return mixed[] */ + private static function arrayValues(mixed $array): array + { + return is_array($array) ? array_values($array) : [$array]; + } + + /** + * CHOOSECOLS. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function chooseCols(mixed $input, mixed ...$args): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + $retval = self::chooseRows(self::transpose($input), ...$args); + + return is_array($retval) ? self::transpose($retval) : $retval; + } + + /** + * CHOOSEROWS. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function chooseRows(mixed $input, mixed ...$args): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + $inputArray = [[]]; // no row 0 + $numRows = 0; + foreach ($input as $inputRow) { + $inputArray[] = self::arrayValues($inputRow); + ++$numRows; + } + $outputArray = []; + foreach (Functions::flattenArray2(...$args) as $arg) { + if (!is_numeric($arg)) { + return ExcelError::VALUE(); + } + $index = (int) $arg; + if ($index < 0) { + $index += $numRows + 1; + } + if ($index <= 0 || $index > $numRows) { + return ExcelError::VALUE(); + } + $outputArray[] = $inputArray[$index]; + } + + return $outputArray; + } + + private static function dropRows(array $array, mixed $offset): array|string + { + if ($offset === null) { + return $array; + } + if (!is_numeric($offset)) { + return ExcelError::VALUE(); + } + $offset = (int) $offset; + $count = count($array); + if (abs($offset) >= $count) { + // In theory, this should be #CALC!, but Excel treats + // #CALC! as corrupt, and it's not worth figuring out why + return ExcelError::VALUE(); + } + if ($offset === 0) { + return $array; + } + if ($offset > 0) { + return array_slice($array, $offset); + } + + return array_slice($array, 0, $count + $offset); + } + + /** + * DROP. + * + * @param mixed $input expect two-dimensional array + * + * @return mixed[]|string + */ + public static function drop(mixed $input, mixed $rows = null, mixed $columns = null): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + $inputArray = []; // no row 0 + foreach ($input as $inputRow) { + $inputArray[] = self::arrayValues($inputRow); + } + $outputArray1 = self::dropRows($inputArray, $rows); + if (is_string($outputArray1)) { + return $outputArray1; + } + $outputArray2 = self::transpose($outputArray1); + $outputArray3 = self::dropRows($outputArray2, $columns); + if (is_string($outputArray3)) { + return $outputArray3; + } + + return self::transpose($outputArray3); + } + + private static function takeRows(array $array, mixed $offset): array|string + { + if ($offset === null) { + return $array; + } + if (!is_numeric($offset)) { + return ExcelError::VALUE(); + } + $offset = (int) $offset; + if ($offset === 0) { + // should be #CALC! - see above + return ExcelError::VALUE(); + } + $count = count($array); + if (abs($offset) >= $count) { + return $array; + } + if ($offset > 0) { + return array_slice($array, 0, $offset); + } + + return array_slice($array, $count + $offset); + } + + /** + * TAKE. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function take(mixed $input, mixed $rows, mixed $columns = null): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + if ($rows === null && $columns === null) { + return $input; + } + $inputArray = []; + foreach ($input as $inputRow) { + $inputArray[] = self::arrayValues($inputRow); + } + $outputArray1 = self::takeRows($inputArray, $rows); + if (is_string($outputArray1)) { + return $outputArray1; + } + $outputArray2 = self::transpose($outputArray1); + $outputArray3 = self::takeRows($outputArray2, $columns); + if (is_string($outputArray3)) { + return $outputArray3; + } + + return self::transpose($outputArray3); + } + + /** + * EXPAND. + * + * @param mixed $input expecting two-dimensional array + * + * @return mixed[]|string + */ + public static function expand(mixed $input, mixed $rows, mixed $columns = null, mixed $pad = '#N/A'): array|string + { + if (!is_array($input)) { + $input = [[$input]]; + } + if ($rows === null && $columns === null) { + return $input; + } + $numRows = count($input); + $rows ??= $numRows; + if (!is_numeric($rows)) { + return ExcelError::VALUE(); + } + $rows = (int) $rows; + if ($rows < count($input)) { + return ExcelError::VALUE(); + } + $numCols = 0; + foreach ($input as $inputRow) { + $numCols = max($numCols, is_array($inputRow) ? count($inputRow) : 1); + } + $columns ??= $numCols; + if (!is_numeric($columns)) { + return ExcelError::VALUE(); + } + $columns = (int) $columns; + if ($columns < $numCols) { + return ExcelError::VALUE(); + } + $inputArray = []; + foreach ($input as $inputRow) { + $inputArray[] = array_pad(self::arrayValues($inputRow), $columns, $pad); + } + $outputArray = []; + $padRow = array_pad([], $columns, $pad); + for ($count = 0; $count < $rows; ++$count) { + $outputArray[] = ($count >= $numRows) ? $padRow : $inputArray[$count]; + } + + return $outputArray; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php index 5d3516777a..80d4caa7f3 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -86,11 +86,9 @@ public static function randArray(mixed $rows = 1, mixed $columns = 1, mixed $min return array_chunk( array_map( - function () use ($min, $max, $wholeNumber): int|float { - return $wholeNumber + fn (): int|float => $wholeNumber ? mt_rand((int) $min, (int) $max) - : (mt_rand() / mt_getrandmax()) * ($max - $min) + $min; - }, + : (mt_rand() / mt_getrandmax()) * ($max - $min) + $min, array_fill(0, $rows * $columns, $min) ), max($columns, 1) diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 49b0dfc924..edc50ad7aa 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -133,10 +133,8 @@ public static function inverseRightTail(mixed $probability, mixed $degrees) return ExcelError::NAN(); } - $callback = function ($value) use ($degrees): float { - return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) + $callback = fn ($value): float => 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2)); - }; $newtonRaphson = new NewtonRaphson($callback); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index f988a6c199..03691a8f27 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -171,11 +171,9 @@ private static function applyPadding(array $rows, mixed $padding): array ); return array_map( - function (array $row) use ($columnCount, $padding): array { - return (count($row) < $columnCount) + fn (array $row): array => (count($row) < $columnCount) ? array_merge($row, array_fill(0, $columnCount - count($row), $padding)) - : $row; - }, + : $row, $rows ); } diff --git a/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php index b8918c9153..58463ebea0 100644 --- a/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php +++ b/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php @@ -9,6 +9,11 @@ * * Alternative implementation should leverage off-memory, non-volatile storage * to reduce overall memory usage. + * + * Either SimpleCache1 or SimpleCache3, but not both, may be used. + * For code coverage testing, it will always be SimpleCache3. + * + * @codeCoverageIgnore */ class SimpleCache1 implements CacheInterface { diff --git a/src/PhpSpreadsheet/Helper/Downloader.php b/src/PhpSpreadsheet/Helper/Downloader.php index 4e98c7925c..85131dc1df 100644 --- a/src/PhpSpreadsheet/Helper/Downloader.php +++ b/src/PhpSpreadsheet/Helper/Downloader.php @@ -30,18 +30,18 @@ class Downloader public function __construct(string $folder, string $filename, ?string $filetype = null) { if ((is_dir($folder) === false) || (is_readable($folder) === false)) { - throw new Exception("Folder {$folder} is not accessable"); + throw new Exception('Folder is not accessible'); } $filepath = "{$folder}/{$filename}"; $this->filepath = (string) realpath($filepath); $this->filename = basename($filepath); if ((file_exists($this->filepath) === false) || (is_readable($this->filepath) === false)) { - throw new Exception("{$this->filename} not found, or cannot be read"); + throw new Exception('File not found, or cannot be read'); } $filetype ??= pathinfo($filename, PATHINFO_EXTENSION); if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) { - throw new Exception("Invalid filetype: {$filetype} cannot be downloaded"); + throw new Exception('Invalid filetype: file cannot be downloaded'); } $this->filetype = strtolower($filetype); } diff --git a/src/PhpSpreadsheet/Reader/Xls/Color.php b/src/PhpSpreadsheet/Reader/Xls/Color.php index 6fd346bfa9..17b6e165ce 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color.php @@ -24,12 +24,6 @@ public static function map(int $color, array $palette, int $version): array return $palette[$color - 8]; } - // default color table - if ($version == Xls::XLS_BIFF8) { - return Color\BIFF8::lookup($color); - } - - // BIFF5 - return Color\BIFF5::lookup($color); + return ($version === Xls::XLS_BIFF8) ? Color\BIFF8::lookup($color) : Color\BIFF5::lookup($color); } } diff --git a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php index 324d12cb21..1c05c63c7e 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php +++ b/src/PhpSpreadsheet/Reader/Xls/ConditionalFormatting.php @@ -36,20 +36,12 @@ class ConditionalFormatting extends Xls public static function type(int $type): ?string { - if (isset(self::$types[$type])) { - return self::$types[$type]; - } - - return null; + return self::$types[$type] ?? null; } public static function operator(int $operator): ?string { - if (isset(self::$operators[$operator])) { - return self::$operators[$operator]; - } - - return null; + return self::$operators[$operator] ?? null; } /** diff --git a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php index a0c897efc9..2fef22d211 100644 --- a/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php +++ b/src/PhpSpreadsheet/Reader/Xls/DataValidationHelper.php @@ -48,29 +48,17 @@ class DataValidationHelper extends Xls public static function type(int $type): ?string { - if (isset(self::$types[$type])) { - return self::$types[$type]; - } - - return null; + return self::$types[$type] ?? null; } public static function errorStyle(int $errorStyle): ?string { - if (isset(self::$errorStyles[$errorStyle])) { - return self::$errorStyles[$errorStyle]; - } - - return null; + return self::$errorStyles[$errorStyle] ?? null; } public static function operator(int $operator): ?string { - if (isset(self::$operators[$operator])) { - return self::$operators[$operator]; - } - - return null; + return self::$operators[$operator] ?? null; } /** diff --git a/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php b/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php index 2d8778ff25..6477606b06 100644 --- a/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php +++ b/src/PhpSpreadsheet/Reader/Xls/ListFunctions.php @@ -44,12 +44,10 @@ protected function listWorksheetNames2(string $filename, Xls $xls): array } foreach ($xls->sheets as $sheet) { - if ($sheet['sheetType'] != 0x00) { + if ($sheet['sheetType'] === 0x00) { // 0x00: Worksheet, 0x02: Chart, 0x06: Visual Basic module - continue; + $worksheetNames[] = $sheet['name']; } - - $worksheetNames[] = $sheet['name']; } return $worksheetNames; @@ -93,7 +91,7 @@ protected function listWorksheetInfo2(string $filename, Xls $xls): array // Parse the individual sheets foreach ($xls->sheets as $sheet) { - if ($sheet['sheetType'] != 0x00) { + if ($sheet['sheetType'] !== 0x00) { // 0x00: Worksheet // 0x02: Chart // 0x06: Visual Basic module diff --git a/src/PhpSpreadsheet/Reader/Xls/Style/Border.php b/src/PhpSpreadsheet/Reader/Xls/Style/Border.php index 97cebbd44e..96275bf9f9 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Style/Border.php +++ b/src/PhpSpreadsheet/Reader/Xls/Style/Border.php @@ -28,10 +28,6 @@ class Border public static function lookup(int $index): string { - if (isset(self::$borderStyleMap[$index])) { - return self::$borderStyleMap[$index]; - } - - return StyleBorder::BORDER_NONE; + return self::$borderStyleMap[$index] ?? StyleBorder::BORDER_NONE; } } diff --git a/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php b/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php index 4e379509e5..93e3f44979 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php +++ b/src/PhpSpreadsheet/Reader/Xls/Style/FillPattern.php @@ -37,10 +37,6 @@ class FillPattern */ public static function lookup(int $index): string { - if (isset(self::$fillPatternMap[$index])) { - return self::$fillPatternMap[$index]; - } - - return Fill::FILL_NONE; + return self::$fillPatternMap[$index] ?? Fill::FILL_NONE; } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index d22f76ff4f..e7cd0b7119 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -854,6 +854,8 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } // Read cell! + $useFormula = isset($c->f) + && ((string) $c->f !== '' || (isset($c->f->attributes()['t']) && strtolower((string) $c->f->attributes()['t']) === 'shared')); switch ($cellDataType) { case DataType::TYPE_STRING: if ((string) $c->v != '') { @@ -868,7 +870,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet break; case DataType::TYPE_BOOL: - if (!isset($c->f) || ((string) $c->f) === '') { + if (!$useFormula) { if (isset($c->v)) { $value = self::castToBoolean($c); } else { @@ -883,16 +885,16 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet break; case DataType::TYPE_STRING2: - if (isset($c->f)) { + if ($useFormula) { $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString'); self::storeFormulaAttributes($c->f, $docSheet, $r); } else { - $value = self::castToString($c); + $value = self::castToString($c); } break; case DataType::TYPE_INLINE: - if (isset($c->f)) { + if ($useFormula) { $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToError'); self::storeFormulaAttributes($c->f, $docSheet, $r); } else { @@ -901,7 +903,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet break; case DataType::TYPE_ERROR: - if (!isset($c->f)) { + if (!$useFormula) { $value = self::castToError($c); } else { // Formula @@ -916,7 +918,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet break; default: - if (!isset($c->f)) { + if (!$useFormula) { $value = self::castToString($c); if (is_numeric($value)) { $value += 0; diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index aac3836ce4..a16b1cafc0 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -19,17 +19,17 @@ class StringHelper /** * Decimal separator. */ - private static ?string $decimalSeparator; + private static ?string $decimalSeparator = null; /** * Thousands separator. */ - private static ?string $thousandsSeparator; + private static ?string $thousandsSeparator = null; /** * Currency code. */ - private static ?string $currencyCode; + private static ?string $currencyCode = null; /** * Is iconv extension avalable? @@ -511,6 +511,23 @@ public static function strCaseReverse(string $textValue): string return implode('', $characters); } + private static function useAlt(string $altValue, string $default, bool $trimAlt): string + { + return ($trimAlt ? trim($altValue) : $altValue) ?: $default; + } + + private static function getLocaleValue(string $key, string $altKey, string $default, bool $trimAlt = false): string + { + $localeconv = localeconv(); + $rslt = $localeconv[$key]; + // win-1252 implements Euro as 0x80 plus other symbols + if (preg_match('//u', $rslt) !== 1) { + $rslt = ''; + } + + return $rslt ?: self::useAlt($localeconv[$altKey], $default, $trimAlt); + } + /** * Get the decimal separator. If it has not yet been set explicitly, try to obtain number * formatting information from locale. @@ -518,14 +535,7 @@ public static function strCaseReverse(string $textValue): string public static function getDecimalSeparator(): string { if (!isset(self::$decimalSeparator)) { - $localeconv = localeconv(); - self::$decimalSeparator = ($localeconv['decimal_point'] != '') - ? $localeconv['decimal_point'] : $localeconv['mon_decimal_point']; - - if (self::$decimalSeparator == '') { - // Default to . - self::$decimalSeparator = '.'; - } + self::$decimalSeparator = self::getLocaleValue('decimal_point', 'mon_decimal_point', '.'); } return self::$decimalSeparator; @@ -549,14 +559,7 @@ public static function setDecimalSeparator(?string $separator): void public static function getThousandsSeparator(): string { if (!isset(self::$thousandsSeparator)) { - $localeconv = localeconv(); - self::$thousandsSeparator = ($localeconv['thousands_sep'] != '') - ? $localeconv['thousands_sep'] : $localeconv['mon_thousands_sep']; - - if (self::$thousandsSeparator == '') { - // Default to . - self::$thousandsSeparator = ','; - } + self::$thousandsSeparator = self::getLocaleValue('thousands_sep', 'mon_thousands_sep', ','); } return self::$thousandsSeparator; @@ -577,22 +580,10 @@ public static function setThousandsSeparator(?string $separator): void * Get the currency code. If it has not yet been set explicitly, try to obtain the * symbol information from locale. */ - public static function getCurrencyCode(): string + public static function getCurrencyCode(bool $trimAlt = false): string { - if (!empty(self::$currencyCode)) { - return self::$currencyCode; - } - self::$currencyCode = '$'; - $localeconv = localeconv(); - if (!empty($localeconv['currency_symbol'])) { - self::$currencyCode = $localeconv['currency_symbol']; - - return self::$currencyCode; - } - if (!empty($localeconv['int_curr_symbol'])) { - self::$currencyCode = $localeconv['int_curr_symbol']; - - return self::$currencyCode; + if (!isset(self::$currencyCode)) { + self::$currencyCode = self::getLocaleValue('currency_symbol', 'int_curr_symbol', '$', $trimAlt); } return self::$currencyCode; diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 943db95cc3..f40a8889ac 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -514,7 +514,7 @@ public function getActiveSheet(): Worksheet public function createSheet(?int $sheetIndex = null): Worksheet { $newSheet = new Worksheet($this); - $this->addSheet($newSheet, $sheetIndex); + $this->addSheet($newSheet, $sheetIndex, true); return $newSheet; } @@ -535,8 +535,20 @@ public function sheetNameExists(string $worksheetName): bool * @param Worksheet $worksheet The worksheet to add * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) */ - public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null): Worksheet + public function addSheet(Worksheet $worksheet, ?int $sheetIndex = null, bool $retitleIfNeeded = false): Worksheet { + if ($retitleIfNeeded) { + $title = $worksheet->getTitle(); + if ($this->sheetNameExists($title)) { + $i = 1; + $newTitle = "$title $i"; + while ($this->sheetNameExists($newTitle)) { + ++$i; + $newTitle = "$title $i"; + } + $worksheet->setTitle($newTitle); + } + } if ($this->sheetNameExists($worksheet->getTitle())) { throw new Exception( "Workbook already contains a worksheet named '{$worksheet->getTitle()}'. Rename this worksheet first." diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index cb15e8f375..c9032a44d1 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -112,11 +112,11 @@ private static function splitFormatForSectionSelection(array $sections, mixed $v * @param null|array|bool|float|int|RichText|string $value Value to format * @param string $format Format code: see = self::FORMAT_* for predefined values; * or can be any valid MS Excel custom format string - * @param ?array $callBack Callback function for additional formatting of string + * @param null|array|callable $callBack Callback function for additional formatting of string * * @return string Formatted string */ - public static function toFormattedString($value, string $format, ?array $callBack = null): string + public static function toFormattedString($value, string $format, null|array|callable $callBack = null): string { while (is_array($value)) { $value = array_shift($value); @@ -200,9 +200,8 @@ public static function toFormattedString($value, string $format, ?array $callBac } // Additional formatting provided by callback function - if ($callBack !== null) { - [$writerInstance, $function] = $callBack; - $value = $writerInstance->$function($value, $colors); + if (is_callable($callBack)) { + $value = $callBack($value, $colors); } return str_replace(chr(0x00), '.', $value); diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 2c4a469559..4b88fcadc9 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -50,7 +50,7 @@ public function setEvaluated(bool $value): void /** * Create a new AutoFilter. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range * A simple string containing a Cell range like 'A1:E10' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 234014a0ff..a7a62a954c 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -64,7 +64,7 @@ class Table implements Stringable /** * Create a new Table. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range * A simple string containing a Cell range like 'A1:E10' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. @@ -268,7 +268,7 @@ public function getRange(): string /** * Set Table Cell Range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range * A simple string containing a Cell range like 'A1:E10' is permitted * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange object. diff --git a/src/PhpSpreadsheet/Worksheet/Validations.php b/src/PhpSpreadsheet/Worksheet/Validations.php index 99b5eedbc5..ec78c22b0b 100644 --- a/src/PhpSpreadsheet/Worksheet/Validations.php +++ b/src/PhpSpreadsheet/Worksheet/Validations.php @@ -36,7 +36,7 @@ public static function validateCellAddress(null|CellAddress|string|array $cellAd /** * Validate a cell address or cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), * or as a CellAddress or AddressRange object. */ @@ -59,7 +59,7 @@ public static function validateCellOrCellRange(AddressRange|CellAddress|int|stri /** * Validate a cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), * or as an AddressRange object. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 5943e5a993..6d30ab0f52 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -323,6 +323,7 @@ public function __construct(?Spreadsheet $parent = null, string $title = 'Worksh { // Set parent and title $this->parent = $parent; + $this->hash = spl_object_id($this); $this->setTitle($title, false); // setTitle can change $pTitle $this->setCodeName($this->getTitle()); @@ -351,7 +352,6 @@ public function __construct(?Spreadsheet $parent = null, string $title = 'Worksh $this->autoFilter = new AutoFilter('', $this); // Table collection $this->tableCollection = new ArrayObject(); - $this->hash = spl_object_id($this); } /** @@ -871,7 +871,7 @@ public function setTitle(string $title, bool $updateFormulaCellReferences = true // Syntax check self::checkSheetTitle($title); - if ($this->parent) { + if ($this->parent && $this->parent->getIndex($this, true) >= 0) { // Is there already such sheet name? if ($this->parent->sheetNameExists($title)) { // Use name, but append with lowest possible integer @@ -901,7 +901,7 @@ public function setTitle(string $title, bool $updateFormulaCellReferences = true // Set title $this->title = $title; - if ($this->parent && $this->parent->getCalculationEngine()) { + if ($this->parent && $this->parent->getIndex($this, true) >= 0 && $this->parent->getCalculationEngine()) { // New title $newTitle = $this->getTitle(); $this->parent->getCalculationEngine() @@ -1391,7 +1391,7 @@ public function getStyles(): array /** * Get style for cell. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellCoordinate + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $cellCoordinate * A simple string containing a cell address like 'A1' or a cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. @@ -1695,7 +1695,7 @@ public function getColumnBreaks(): array /** * Set merge on a cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. * @param string $behaviour How the merged cells should behave. @@ -1820,7 +1820,7 @@ public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behavi /** * Remove merge on a cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. * @@ -1871,7 +1871,7 @@ public function setMergeCells(array $mergeCells): static /** * Set protection on a cell or cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @param string $password Password to unlock the protection @@ -1894,7 +1894,7 @@ public function protectCells(AddressRange|CellAddress|int|string|array $range, s /** * Remove protection on a cell or cell range. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * @@ -1952,7 +1952,7 @@ public function getAutoFilter(): AutoFilter /** * Set AutoFilter. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|AutoFilter|string $autoFilterOrRange * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or an AddressRange. @@ -2698,7 +2698,7 @@ public function setSelectedCell(string $coordinate): static /** * Select a range of cells. * - * @param AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10' + * @param AddressRange|AddressRange|AddressRange|array{0: int, 1: int, 2: int, 3: int}|array{0: int, 1: int}|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10' * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), * or a CellAddress or AddressRange object. * diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index df1353ea08..d70a067f6f 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -403,12 +403,12 @@ public function generateHTMLHeader(bool $includeStyles = false): string } else { $propertyValue = (string) $propertyValue; } - $html .= self::generateMeta($propertyValue, "custom.$propertyQualifier.$customProperty"); + $html .= self::generateMeta($propertyValue, htmlspecialchars("custom.$propertyQualifier.$customProperty")); } } if (!empty($properties->getHyperlinkBase())) { - $html .= ' ' . PHP_EOL; + $html .= ' ' . PHP_EOL; } $html .= $includeStyles ? $this->generateStyles(true) : $this->generatePageDeclarations(true); @@ -1586,8 +1586,9 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri // Hyperlink? if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { $url = $worksheet->getHyperlink($coordinate)->getUrl(); - $urldecode = strtolower(html_entity_decode(trim($url), encoding: 'UTF-8')); - $parseScheme = preg_match('/^(\\w+):/', $urldecode, $matches); + $urlDecode1 = html_entity_decode($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $urlTrim = preg_replace('/^\\s+/u', '', $urlDecode1) ?? $urlDecode1; + $parseScheme = preg_match('/^([\\w\\s]+):/u', strtolower($urlTrim), $matches); if ($parseScheme === 1 && !in_array($matches[1], ['http', 'https', 'file', 'ftp', 's3'], true)) { $cellData = htmlspecialchars($url, Settings::htmlEntityFlags()); } else { @@ -1720,6 +1721,17 @@ public function setUseInlineCss(bool $useInlineCss): static * @param string $format Format code */ public function formatColor(string $value, string $format): string + { + return self::formatColorStatic($value, $format); + } + + /** + * Add color to formatted string as inline style. + * + * @param string $value Plain formatted value without color + * @param string $format Format code + */ + public static function formatColorStatic(string $value, string $format): string { // Color information, e.g. [Red] is always at the beginning $color = null; // initialize diff --git a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index 7573230689..92fc497e77 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -20,6 +20,7 @@ class Style public const COLUMN_STYLE_PREFIX = 'co'; public const ROW_STYLE_PREFIX = 'ro'; public const TABLE_STYLE_PREFIX = 'ta'; + public const INDENT_TO_INCHES = 0.1043; // undocumented, used trial and error private XMLWriter $writer; @@ -28,12 +29,13 @@ public function __construct(XMLWriter $writer) $this->writer = $writer; } - private function mapHorizontalAlignment(string $horizontalAlignment): string + private function mapHorizontalAlignment(?string $horizontalAlignment): string { return match ($horizontalAlignment) { Alignment::HORIZONTAL_CENTER, Alignment::HORIZONTAL_CENTER_CONTINUOUS, Alignment::HORIZONTAL_DISTRIBUTED => 'center', Alignment::HORIZONTAL_RIGHT => 'end', Alignment::HORIZONTAL_FILL, Alignment::HORIZONTAL_JUSTIFY => 'justify', + Alignment::HORIZONTAL_GENERAL, '', null => '', default => 'start', }; } @@ -145,8 +147,10 @@ private function writeCellProperties(CellStyle $style): void { // Align $hAlign = $style->getAlignment()->getHorizontal(); + $hAlign = $this->mapHorizontalAlignment($hAlign); $vAlign = $style->getAlignment()->getVertical(); $wrap = $style->getAlignment()->getWrapText(); + $indent = $style->getAlignment()->getIndent(); $this->writer->startElement('style:table-cell-properties'); if (!empty($vAlign) || $wrap) { @@ -168,10 +172,16 @@ private function writeCellProperties(CellStyle $style): void $this->writer->endElement(); - if (!empty($hAlign)) { - $hAlign = $this->mapHorizontalAlignment($hAlign); - $this->writer->startElement('style:paragraph-properties'); - $this->writer->writeAttribute('fo:text-align', $hAlign); + if ($hAlign !== '' || !empty($indent)) { + $this->writer + ->startElement('style:paragraph-properties'); + if ($hAlign !== '') { + $this->writer->writeAttribute('fo:text-align', $hAlign); + } + if (!empty($indent)) { + $indentString = sprintf('%.4f', $indent * self::INDENT_TO_INCHES) . 'in'; + $this->writer->writeAttribute('fo:margin-left', $indentString); + } $this->writer->endElement(); } } @@ -289,6 +299,7 @@ public function writeTableStyle(Worksheet $worksheet, int $sheetId): void 'style:name', sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId) ); + $this->writer->writeAttribute('style:master-page-name', 'Default'); $this->writer->startElement('style:table-properties'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 91ac5d1d21..3b9cc82458 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -20,9 +20,6 @@ */ class Content extends WriterPart { - const NUMBER_COLS_REPEATED_MAX = 1024; - const NUMBER_ROWS_REPEATED_MAX = 1048576; - private Formula $formulaConvertor; /** @@ -142,7 +139,6 @@ private function writeSheets(XMLWriter $objWriter): void sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric()) ); $objWriter->writeAttribute('table:default-cell-style-name', 'ce0'); -// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); $objWriter->endElement(); } $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex); @@ -155,34 +151,33 @@ private function writeSheets(XMLWriter $objWriter): void */ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void { - $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; - $span_row = 0; + $spanRow = 0; $rows = $sheet->getRowIterator(); foreach ($rows as $row) { - $cellIterator = $row->getCellIterator(); - --$numberRowsRepeated; - if ($cellIterator->valid()) { - $objWriter->startElement('table:table-row'); - if ($span_row) { - if ($span_row > 1) { - $objWriter->writeAttribute('table:number-rows-repeated', (string) $span_row); - } - $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX); + $cellIterator = $row->getCellIterator(iterateOnlyExistingCells: true); + $cellIterator->rewind(); + $rowStyleExists = $sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0; + if ($cellIterator->valid() || $rowStyleExists) { + if ($spanRow) { + $objWriter->startElement('table:table-row'); + $objWriter->writeAttribute( + 'table:number-rows-repeated', + (string) $spanRow + ); $objWriter->endElement(); - $span_row = 0; - } else { - if ($sheet->rowDimensionExists($row->getRowIndex()) && $sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) { - $objWriter->writeAttribute( - 'table:style-name', - sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) - ); - } - $this->writeCells($objWriter, $cellIterator); + $spanRow = 0; + } + $objWriter->startElement('table:table-row'); + if ($rowStyleExists) { + $objWriter->writeAttribute( + 'table:style-name', + sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) + ); } + $this->writeCells($objWriter, $cellIterator); $objWriter->endElement(); } else { - ++$span_row; + ++$spanRow; } } } @@ -192,7 +187,6 @@ private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetInd */ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void { - $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX; $prevColumn = -1; foreach ($cells as $cell) { /** @var Cell $cell */ @@ -293,17 +287,6 @@ private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void $objWriter->endElement(); $prevColumn = $column; } - - $numberColsRepeated = $numberColsRepeated - $prevColumn - 1; - if ($numberColsRepeated > 0) { - if ($numberColsRepeated > 1) { - $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', (string) $numberColsRepeated); - $objWriter->endElement(); - } else { - $objWriter->writeElement('table:table-cell'); - } - } } /** diff --git a/src/PhpSpreadsheet/Writer/Ods/Styles.php b/src/PhpSpreadsheet/Writer/Ods/Styles.php index 448b1eff13..6bfdfb1b33 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Styles.php +++ b/src/PhpSpreadsheet/Writer/Ods/Styles.php @@ -56,8 +56,17 @@ public function write(): string $objWriter->writeElement('office:font-face-decls'); $objWriter->writeElement('office:styles'); - $objWriter->writeElement('office:automatic-styles'); - $objWriter->writeElement('office:master-styles'); + $objWriter->startElement('office:automatic-styles'); + $objWriter->startElement('style:page-layout'); + $objWriter->writeAttribute('style:name', 'Mpm1'); + $objWriter->endElement(); // style:page-layout + $objWriter->endElement(); // office:automatic-styles + $objWriter->startElement('office:master-styles'); + $objWriter->startElement('style:master-page'); + $objWriter->writeAttribute('style:name', 'Default'); + $objWriter->writeAttribute('style:page-layout-name', 'Mpm1'); + $objWriter->endElement(); //style:master-page + $objWriter->endElement(); //office:master-styles $objWriter->endElement(); return $objWriter->getData(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index a9d06db581..b74d810502 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -140,6 +140,8 @@ class Xlsx extends BaseWriter private bool $useDynamicArray = false; + private ?bool $forceFullCalc = null; + /** * Create a new Xlsx Writer. */ @@ -342,7 +344,7 @@ public function save($filename, int $flags = 0): void $zipContent['xl/styles.xml'] = $this->getWriterPartStyle()->writeStyles($this->spreadSheet); // Add workbook to ZIP file - $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas); + $zipContent['xl/workbook.xml'] = $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas, $this->forceFullCalc); $chartCount = 0; // Add worksheets @@ -747,4 +749,20 @@ private function determineUseDynamicArrays(): void { $this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getInstanceArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays; } + + /** + * If this is set when a spreadsheet is opened, + * values may not be automatically re-calculated, + * and a button will be available to force re-calculation. + * This may apply to all spreadsheets open at that time. + * If null, this will be set to the opposite of $preCalculateFormulas. + * It is likely that false is the desired setting, although + * cases have been reported where true is required (issue #456). + */ + public function setForceFullCalc(?bool $forceFullCalc): self + { + $this->forceFullCalc = $forceFullCalc; + + return $this; + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index 24a2eeb064..c907e100b7 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -15,10 +15,11 @@ class Workbook extends WriterPart * Write workbook to XML format. * * @param bool $preCalculateFormulas If true, formulas will be calculated before writing + * @param ?bool $forceFullCalc If null, !$preCalculateFormulas * * @return string XML Output */ - public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false): string + public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormulas = false, ?bool $forceFullCalc = null): string { // Create XML writer if ($this->getParentWriter()->getUseDiskCaching()) { @@ -57,7 +58,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $preCalculateFormul (new DefinedNamesWriter($objWriter, $spreadsheet))->write(); // calcPr - $this->writeCalcPr($objWriter, $preCalculateFormulas); + $this->writeCalcPr($objWriter, $preCalculateFormulas, $forceFullCalc); $objWriter->endElement(); @@ -148,7 +149,7 @@ private function writeWorkbookProtection(XMLWriter $objWriter, Spreadsheet $spre * * @param bool $preCalculateFormulas If true, formulas will be calculated before writing */ - private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = true): void + private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas, ?bool $forceFullCalc): void { $objWriter->startElement('calcPr'); @@ -160,7 +161,11 @@ private function writeCalcPr(XMLWriter $objWriter, bool $preCalculateFormulas = // fullCalcOnLoad isn't needed if we will calculate before writing $objWriter->writeAttribute('calcCompleted', ($preCalculateFormulas) ? '1' : '0'); $objWriter->writeAttribute('fullCalcOnLoad', ($preCalculateFormulas) ? '0' : '1'); - $objWriter->writeAttribute('forceFullCalc', ($preCalculateFormulas) ? '0' : '1'); + if ($forceFullCalc === null) { + $objWriter->writeAttribute('forceFullCalc', $preCalculateFormulas ? '0' : '1'); + } else { + $objWriter->writeAttribute('forceFullCalc', $forceFullCalc ? '1' : '0'); + } $objWriter->endElement(); } diff --git a/src/PhpSpreadsheet/Writer/ZipStream2.php b/src/PhpSpreadsheet/Writer/ZipStream2.php index ed21a7e399..e03e1d0cf9 100644 --- a/src/PhpSpreadsheet/Writer/ZipStream2.php +++ b/src/PhpSpreadsheet/Writer/ZipStream2.php @@ -5,6 +5,12 @@ use ZipStream\Option\Archive; use ZipStream\ZipStream; +/** + * Either ZipStream2 or ZipStream3, but not both, may be used. + * For code coverage testing, it will always be ZipStream3. + * + * @codeCoverageIgnore + */ class ZipStream2 { /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php index da8d350e4a..0f177c81f4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -15,6 +16,8 @@ class AllSetupTeardown extends TestCase { protected string $compatibilityMode; + protected string $arrayReturnType; + private ?Spreadsheet $spreadsheet = null; private ?Worksheet $sheet = null; @@ -22,11 +25,13 @@ class AllSetupTeardown extends TestCase protected function setUp(): void { $this->compatibilityMode = Functions::getCompatibilityMode(); + $this->arrayReturnType = Calculation::getArrayReturnType(); } protected function tearDown(): void { Functions::setCompatibilityMode($this->compatibilityMode); + Calculation::setArrayReturnType($this->arrayReturnType); $this->sheet = null; if ($this->spreadsheet !== null) { $this->spreadsheet->disconnectWorksheets(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ChooseColsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ChooseColsTest.php new file mode 100644 index 0000000000..26c8eb45fb --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ChooseColsTest.php @@ -0,0 +1,54 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray( + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + null, + 'B3', + true + ); + $this->getSpreadsheet()->addNamedRange( + new NamedRange( + 'definedname', + $sheet, + '$B$3:$D$11' + ) + ); + + $sheet->setCellValue('F3', $formula); + $result = $sheet->getCell('F3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public static function providerChooseCols(): array + { + return require 'tests/data/Calculation/LookupRef/CHOOSECOLS.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ChooseRowsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ChooseRowsTest.php new file mode 100644 index 0000000000..3766ea203b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ChooseRowsTest.php @@ -0,0 +1,54 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray( + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + null, + 'B3', + true + ); + $this->getSpreadsheet()->addNamedRange( + new NamedRange( + 'definedname', + $sheet, + '$B$3:$D$11' + ) + ); + + $sheet->setCellValue('F3', $formula); + $result = $sheet->getCell('F3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public static function providerChooseRows(): array + { + return require 'tests/data/Calculation/LookupRef/CHOOSEROWS.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/DropTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/DropTest.php new file mode 100644 index 0000000000..0a8411464d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/DropTest.php @@ -0,0 +1,54 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray( + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + null, + 'B3', + true + ); + $this->getSpreadsheet()->addNamedRange( + new NamedRange( + 'definedname', + $sheet, + '$B$3:$D$11' + ) + ); + + $sheet->setCellValue('F3', $formula); + $result = $sheet->getCell('F3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public static function providerDrop(): array + { + return require 'tests/data/Calculation/LookupRef/DROP.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ExpandTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ExpandTest.php new file mode 100644 index 0000000000..b68c64005b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ExpandTest.php @@ -0,0 +1,47 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray( + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ], + null, + 'B3', + true + ); + $this->getSpreadsheet()->addNamedRange( + new NamedRange( + 'definedname', + $sheet, + '$B$3:$D$11' + ) + ); + + $sheet->setCellValue('F3', $formula); + $result = $sheet->getCell('F3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public static function providerExpand(): array + { + return require 'tests/data/Calculation/LookupRef/EXPAND.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TakeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TakeTest.php new file mode 100644 index 0000000000..d03187ecf0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TakeTest.php @@ -0,0 +1,54 @@ +mightHaveException($expectedResult); + $sheet = $this->getSheet(); + $sheet->fromArray( + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + null, + 'B3', + true + ); + $this->getSpreadsheet()->addNamedRange( + new NamedRange( + 'definedname', + $sheet, + '$B$3:$D$11' + ) + ); + + $sheet->setCellValue('F3', $formula); + $result = $sheet->getCell('F3')->getCalculatedValue(); + self::assertSame($expectedResult, $result); + } + + public static function providerTake(): array + { + return require 'tests/data/Calculation/LookupRef/TAKE.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php b/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php index 10da1dd21c..f15352aee9 100644 --- a/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php +++ b/tests/PhpSpreadsheetTests/Cell/ColumnRangeTest.php @@ -43,6 +43,7 @@ public function testCreateColumnRangeWithWorksheet(): void self::assertSame('E', $columnRange->to()); self::assertSame("'Mark''s Worksheet'!C:E", (string) $columnRange); self::assertSame("'Mark''s Worksheet'!C1:E1048576", (string) $columnRange->toCellRange()); + $spreadsheet->disconnectWorksheets(); } public function testCreateColumnRangeFromArray(): void @@ -88,4 +89,23 @@ public function testColumnRangePrevious(): void // Check that original Column Range isn't changed self::assertSame('C:E', (string) $columnRange); } + + public function testIssue4309(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $columnRange = new ColumnRange('A', 'A'); + $columnStyle = $sheet->getStyle($columnRange); + $columnStyle->applyFromArray([ + 'font' => ['bold' => true], + ]); + $columnXf = $sheet->getColumnDimension('A')->getXfIndex(); + self::assertNotNull($columnXf); + self::assertTrue( + $spreadsheet->getCellXfByIndex($columnXf) + ->getFont()->getBold() + ); + + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php b/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php index 52fd357f51..005f1af6b2 100644 --- a/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php +++ b/tests/PhpSpreadsheetTests/Cell/RowRangeTest.php @@ -39,6 +39,7 @@ public function testCreateRowRangeWithWorksheet(): void self::assertSame(3, $rowRange->from()); self::assertSame(5, $rowRange->to()); self::assertSame("'Mark''s Worksheet'!3:5", (string) $rowRange); + $spreadsheet->disconnectWorksheets(); } public function testCreateRowRangeFromArray(): void @@ -74,4 +75,20 @@ public function testRowRangePrevious(): void // Check that original Row Range isn't changed self::assertSame('3:5', (string) $rowRange); } + + public function testIssue4309(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $rowRange = new RowRange(1, 1); + $rowStyle = $sheet->getStyle($rowRange); + $rowStyle->applyFromArray([ + 'font' => ['name' => 'Arial'], + ]); + $rowXf = $sheet->getRowDimension(1)->getXfIndex(); + self::assertNotNull($rowXf); + self::assertSame('Arial', $spreadsheet->getCellXfByIndex($rowXf)->getFont()->getName()); + + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php b/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php index 7aa484d9cb..2546d60de6 100644 --- a/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php +++ b/tests/PhpSpreadsheetTests/Chart/ChartsDynamicTitleTest.php @@ -34,7 +34,6 @@ public function writeCharts(XlsxWriter $writer): void $writer->setIncludeCharts(true); } - #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] public function testDynamicTitle(): void { // based on samples/templates/issue.3797.2007.xlsx diff --git a/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php b/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php index 513576b19b..086d780f53 100644 --- a/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Csv/CsvNumberFormatLocaleTest.php @@ -6,8 +6,12 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Reader\Csv; +use PHPUnit\Framework\Attributes; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +// separate processes due to setLocale +#[Attributes\RunTestsInSeparateProcesses] class CsvNumberFormatLocaleTest extends TestCase { private bool $localeAdjusted; @@ -44,8 +48,7 @@ protected function tearDown(): void } } - #[\PHPUnit\Framework\Attributes\DataProvider('providerNumberFormatNoConversionTest')] - #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + #[DataProvider('providerNumberFormatNoConversionTest')] public function testNumberFormatNoConversion(mixed $expectedValue, string $expectedFormat, string $cellAddress): void { if (!$this->localeAdjusted) { @@ -85,8 +88,7 @@ public static function providerNumberFormatNoConversionTest(): array ]; } - #[\PHPUnit\Framework\Attributes\DataProvider('providerNumberValueConversionTest')] - #[\PHPUnit\Framework\Attributes\RunInSeparateProcess] + #[DataProvider('providerNumberValueConversionTest')] public function testNumberValueConversion(mixed $expectedValue, string $cellAddress): void { if (!$this->localeAdjusted) { diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php new file mode 100644 index 0000000000..b64f8fd1dc --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Ods/RepeatEmptyCellsAndRowsTest.php @@ -0,0 +1,48 @@ +getActiveSheet(); + $oldSheet->setCellValue('C1', 'xx'); + $oldSheet->setCellValue('G1', 'aa'); + $oldSheet->setCellValue('BB1', 'bb'); + $oldSheet->setCellValue('A6', 'aaa'); + $oldSheet->setCellValue('B7', 'bbb'); + $oldSheet->getRowDimension(10)->setRowHeight(12); + $oldSheet->setCellValue('A12', 'this is A12'); + $style = $oldSheet->getStyle('B14:D14'); + $style->getFont()->setBold(true); + $oldSheet->getCell('E15')->setValue('X'); + $oldSheet->mergeCells('E15:G16'); + $oldSheet->getCell('J15')->setValue('j15'); + $oldSheet->getCell('J16')->setValue('j16'); + $oldSheet->getCell('A19')->setValue('lastrow'); + $spreadsheet = $this->writeAndReload($spreadsheetOld, 'Ods'); + $spreadsheetOld->disconnectWorksheets(); + + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('xx', $sheet->getCell('C1')->getValue()); + self::assertSame('aa', $sheet->getCell('G1')->getValue()); + self::assertSame('bb', $sheet->getCell('BB1')->getValue()); + self::assertSame('aaa', $sheet->getCell('A6')->getValue()); + self::assertSame('bbb', $sheet->getCell('B7')->getValue()); + self::assertSame('this is A12', $sheet->getCell('A12')->getValue()); + // Read styles, including row height, not yet implemented for ODS + self::assertSame('j15', $sheet->getCell('J15')->getValue()); + self::assertSame('j16', $sheet->getCell('J16')->getValue()); + self::assertSame(['E15:G16' => 'E15:G16'], $sheet->getMergeCells()); + self::assertSame('lastrow', $sheet->getCell('A19')->getValue()); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php new file mode 100644 index 0000000000..da9b4f9c98 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/Biff8CoverTest.php @@ -0,0 +1,42 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('=SUM({1;2;3;4;5})', $sheet->getCell('A1')->getValue()); + self::assertSame(15, $sheet->getCell('A1')->getCalculatedValue()); + self::assertSame( + '=VLOOKUP("hello",' + . '{"what",1;"why",TRUE;"hello","there";"when",FALSE}' + . ',2,FALSE)', + $sheet->getCell('C1')->getValue() + ); + self::assertSame('there', $sheet->getCell('C1')->getCalculatedValue()); + self::assertSame(2, $sheet->getCell('A3')->getValue()); + self::assertTrue( + $sheet->getStyle('A3')->getFont()->getSuperscript() + ); + self::assertSame('n', $sheet->getCell('B3')->getValue()); + self::assertTrue( + $sheet->getStyle('B3')->getFont()->getSubscript() + ); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/SharedFormulaeTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/SharedFormulaeTest.php new file mode 100644 index 0000000000..b613fb2973 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/SharedFormulaeTest.php @@ -0,0 +1,37 @@ +load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $expected = [ + [1, '=A1+1', '=A1>3', '="x"&A1'], + [2, '=A2+1', '=A2>3', '="x"&A2'], + [3, '=A3+1', '=A3>3', '="x"&A3'], + [4, '=A4+1', '=A4>3', '="x"&A4'], + [5, '=A5+1', '=A5>3', '="x"&A5'], + ]; + self::assertSame($expected, $sheet->toArray(null, false, false)); + $expected = [ + [1, 2, false, 'x1'], + [2, 3, false, 'x2'], + [3, 4, false, 'x3'], + [4, 5, true, 'x4'], + [5, 6, true, 'x5'], + ]; + self::assertSame($expected, $sheet->toArray(null, true, false)); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php new file mode 100644 index 0000000000..c4178f53ab --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/StringHelperLocaleTest.php @@ -0,0 +1,55 @@ +currentLocale = setlocale(LC_ALL, '0'); + } + + protected function tearDown(): void + { + if (is_string($this->currentLocale)) { + setlocale(LC_ALL, $this->currentLocale); + } + StringHelper::setCurrencyCode(null); + } + + public function testCurrency(): void + { + if ($this->currentLocale === false || !setlocale(LC_ALL, 'de_DE.UTF-8', 'deu_deu.utf8')) { + self::markTestSkipped('Unable to set German UTF8 locale for testing.'); + } + $result = StringHelper::getCurrencyCode(); + self::assertSame('€', $result); + if (!setlocale(LC_ALL, $this->currentLocale)) { + self::markTestSkipped('Unable to restore default locale.'); + } + $result = StringHelper::getCurrencyCode(); + self::assertSame('€', $result, 'result persists despite locale change'); + StringHelper::setCurrencyCode(null); + $result = StringHelper::getCurrencyCode(); + self::assertSame('$', $result, 'locale now used'); + StringHelper::setCurrencyCode(null); + if (!setlocale(LC_ALL, 'deu_deu', 'de_DE@euro')) { + self::markTestSkipped('Unable to set German single-byte locale for testing.'); + } + $result = StringHelper::getCurrencyCode(true); // trim if alt symbol is used + self::assertSame('EUR', $result, 'non-UTF8 result ignored'); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Issue641Test.php b/tests/PhpSpreadsheetTests/Worksheet/Issue641Test.php new file mode 100644 index 0000000000..bbb6c8bf00 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Issue641Test.php @@ -0,0 +1,82 @@ +removeSheetByIndex(0); + $availableWs = []; + + $worksheet = $xlsx->createSheet(); + $worksheet->setTitle('Condensed A'); + $worksheet->getCell('A1')->setValue("=SUM('Detailed A'!A1:A10)"); + $worksheet->getCell('A2')->setValue(mt_rand(1, 30)); + $availableWs[] = 'Condensed A'; + + $worksheet = $xlsx->createSheet(); + $worksheet->setTitle('Condensed B'); + $worksheet->getCell('A1')->setValue("=SUM('Detailed B'!A1:A10)"); + $worksheet->getCell('A2')->setValue(mt_rand(1, 30)); + $availableWs[] = 'Condensed B'; + + // at this point the value in worksheet 'Condensed B' cell A1 is + // =SUM('Detailed B'!A1:A10) + + // worksheet in question is cloned and totals are attached + $totalWs1 = clone $xlsx->getSheet($xlsx->getSheetCount() - 1); + $totalWs1->setTitle('Condensed Total'); + $xlsx->addSheet($totalWs1); + $formula = '='; + foreach ($availableWs as $ws) { + $formula .= sprintf("+'%s'!A2", $ws); + } + $totalWs1->getCell('A1')->setValue("=SUM('Detailed Total'!A1:A10)"); + $totalWs1->getCell('A2')->setValue($formula); + + $availableWs = []; + + $worksheet = $xlsx->createSheet(); + $worksheet->setTitle('Detailed A'); + for ($step = 1; $step <= 10; ++$step) { + $worksheet->getCell("A{$step}")->setValue(mt_rand(1, 30)); + } + $availableWs[] = 'Detailed A'; + + $worksheet = $xlsx->createSheet(); + $worksheet->setTitle('Detailed B'); + for ($step = 1; $step <= 10; ++$step) { + $worksheet->getCell("A{$step}")->setValue(mt_rand(1, 30)); + } + $availableWs[] = 'Detailed B'; + + $totalWs2 = clone $xlsx->getSheet($xlsx->getSheetCount() - 1); + $totalWs2->setTitle('Detailed Total'); + $xlsx->addSheet($totalWs2); + + for ($step = 1; $step <= 10; ++$step) { + $formula = '='; + foreach ($availableWs as $ws) { + $formula .= sprintf("+'%s'!A%s", $ws, $step); + } + $totalWs2->getCell("A{$step}")->setValue($formula); + } + + self::assertSame("=SUM('Detailed A'!A1:A10)", $xlsx->getSheetByName('Condensed A')?->getCell('A1')?->getValue()); + self::assertSame("=SUM('Detailed B'!A1:A10)", $xlsx->getSheetByName('Condensed B')?->getCell('A1')?->getValue()); + self::assertSame("=SUM('Detailed Total'!A1:A10)", $xlsx->getSheetByName('Condensed Total')?->getCell('A1')?->getValue()); + self::assertSame("=+'Detailed A'!A1+'Detailed B'!A1", $xlsx->getSheetByName('Detailed Total')?->getCell('A1')?->getValue()); + + $xlsx->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php new file mode 100644 index 0000000000..a9ef67b791 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadCustomPropertyTest.php @@ -0,0 +1,23 @@ +load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('', $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php new file mode 100644 index 0000000000..1f12bce570 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkBaseTest.php @@ -0,0 +1,23 @@ +load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString('', $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php new file mode 100644 index 0000000000..669594bb1c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/BadHyperlinkTest.php @@ -0,0 +1,23 @@ +load($infile); + $writer = new HtmlWriter($spreadsheet); + $html = $writer->generateHtmlAll(); + self::assertStringContainsString("jav\tascript:alert()", $html); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php b/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php new file mode 100644 index 0000000000..7b6a62baa2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Ods/IndentTest.php @@ -0,0 +1,62 @@ +compatibilityMode = Functions::getCompatibilityMode(); + Functions::setCompatibilityMode( + Functions::COMPATIBILITY_OPENOFFICE + ); + } + + protected function tearDown(): void + { + parent::tearDown(); + Functions::setCompatibilityMode($this->compatibilityMode); + } + + public function testWriteSpreadsheet(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', 'aa'); + $sheet->setCellValue('B1', 'bb'); + $sheet->setCellValue('A2', 'cc'); + $sheet->setCellValue('B2', 'dd'); + $sheet->getStyle('A1')->getAlignment()->setIndent(2); + $writer = new Ods($spreadsheet); + $content = new Content($writer); + $xml = $content->write(); + self::assertStringContainsString( + '' + . '' + . '' + . '', + $xml + ); + self::assertStringContainsString( + '' + . '' + . '' // fo:margin-left is what we're looking for + . '' + . '', + $xml + ); + self::assertSame(3, substr_count($xml, 'table:style-name="ce0"')); + self::assertSame(1, substr_count($xml, 'table:style-name="ce1"')); + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php new file mode 100644 index 0000000000..ffe9e30ae0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue4269Test.php @@ -0,0 +1,65 @@ +outputFile !== '') { + unlink($this->outputFile); + $this->outputFile = ''; + } + } + + #[DataProvider('validationProvider')] + public function testWriteArrayFormulaTextJoin( + bool $preCalculateFormulas, + ?bool $forceFullCalc, + string $expected + ): void { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A1', '=A2*2'); + $sheet->setCellValue('A2', 0); + + $writer = new XlsxWriter($spreadsheet); + $writer->setPreCalculateFormulas($preCalculateFormulas); + if ($forceFullCalc !== null) { + $writer->setForceFullCalc($forceFullCalc); + } + $this->outputFile = File::temporaryFilename(); + $writer->save($this->outputFile); + $spreadsheet->disconnectWorksheets(); + + $file = 'zip://'; + $file .= $this->outputFile; + $file .= '#xl/workbook.xml'; + $data = file_get_contents($file); + if ($data === false) { + self::fail('Unable to read file'); + } else { + self::assertStringContainsString($expected, $data); + } + } + + public static function validationProvider(): array + { + return [ + 'normal case' => [true, null, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="0"'], + 'issue 456' => [false, null, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="1"'], + 'better choice for no precalc' => [false, false, 'calcMode="auto" calcCompleted="0" fullCalcOnLoad="1" forceFullCalc="0"'], + 'unlikely use case' => [true, true, 'calcMode="auto" calcCompleted="1" fullCalcOnLoad="0" forceFullCalc="1"'], + ]; + } +} diff --git a/tests/data/Calculation/LookupRef/CHOOSECOLS.php b/tests/data/Calculation/LookupRef/CHOOSECOLS.php new file mode 100644 index 0000000000..b548382cc9 --- /dev/null +++ b/tests/data/Calculation/LookupRef/CHOOSECOLS.php @@ -0,0 +1,180 @@ + [ + [ + ['c'], + ['f'], + ['i'], + ['l'], + ['o'], + ['r'], + ['u'], + ['x'], + ['#'], + ], + '=CHOOSECOLS(B3:D11, -1)', + ], + 'select 1 column' => [ + [ + ['b'], + ['e'], + ['h'], + ['k'], + ['n'], + ['q'], + ['t'], + ['w'], + ['z'], + ], + '=CHOOSECOLS(B3:D11, 2)', + ], + 'fractional column' => [ + [ + ['b'], + ['e'], + ['h'], + ['k'], + ['n'], + ['q'], + ['t'], + ['w'], + ['z'], + ], + '=CHOOSECOLS(B3:D11, 2.8)', + ], + 'numeric string column' => [ + [ + ['b'], + ['e'], + ['h'], + ['k'], + ['n'], + ['q'], + ['t'], + ['w'], + ['z'], + ], + '=CHOOSECOLS(B3:D11, " 2.8 ")', + ], + 'multiple columns including duplicates' => [ + [ + ['b', 'a', 'b'], + ['e', 'd', 'e'], + ['h', 'g', 'h'], + ['k', 'j', 'k'], + ['n', 'm', 'n'], + ['q', 'p', 'q'], + ['t', 's', 't'], + ['w', 'v', 'w'], + ['z', 'y', 'z'], + ], + '=CHOOSECOLS(B3:D11, 2, 1, 2)', + ], + 'multiple columns mixed +/- mixed scalar/matrix' => [ + [ + ['b', 'a', 'b'], + ['e', 'd', 'e'], + ['h', 'g', 'h'], + ['k', 'j', 'k'], + ['n', 'm', 'n'], + ['q', 'p', 'q'], + ['t', 's', 't'], + ['w', 'v', 'w'], + ['z', 'y', 'z'], + ], + '=CHOOSECOLS(B3:D11, 2, {-3, 2})', + ], + 'reverse Columns' => [ + [ + ['c', 'b', 'a'], + ['f', 'e', 'd'], + ['i', 'h', 'g'], + ['l', 'k', 'j'], + ['o', 'n', 'm'], + ['r', 'q', 'p'], + ['u', 't', 's'], + ['x', 'w', 'v'], + ['#', 'z', 'y'], + ], + '=CHOOSECOLS(B3:D11, SEQUENCE(COLUMNS(B3:D11),,COLUMNS(B3:D11),-1))', + ], + 'inline array' => [ + [ + ['c', 'b'], + ['f', 'e'], + ['i', 'h'], + ['l', 'k'], + ['o', 'n'], + ['r', 'q'], + ['u', 't'], + ['x', 'w'], + ['#', 'z'], + ], + '=CHOOSECOLS(B3:D11, {3;2})', + ], + 'inline array with negative numbers' => [ + [ + ['b', 'c'], + ['e', 'f'], + ['h', 'i'], + ['k', 'l'], + ['n', 'o'], + ['q', 'r'], + ['t', 'u'], + ['w', 'x'], + ['z', '#'], + ], + '=CHOOSECOLS(B3:D11, {-2;-1})', + ], + 'defined name' => [ + [ + ['b', 'c'], + ['e', 'f'], + ['h', 'i'], + ['k', 'l'], + ['n', 'o'], + ['q', 'r'], + ['t', 'u'], + ['w', 'x'], + ['z', '#'], + ], + '=CHOOSECOLS(definedname, {-2;-1})', + ], + 'only 1 argument' => [ + 'exception', + '=CHOOSECOLS(B3:D11)', + ], + 'non-numeric column' => [ + '#VALUE!', + '=CHOOSECOLS(B3:D11, "x")', + ], + 'positive column too large' => [ + '#VALUE!', + '=CHOOSECOLS(B3:D11, 4)', + ], + 'negative column too large' => [ + '#VALUE!', + '=CHOOSECOLS(B3:D11, 1, -4)', + ], + 'zero column' => [ + '#VALUE!', + '=CHOOSECOLS(B3:D11, 0)', + ], + 'single cell' => [ + [ + ['a'], + ], + '=CHOOSECOLS(B3, 1)', + ], + 'inline array rather than range' => [ + [ + [2, 1, 1], + [4, 3, 3], + [6, 5, 5], + ], + '=CHOOSECOLS({1,2;3,4;5,6}, " 2.4 ", 1, 1)', + ], +]; diff --git a/tests/data/Calculation/LookupRef/CHOOSEROWS.php b/tests/data/Calculation/LookupRef/CHOOSEROWS.php new file mode 100644 index 0000000000..1f0e456268 --- /dev/null +++ b/tests/data/Calculation/LookupRef/CHOOSEROWS.php @@ -0,0 +1,115 @@ + [ + [ + ['y', 'z', '#'], + ], + '=CHOOSEROWS(B3:D11, -1)', + ], + 'fractional row' => [ + [ + ['y', 'z', '#'], + ], + '=CHOOSEROWS(B3:D11, -1.8)', + ], + 'numeric string row' => [ + [ + ['y', 'z', '#'], + ], + '=CHOOSEROWS(B3:D11, " -1.8 ")', + ], + 'select 1 row' => [ + [ + ['d', 'e', 'f'], + ], + '=CHOOSEROWS(B3:D11, 2)', + ], + 'multiple rows including duplicates' => [ + [ + ['d', 'e', 'f'], + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ], + '=CHOOSEROWS(B3:D11, 2, 1, 2)', + ], + 'multiple rows mixed +/- mixed scalar/matrix' => [ + [ + ['d', 'e', 'f'], + ['s', 't', 'u'], + ['d', 'e', 'f'], + ], + '=CHOOSEROWS(B3:D11, 2, {-3; 2})', + ], + 'reverse Rows' => [ + [ + ['y', 'z', '#'], + ['v', 'w', 'x'], + ['s', 't', 'u'], + ['p', 'q', 'r'], + ['m', 'n', 'o'], + ['j', 'k', 'l'], + ['g', 'h', 'i'], + ['d', 'e', 'f'], + ['a', 'b', 'c'], + ], + '=CHOOSEROWS(B3:D11, SEQUENCE(ROWS(B3:D11),,ROWS(B3:D11),-1))', + ], + 'inline array' => [ + [ + ['g', 'h', 'i'], + ['d', 'e', 'f'], + ], + '=CHOOSEROWS(B3:D11, {3;2})', + ], + 'inline array with negative numbers' => [ + [ + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=CHOOSEROWS(B3:D11, {-2;-1})', + ], + 'named range' => [ + [ + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=CHOOSEROWS(definedname, {-2;-1})', + ], + 'only 1 argument' => [ + 'exception', + '=CHOOSEROWS(B3:D11)', + ], + 'non-numeric row' => [ + '#VALUE!', + '=CHOOSEROWS(B3:D11, "x")', + ], + 'positive row too large' => [ + '#VALUE!', + '=CHOOSEROWS(B3:D11, 10)', + ], + 'negative row too large' => [ + '#VALUE!', + '=CHOOSEROWS(B3:D11, 1, -10)', + ], + 'zero row' => [ + '#VALUE!', + '=CHOOSEROWS(B3:D11, 0)', + ], + 'single cell' => [ + [ + ['a'], + ], + '=CHOOSEROWS(B3, 1)', + ], + 'inline array rather than range' => [ + [ + [3, 4], + [1, 2], + [1, 2], + ], + '=CHOOSEROWS({1,2;3,4;5,6}, " 2.4 ", 1, 1)', + ], +]; diff --git a/tests/data/Calculation/LookupRef/DROP.php b/tests/data/Calculation/LookupRef/DROP.php new file mode 100644 index 0000000000..628315772e --- /dev/null +++ b/tests/data/Calculation/LookupRef/DROP.php @@ -0,0 +1,178 @@ + [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ], + '=DROP(B3:D11, -1)', + ], + 'drop first 2 rows' => [ + [ + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=DROP(B3:D11, 2)', + ], + 'drop last 2 columns' => [ + [ + ['a'], + ['d'], + ['g'], + ['j'], + ['m'], + ['p'], + ['s'], + ['v'], + ['y'], + ], + '=DROP(B3:D11, , -2)', + ], + 'drop first column' => [ + [ + ['b', 'c'], + ['e', 'f'], + ['h', 'i'], + ['k', 'l'], + ['n', 'o'], + ['q', 'r'], + ['t', 'u'], + ['w', 'x'], + ['z', '#'], + ], + '=DROP(B3:D11, , 1)', + ], + 'drop last row first column' => [ + [ + ['b', 'c'], + ['e', 'f'], + ['h', 'i'], + ['k', 'l'], + ['n', 'o'], + ['q', 'r'], + ['t', 'u'], + ['w', 'x'], + ], + '=DROP(B3:D11, -1, 1)', + ], + 'drop first 2 rows last 2 columns' => [ + [ + ['g'], + ['j'], + ['m'], + ['p'], + ['s'], + ['v'], + ['y'], + ], + '=DROP(B3:D11, 2, -2)', + ], + 'drop first 2 rows last 2 columns fractional and string' => [ + [ + ['g'], + ['j'], + ['m'], + ['p'], + ['s'], + ['v'], + ['y'], + ], + '=DROP(B3:D11, 2.8, " -2.8 ")', + ], + 'named range' => [ + [ + ['g'], + ['j'], + ['m'], + ['p'], + ['s'], + ['v'], + ['y'], + ], + '=DROP(definedname, 2,-2)', + ], + 'only 1 argument' => [ + 'exception', + '=DROP(B3:D11)', + ], + 'non-numeric row' => [ + '#VALUE!', + '=DROP(B3:D11, "x")', + ], + 'non-numeric column' => [ + '#VALUE!', + '=DROP(B3:D11, 1, "x")', + ], + 'positive row too large' => [ + '#VALUE!', + '=DROP(B3:D11, 20)', + ], + 'negative row too large' => [ + '#VALUE!', + '=DROP(B3:D11, -20)', + ], + 'positive column too large' => [ + '#VALUE!', + '=DROP(B3:D11, , 20)', + ], + 'negative column too large' => [ + '#VALUE!', + '=DROP(B3:D11, , -20)', + ], + 'row okay column too large' => [ + '#VALUE!', + '=DROP(B3:D11, -1, 20)', + ], + 'zero row' => [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=DROP(B3:D11, 0)', + ], + 'zero column' => [ + [ + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=DROP(B3:D11, 1, 0)', + ], + 'single cell' => [ + '#VALUE!', + '=DROP(B3, 1)', + ], + 'inline array rather than range' => [ + [ + [4], + [6], + ], + '=DROP({1,2;3,4;5,6}, 1, 1)', + ], +]; diff --git a/tests/data/Calculation/LookupRef/EXPAND.php b/tests/data/Calculation/LookupRef/EXPAND.php new file mode 100644 index 0000000000..d00f7a7005 --- /dev/null +++ b/tests/data/Calculation/LookupRef/EXPAND.php @@ -0,0 +1,74 @@ + [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['#N/A', '#N/A', '#N/A'], + ['#N/A', '#N/A', '#N/A'], + ], + '=EXPAND(B3:D4, 4)', + ], + 'Add 1 colun' => [ + [ + ['a', 'b', 'c', '#N/A'], + ['d', 'e', 'f', '#N/A'], + ], + '=EXPAND(B3:D4, , 4)', + ], + 'Add 1 row 2 columns set cells to 0' => [ + [ + ['a', 'b', 'c', 0, 0], + ['d', 'e', 'f', 0, 0], + [0, 0, 0, 0, 0], + ], + '=EXPAND(B3:D4, 3, 5, 0)', + ], + 'Fractional row and column' => [ + [ + ['a', 'b', 'c', 0, 0], + ['d', 'e', 'f', 0, 0], + [0, 0, 0, 0, 0], + ], + '=EXPAND(B3:D4, 3.2, 5.8, 0)', + ], + 'only 1 argument' => [ + 'exception', + '=EXPAND(B3:D4)', + ], + 'non-numeric row' => [ + '#VALUE!', + '=EXPAND(B3:D4, "x")', + ], + 'non-numeric column' => [ + '#VALUE!', + '=EXPAND(B3:D4, 1, "x")', + ], + 'row too small' => [ + '#VALUE!', + '=EXPAND(B3:D4, 1)', + ], + 'column too small' => [ + '#VALUE!', + '=EXPAND(B3:D4, , 2)', + ], + 'single cell' => [ + [ + ['a', 'xx'], + ['xx', 'xx'], + ], + '=EXPAND(B3, 2, 2, "xx")', + ], + 'inline array rather than range' => [ + [ + [1, 2, 0, 0], + [3, 4, 0, 0], + [5, 6, 0, 0], + [0, 0, 0, 0], + ], + '=EXPAND({1,2;3,4;5,6}, 4.4, " 4.5 ", 0)', + ], +]; diff --git a/tests/data/Calculation/LookupRef/TAKE.php b/tests/data/Calculation/LookupRef/TAKE.php new file mode 100644 index 0000000000..b5ed8cf472 --- /dev/null +++ b/tests/data/Calculation/LookupRef/TAKE.php @@ -0,0 +1,168 @@ + [ + [ + ['y', 'z', '#'], + ], + '=TAKE(B3:D11, -1)', + ], + 'take first 2 rows' => [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ], + '=TAKE(B3:D11, 2)', + ], + 'take last 2 columns' => [ + [ + ['b', 'c'], + ['e', 'f'], + ['h', 'i'], + ['k', 'l'], + ['n', 'o'], + ['q', 'r'], + ['t', 'u'], + ['w', 'x'], + ['z', '#'], + ], + '=TAKE(B3:D11, , -2)', + ], + 'take first column' => [ + [ + ['a'], + ['d'], + ['g'], + ['j'], + ['m'], + ['p'], + ['s'], + ['v'], + ['y'], + ], + '=TAKE(B3:D11, , 1)', + ], + 'take last row first column' => [ + [ + ['y'], + ], + '=TAKE(B3:D11, -1, 1)', + ], + 'take first 2 rows last 2 columns' => [ + [ + ['b', 'c'], + ['e', 'f'], + ], + '=TAKE(B3:D11, 2, -2)', + ], + 'take first 2 rows last 2 columns fractional and string' => [ + [ + ['b', 'c'], + ['e', 'f'], + ], + '=TAKE(B3:D11, 2.8, " -2.8 ")', + ], + 'named range' => [ + [ + ['x'], + ['#'], + ], + '=TAKE(definedname, -2,-1)', + ], + 'only 1 argument' => [ + 'exception', + '=TAKE(B3:D11)', + ], + 'non-numeric row' => [ + '#VALUE!', + '=TAKE(B3:D11, "x")', + ], + 'non-numeric column' => [ + '#VALUE!', + '=TAKE(B3:D11, 1, "x")', + ], + 'positive row too large' => [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=TAKE(B3:D11, 20)', + ], + 'negative row too large' => [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=TAKE(B3:D11, -20)', + ], + 'positive column too large' => [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=TAKE(B3:D11, , 20)', + ], + 'negative column too large' => [ + [ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ['g', 'h', 'i'], + ['j', 'k', 'l'], + ['m', 'n', 'o'], + ['p', 'q', 'r'], + ['s', 't', 'u'], + ['v', 'w', 'x'], + ['y', 'z', '#'], + ], + '=TAKE(B3:D11, , -20)', + ], + 'row okay column too large' => [ + [ + ['y', 'z', '#'], + ], + '=TAKE(B3:D11, -1, 20)', + ], + 'zero row' => [ + '#VALUE!', + '=TAKE(B3:D11, 0)', + ], + 'zero column' => [ + '#VALUE!', + '=TAKE(B3:D11, 1, 0)', + ], + 'single cell' => [ + [ + ['a'], + ], + '=TAKE(B3, 1)', + ], + 'inline array rather than range' => [ + [ + [1, 2], + ], + '=TAKE({1,2;3,4;5,6}, 1, 2)', + ], +]; diff --git a/tests/data/Reader/XLS/biff8cover.xls b/tests/data/Reader/XLS/biff8cover.xls new file mode 100644 index 0000000000..92da6e12fa Binary files /dev/null and b/tests/data/Reader/XLS/biff8cover.xls differ diff --git a/tests/data/Reader/XLSX/sec-j47r.dontuse b/tests/data/Reader/XLSX/sec-j47r.dontuse new file mode 100644 index 0000000000..9914b1ffd1 Binary files /dev/null and b/tests/data/Reader/XLSX/sec-j47r.dontuse differ diff --git a/tests/data/Reader/XLSX/sec-p66w.dontuse b/tests/data/Reader/XLSX/sec-p66w.dontuse new file mode 100644 index 0000000000..7257bfc9d3 Binary files /dev/null and b/tests/data/Reader/XLSX/sec-p66w.dontuse differ diff --git a/tests/data/Reader/XLSX/sec-q229.dontuse b/tests/data/Reader/XLSX/sec-q229.dontuse new file mode 100644 index 0000000000..edb9024987 Binary files /dev/null and b/tests/data/Reader/XLSX/sec-q229.dontuse differ diff --git a/tests/data/Reader/XLSX/sharedformulae.xlsx b/tests/data/Reader/XLSX/sharedformulae.xlsx new file mode 100644 index 0000000000..eab8198b34 Binary files /dev/null and b/tests/data/Reader/XLSX/sharedformulae.xlsx differ diff --git a/tests/data/Writer/Ods/content-arrays.xml b/tests/data/Writer/Ods/content-arrays.xml index a33b7dbfca..8e72601a59 100644 --- a/tests/data/Writer/Ods/content-arrays.xml +++ b/tests/data/Writer/Ods/content-arrays.xml @@ -1,2 +1,44 @@ -11133 \ No newline at end of file + + + + + + + + + + + + + + + + + + + +1 + + +1 + + + + +1 + + +3 + + + + +3 + + + + + + + \ No newline at end of file diff --git a/tests/data/Writer/Ods/content-empty.xml b/tests/data/Writer/Ods/content-empty.xml index 84f4c23977..e9015af896 100644 --- a/tests/data/Writer/Ods/content-empty.xml +++ b/tests/data/Writer/Ods/content-empty.xml @@ -3,12 +3,11 @@ - + - @@ -17,10 +16,6 @@ - - - - diff --git a/tests/data/Writer/Ods/content-hidden-worksheet.xml b/tests/data/Writer/Ods/content-hidden-worksheet.xml index 88a53257a1..b583466701 100644 --- a/tests/data/Writer/Ods/content-hidden-worksheet.xml +++ b/tests/data/Writer/Ods/content-hidden-worksheet.xml @@ -3,15 +3,14 @@ - + - + - @@ -24,7 +23,6 @@ 1 - @@ -33,7 +31,6 @@ 2 - diff --git a/tests/data/Writer/Ods/content-with-data.xml b/tests/data/Writer/Ods/content-with-data.xml index db7d75a747..bde23e29d3 100644 --- a/tests/data/Writer/Ods/content-with-data.xml +++ b/tests/data/Writer/Ods/content-with-data.xml @@ -3,65 +3,54 @@ - + - + - - - - - - - - - - - @@ -92,7 +81,6 @@ Lorem ipsum - @@ -107,10 +95,6 @@ 42798.572060185 - - - - @@ -119,7 +103,6 @@ 2 -